diff --git a/web/src/components/overlay/detail/TrackingDetails.tsx b/web/src/components/overlay/detail/TrackingDetails.tsx index fa2f0cb2b..2cdd330ac 100644 --- a/web/src/components/overlay/detail/TrackingDetails.tsx +++ b/web/src/components/overlay/detail/TrackingDetails.tsx @@ -559,6 +559,7 @@ export function TrackingDetails({ isDetailMode={true} camera={event.camera} currentTimeOverride={currentTime} + enableGapControllerRecovery={true} /> {isVideoLoading && ( diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index 045b8366f..aabd93ac8 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -5,7 +5,7 @@ import { useRef, useState, } from "react"; -import Hls from "hls.js"; +import Hls, { HlsConfig } from "hls.js"; import { isDesktop, isMobile } from "react-device-detect"; import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; import VideoControls from "./VideoControls"; @@ -57,6 +57,7 @@ type HlsVideoPlayerProps = { isDetailMode?: boolean; camera?: string; currentTimeOverride?: number; + enableGapControllerRecovery?: boolean; }; export default function HlsVideoPlayer({ @@ -81,6 +82,7 @@ export default function HlsVideoPlayer({ isDetailMode = false, camera, currentTimeOverride, + enableGapControllerRecovery = false, }: HlsVideoPlayerProps) { const { t } = useTranslation("components/player"); const { data: config } = useSWR("config"); @@ -170,11 +172,23 @@ export default function HlsVideoPlayer({ return; } - hlsRef.current = new Hls({ + // Base HLS configuration + const baseConfig: Partial = { maxBufferLength: 10, maxBufferSize: 20 * 1000 * 1000, startPosition: currentSource.startPosition, - }); + }; + + const hlsConfig = { ...baseConfig }; + + if (enableGapControllerRecovery) { + hlsConfig.highBufferWatchdogPeriod = 1; // Check for stalls every 1 second (default: 3) + hlsConfig.nudgeOffset = 0.2; // Nudge playhead forward 0.2s when stalled (default: 0.1) + hlsConfig.nudgeMaxRetry = 5; // Try up to 5 nudges before giving up (default: 3) + hlsConfig.maxBufferHole = 0.5; // Tolerate up to 0.5s gaps between fragments (default: 0.1) + } + + hlsRef.current = new Hls(hlsConfig); hlsRef.current.attachMedia(videoRef.current); hlsRef.current.loadSource(currentSource.playlist); videoRef.current.playbackRate = currentPlaybackRate; @@ -187,7 +201,13 @@ export default function HlsVideoPlayer({ hlsRef.current.destroy(); } }; - }, [videoRef, hlsRef, useHlsCompat, currentSource]); + }, [ + videoRef, + hlsRef, + useHlsCompat, + currentSource, + enableGapControllerRecovery, + ]); // state handling