diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index d66ed4b53..7e71748a2 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -17,7 +17,7 @@ import { toast } from "sonner"; import { useOverlayState } from "@/hooks/use-overlay-state"; import { usePersistence } from "@/hooks/use-persistence"; import { cn } from "@/lib/utils"; -import { ASPECT_VERTICAL_LAYOUT } from "@/types/record"; +import { ASPECT_VERTICAL_LAYOUT, RecordingPlayerError } from "@/types/record"; // Android native hls does not seek correctly const USE_NATIVE_HLS = !isAndroid; @@ -29,6 +29,7 @@ const unsupportedErrorCodes = [ type HlsVideoPlayerProps = { videoRef: MutableRefObject; + containerRef?: React.MutableRefObject; visible: boolean; currentSource: string; hotKeys: boolean; @@ -40,10 +41,11 @@ type HlsVideoPlayerProps = { setFullResolution?: React.Dispatch>; onUploadFrame?: (playTime: number) => Promise | undefined; toggleFullscreen?: () => void; - containerRef?: React.MutableRefObject; + onError?: (error: RecordingPlayerError) => void; }; export default function HlsVideoPlayer({ videoRef, + containerRef, visible, currentSource, hotKeys, @@ -55,7 +57,7 @@ export default function HlsVideoPlayer({ setFullResolution, onUploadFrame, toggleFullscreen, - containerRef, + onError, }: HlsVideoPlayerProps) { const { data: config } = useSWR("config"); @@ -64,6 +66,7 @@ export default function HlsVideoPlayer({ const hlsRef = useRef(); const [useHlsCompat, setUseHlsCompat] = useState(false); const [loadedMetadata, setLoadedMetadata] = useState(false); + const [bufferTimeout, setBufferTimeout] = useState(); const handleLoadedMetadata = useCallback(() => { setLoadedMetadata(true); @@ -270,6 +273,29 @@ export default function HlsVideoPlayer({ clearTimeout(mobileCtrlTimeout); } }} + onProgress={() => { + if (onError != undefined) { + if (videoRef.current?.paused) { + return; + } + + if (bufferTimeout) { + clearTimeout(bufferTimeout); + setBufferTimeout(undefined); + } + + setBufferTimeout( + setTimeout(() => { + if ( + document.visibilityState === "visible" && + videoRef.current + ) { + onError("stalled"); + } + }, 3000), + ); + } + }} onTimeUpdate={() => onTimeUpdate && videoRef.current ? onTimeUpdate(videoRef.current.currentTime) diff --git a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx index 3f82cd352..9e2d9a353 100644 --- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx +++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx @@ -91,6 +91,7 @@ export default function DynamicVideoPlayer({ // initial state const [isLoading, setIsLoading] = useState(false); + const [isBuffering, setIsBuffering] = useState(false); const [loadingTimeout, setLoadingTimeout] = useState(); const [source, setSource] = useState( `${apiHost}vod/${camera}/start/${timeRange.after}/end/${timeRange.before}/master.m3u8`, @@ -130,9 +131,13 @@ export default function DynamicVideoPlayer({ setIsLoading(false); } + if (isBuffering) { + setIsBuffering(false); + } + onTimestampUpdate(controller.getProgress(time)); }, - [controller, onTimestampUpdate, isScrubbing, isLoading], + [controller, onTimestampUpdate, isBuffering, isLoading, isScrubbing], ); const onUploadFrameToPlus = useCallback( @@ -188,6 +193,7 @@ export default function DynamicVideoPlayer({ <> { + if (error == "stalled" && !isScrubbing) { + setIsBuffering(true); + } + }} /> { - setPreviewController(previewController); - }} + onControllerReady={(previewController) => + setPreviewController(previewController) + } /> - {!isScrubbing && isLoading && !noRecording && ( + {!isScrubbing && (isLoading || isBuffering) && !noRecording && ( )} {!isScrubbing && noRecording && ( diff --git a/web/src/types/record.ts b/web/src/types/record.ts index d3fcfce94..a93029376 100644 --- a/web/src/types/record.ts +++ b/web/src/types/record.ts @@ -39,5 +39,7 @@ export type RecordingStartingPoint = { severity: ReviewSeverity; }; +export type RecordingPlayerError = "stalled" | "startup"; + export const ASPECT_VERTICAL_LAYOUT = 1.5; export const ASPECT_WIDE_LAYOUT = 2;