Improve Recordings loading (#10462)

* Show skeleton until video players finishes loading

* Clean up android logic

* Ensure mobile view video is consistent

* Cleanup

* Only show when not scrubbing

* Don't use loading

* Start preview at correct time too

* Fix react race condition

* Be wait for seek to show video player
This commit is contained in:
Nicolas Mowen 2024-03-15 06:52:38 -06:00 committed by GitHub
parent d882cb0f63
commit c66f552280
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 63 additions and 18 deletions

View File

@ -42,6 +42,7 @@ type HlsVideoPlayerProps = {
onClipEnded?: () => void; onClipEnded?: () => void;
onPlayerLoaded?: () => void; onPlayerLoaded?: () => void;
onTimeUpdate?: (time: number) => void; onTimeUpdate?: (time: number) => void;
onPlaying?: () => void;
}; };
export default function HlsVideoPlayer({ export default function HlsVideoPlayer({
className, className,
@ -51,6 +52,7 @@ export default function HlsVideoPlayer({
onClipEnded, onClipEnded,
onPlayerLoaded, onPlayerLoaded,
onTimeUpdate, onTimeUpdate,
onPlaying,
}: HlsVideoPlayerProps) { }: HlsVideoPlayerProps) {
// playback // playback
@ -183,6 +185,7 @@ export default function HlsVideoPlayer({
setMobileCtrlTimeout(setTimeout(() => setControls(false), 4000)); setMobileCtrlTimeout(setTimeout(() => setControls(false), 4000));
} }
}} }}
onPlaying={onPlaying}
onPause={() => { onPause={() => {
setIsPlaying(false); setIsPlaying(false);

View File

@ -272,26 +272,25 @@ class PreviewVideoController extends PreviewController {
} }
if (this.timeToSeek) { if (this.timeToSeek) {
if ( const diff =
Math.round(this.previewRef.current.currentTime + this.preview.start) != Math.round(this.timeToSeek) -
Math.round(this.timeToSeek) Math.round(this.previewRef.current.currentTime + this.preview.start);
) {
if (isAndroid) {
const currentTs =
this.previewRef.current.currentTime + this.preview.start;
const diff = this.timeToSeek - currentTs;
if (Math.abs(diff) > 1) {
let seekTime;
if (isAndroid) {
if (diff < 30) { if (diff < 30) {
this.previewRef.current.currentTime = seekTime = Math.round(
this.previewRef.current.currentTime + diff / 2; this.previewRef.current.currentTime + diff / 2,
);
} else { } else {
this.previewRef.current.currentTime = seekTime = Math.round(this.timeToSeek - this.preview.start);
this.timeToSeek - this.preview.start;
} }
} else { } else {
this.previewRef.current.currentTime = seekTime = this.timeToSeek - this.preview.start;
this.timeToSeek - this.preview.start;
} }
this.previewRef.current.currentTime = seekTime;
} else { } else {
this.seeking = false; this.seeking = false;
this.timeToSeek = undefined; this.timeToSeek = undefined;

View File

@ -81,13 +81,27 @@ export class DynamicVideoController {
this.playerController.currentTime = seekSeconds; this.playerController.currentTime = seekSeconds;
if (play) { if (play) {
this.playerController.play(); this.waitAndPlay();
} else { } else {
this.playerController.pause(); this.playerController.pause();
} }
} }
} }
waitAndPlay() {
return new Promise((resolve) => {
const onSeekedHandler = () => {
this.playerController.removeEventListener("seeked", onSeekedHandler);
this.playerController.play();
resolve(undefined);
};
this.playerController.addEventListener("seeked", onSeekedHandler, {
once: true,
});
});
}
seekToTimelineItem(timeline: Timeline) { seekToTimelineItem(timeline: Timeline) {
this.playerController.pause(); this.playerController.pause();
this.seekToTimestamp(timeline.timestamp + this.annotationOffset); this.seekToTimestamp(timeline.timestamp + this.annotationOffset);

View File

@ -90,12 +90,19 @@ export default function DynamicVideoPlayer({
// initial state // initial state
const [isLoading, setIsLoading] = useState(false);
const [source, setSource] = useState( const [source, setSource] = useState(
`${apiHost}vod/${camera}/start/${timeRange.start}/end/${timeRange.end}/master.m3u8`, `${apiHost}vod/${camera}/start/${timeRange.start}/end/${timeRange.end}/master.m3u8`,
); );
// start at correct time // start at correct time
useEffect(() => {
if (isScrubbing) {
setIsLoading(true);
}
}, [isScrubbing]);
const onPlayerLoaded = useCallback(() => { const onPlayerLoaded = useCallback(() => {
if (!controller || !startTimestamp) { if (!controller || !startTimestamp) {
return; return;
@ -140,6 +147,7 @@ export default function DynamicVideoPlayer({
setSource( setSource(
`${apiHost}vod/${camera}/start/${timeRange.start}/end/${timeRange.end}/master.m3u8`, `${apiHost}vod/${camera}/start/${timeRange.start}/end/${timeRange.end}/master.m3u8`,
); );
setIsLoading(true);
controller.newPlayback({ controller.newPlayback({
recordings: recordings ?? [], recordings: recordings ?? [],
@ -151,14 +159,17 @@ export default function DynamicVideoPlayer({
return ( return (
<div className={`relative ${className ?? ""} cursor-pointer`}> <div className={`relative ${className ?? ""} cursor-pointer`}>
<div className={`w-full relative ${isScrubbing ? "hidden" : "visible"}`}> <div
className={`w-full relative ${isScrubbing || isLoading ? "hidden" : "visible"}`}
>
<HlsVideoPlayer <HlsVideoPlayer
className={` ${wideVideo ? "" : "aspect-video"}`} className={`${wideVideo ? "" : "aspect-video"}`}
videoRef={playerRef} videoRef={playerRef}
currentSource={source} currentSource={source}
onTimeUpdate={onTimeUpdate} onTimeUpdate={onTimeUpdate}
onPlayerLoaded={onPlayerLoaded} onPlayerLoaded={onPlayerLoaded}
onClipEnded={onClipEnded} onClipEnded={onClipEnded}
onPlaying={() => setIsLoading(false)}
> >
{config && focusedItem && ( {config && focusedItem && (
<TimelineEventOverlay <TimelineEventOverlay
@ -169,10 +180,11 @@ export default function DynamicVideoPlayer({
</HlsVideoPlayer> </HlsVideoPlayer>
</div> </div>
<PreviewPlayer <PreviewPlayer
className={`${isScrubbing ? "visible" : "hidden"} ${className ?? ""}`} className={`${isScrubbing || isLoading ? "visible" : "hidden"} ${className ?? ""}`}
camera={camera} camera={camera}
timeRange={timeRange} timeRange={timeRange}
cameraPreviews={cameraPreviews} cameraPreviews={cameraPreviews}
startTime={startTimestamp}
onControllerReady={(previewController) => { onControllerReady={(previewController) => {
setPreviewController(previewController); setPreviewController(previewController);
}} }}

View File

@ -301,6 +301,7 @@ export function MobileRecordingView({
relevantPreviews, relevantPreviews,
allCameras, allCameras,
}: MobileRecordingViewProps) { }: MobileRecordingViewProps) {
const { data: config } = useSWR<FrigateConfig>("config");
const navigate = useNavigate(); const navigate = useNavigate();
const contentRef = useRef<HTMLDivElement | null>(null); const contentRef = useRef<HTMLDivElement | null>(null);
@ -310,6 +311,21 @@ export function MobileRecordingView({
const [playbackCamera, setPlaybackCamera] = useState(startCamera); const [playbackCamera, setPlaybackCamera] = useState(startCamera);
const [playbackStart, setPlaybackStart] = useState(startTime); const [playbackStart, setPlaybackStart] = useState(startTime);
const grow = useMemo(() => {
if (!config) {
return "aspect-video";
}
const aspectRatio =
config.cameras[playbackCamera].detect.width /
config.cameras[playbackCamera].detect.height;
if (aspectRatio > 2) {
return "aspect-wide";
} else {
return "aspect-video";
}
}, [config, playbackCamera]);
// timeline time // timeline time
const timeRange = useMemo(() => getChunkedTimeDay(startTime), [startTime]); const timeRange = useMemo(() => getChunkedTimeDay(startTime), [startTime]);
@ -453,6 +469,7 @@ export function MobileRecordingView({
<div> <div>
<DynamicVideoPlayer <DynamicVideoPlayer
className={`w-full ${grow}`}
camera={playbackCamera} camera={playbackCamera}
timeRange={currentTimeRange} timeRange={currentTimeRange}
cameraPreviews={relevantPreviews || []} cameraPreviews={relevantPreviews || []}