mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-08-22 13:47:29 +02:00
HLS Playback Startup Time Optimization (#19503)
* Include preferred startTime in source so that the playlist does not need to seek * Compatibility * Cleanup * Adjust based on inpoint * Don't set start position if it is not valid * Handle firefox buggy behavior
This commit is contained in:
parent
89db960c05
commit
fb290c411b
@ -107,7 +107,9 @@ export function GenericVideoPlayer({
|
|||||||
>
|
>
|
||||||
<HlsVideoPlayer
|
<HlsVideoPlayer
|
||||||
videoRef={videoRef}
|
videoRef={videoRef}
|
||||||
currentSource={source}
|
currentSource={{
|
||||||
|
playlist: source,
|
||||||
|
}}
|
||||||
hotKeys
|
hotKeys
|
||||||
visible
|
visible
|
||||||
frigateControls={false}
|
frigateControls={false}
|
||||||
|
@ -28,17 +28,22 @@ const unsupportedErrorCodes = [
|
|||||||
MediaError.MEDIA_ERR_DECODE,
|
MediaError.MEDIA_ERR_DECODE,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export interface HlsSource {
|
||||||
|
playlist: string;
|
||||||
|
startPosition?: number;
|
||||||
|
}
|
||||||
|
|
||||||
type HlsVideoPlayerProps = {
|
type HlsVideoPlayerProps = {
|
||||||
videoRef: MutableRefObject<HTMLVideoElement | null>;
|
videoRef: MutableRefObject<HTMLVideoElement | null>;
|
||||||
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
|
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
currentSource: string;
|
currentSource: HlsSource;
|
||||||
hotKeys: boolean;
|
hotKeys: boolean;
|
||||||
supportsFullscreen: boolean;
|
supportsFullscreen: boolean;
|
||||||
fullscreen: boolean;
|
fullscreen: boolean;
|
||||||
frigateControls?: boolean;
|
frigateControls?: boolean;
|
||||||
inpointOffset?: number;
|
inpointOffset?: number;
|
||||||
onClipEnded?: () => void;
|
onClipEnded?: (currentTime: number) => void;
|
||||||
onPlayerLoaded?: () => void;
|
onPlayerLoaded?: () => void;
|
||||||
onTimeUpdate?: (time: number) => void;
|
onTimeUpdate?: (time: number) => void;
|
||||||
onPlaying?: () => void;
|
onPlaying?: () => void;
|
||||||
@ -113,17 +118,25 @@ export default function HlsVideoPlayer({
|
|||||||
const currentPlaybackRate = videoRef.current.playbackRate;
|
const currentPlaybackRate = videoRef.current.playbackRate;
|
||||||
|
|
||||||
if (!useHlsCompat) {
|
if (!useHlsCompat) {
|
||||||
videoRef.current.src = currentSource;
|
videoRef.current.src = currentSource.playlist;
|
||||||
videoRef.current.load();
|
videoRef.current.load();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hlsRef.current) {
|
// we must destroy the hlsRef every time the source changes
|
||||||
hlsRef.current = new Hls();
|
// so that we can create a new HLS instance with startPosition
|
||||||
hlsRef.current.attachMedia(videoRef.current);
|
// set at the optimal point in time
|
||||||
|
if (hlsRef.current) {
|
||||||
|
hlsRef.current.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
hlsRef.current.loadSource(currentSource);
|
hlsRef.current = new Hls({
|
||||||
|
maxBufferLength: 10,
|
||||||
|
maxBufferSize: 20 * 1000 * 1000,
|
||||||
|
startPosition: currentSource.startPosition,
|
||||||
|
});
|
||||||
|
hlsRef.current.attachMedia(videoRef.current);
|
||||||
|
hlsRef.current.loadSource(currentSource.playlist);
|
||||||
videoRef.current.playbackRate = currentPlaybackRate;
|
videoRef.current.playbackRate = currentPlaybackRate;
|
||||||
}, [videoRef, hlsRef, useHlsCompat, currentSource]);
|
}, [videoRef, hlsRef, useHlsCompat, currentSource]);
|
||||||
|
|
||||||
@ -374,7 +387,11 @@ export default function HlsVideoPlayer({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onEnded={onClipEnded}
|
onEnded={() => {
|
||||||
|
if (onClipEnded) {
|
||||||
|
onClipEnded(getVideoTime() ?? 0);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
if (
|
if (
|
||||||
!hlsRef.current &&
|
!hlsRef.current &&
|
||||||
|
@ -6,7 +6,7 @@ import { Recording } from "@/types/record";
|
|||||||
import { Preview } from "@/types/preview";
|
import { Preview } from "@/types/preview";
|
||||||
import PreviewPlayer, { PreviewController } from "../PreviewPlayer";
|
import PreviewPlayer, { PreviewController } from "../PreviewPlayer";
|
||||||
import { DynamicVideoController } from "./DynamicVideoController";
|
import { DynamicVideoController } from "./DynamicVideoController";
|
||||||
import HlsVideoPlayer from "../HlsVideoPlayer";
|
import HlsVideoPlayer, { HlsSource } from "../HlsVideoPlayer";
|
||||||
import { TimeRange } from "@/types/timeline";
|
import { TimeRange } from "@/types/timeline";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import { VideoResolutionType } from "@/types/live";
|
import { VideoResolutionType } from "@/types/live";
|
||||||
@ -14,6 +14,7 @@ import axios from "axios";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { calculateInpointOffset } from "@/utils/videoUtil";
|
import { calculateInpointOffset } from "@/utils/videoUtil";
|
||||||
|
import { isFirefox } from "react-device-detect";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically switches between video playback and scrubbing preview player.
|
* Dynamically switches between video playback and scrubbing preview player.
|
||||||
@ -98,9 +99,10 @@ export default function DynamicVideoPlayer({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isBuffering, setIsBuffering] = useState(false);
|
const [isBuffering, setIsBuffering] = useState(false);
|
||||||
const [loadingTimeout, setLoadingTimeout] = useState<NodeJS.Timeout>();
|
const [loadingTimeout, setLoadingTimeout] = useState<NodeJS.Timeout>();
|
||||||
const [source, setSource] = useState(
|
const [source, setSource] = useState<HlsSource>({
|
||||||
`${apiHost}vod/${camera}/start/${timeRange.after}/end/${timeRange.before}/master.m3u8`,
|
playlist: `${apiHost}vod/${camera}/start/${timeRange.after}/end/${timeRange.before}/master.m3u8`,
|
||||||
);
|
startPosition: startTimestamp ? timeRange.after - startTimestamp : 0,
|
||||||
|
});
|
||||||
|
|
||||||
// start at correct time
|
// start at correct time
|
||||||
|
|
||||||
@ -184,9 +186,28 @@ export default function DynamicVideoPlayer({
|
|||||||
playerRef.current.autoplay = !isScrubbing;
|
playerRef.current.autoplay = !isScrubbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSource(
|
let startPosition = undefined;
|
||||||
`${apiHost}vod/${camera}/start/${recordingParams.after}/end/${recordingParams.before}/master.m3u8`,
|
|
||||||
);
|
if (startTimestamp) {
|
||||||
|
const inpointOffset = calculateInpointOffset(
|
||||||
|
recordingParams.after,
|
||||||
|
(recordings || [])[0],
|
||||||
|
);
|
||||||
|
const idealStartPosition = Math.max(
|
||||||
|
0,
|
||||||
|
startTimestamp - timeRange.after - inpointOffset,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (idealStartPosition >= recordings[0].start_time - timeRange.after) {
|
||||||
|
startPosition = idealStartPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSource({
|
||||||
|
playlist: `${apiHost}vod/${camera}/start/${recordingParams.after}/end/${recordingParams.before}/master.m3u8`,
|
||||||
|
startPosition,
|
||||||
|
});
|
||||||
|
|
||||||
setLoadingTimeout(setTimeout(() => setIsLoading(true), 1000));
|
setLoadingTimeout(setTimeout(() => setIsLoading(true), 1000));
|
||||||
|
|
||||||
controller.newPlayback({
|
controller.newPlayback({
|
||||||
@ -203,6 +224,33 @@ export default function DynamicVideoPlayer({
|
|||||||
[recordingParams, recordings],
|
[recordingParams, recordings],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onValidateClipEnd = useCallback(
|
||||||
|
(currentTime: number) => {
|
||||||
|
if (!onClipEnded || !controller || !recordings) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFirefox) {
|
||||||
|
onClipEnded();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Firefox has a bug where clipEnded can be called prematurely due to buffering
|
||||||
|
// we need to validate if the current play-point is truly at the end of available recordings
|
||||||
|
|
||||||
|
const lastRecordingTime = recordings.at(-1)?.start_time;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!lastRecordingTime ||
|
||||||
|
controller.getProgress(currentTime) < lastRecordingTime
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClipEnded();
|
||||||
|
},
|
||||||
|
[onClipEnded, controller, recordings],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HlsVideoPlayer
|
<HlsVideoPlayer
|
||||||
@ -216,7 +264,7 @@ export default function DynamicVideoPlayer({
|
|||||||
inpointOffset={inpointOffset}
|
inpointOffset={inpointOffset}
|
||||||
onTimeUpdate={onTimeUpdate}
|
onTimeUpdate={onTimeUpdate}
|
||||||
onPlayerLoaded={onPlayerLoaded}
|
onPlayerLoaded={onPlayerLoaded}
|
||||||
onClipEnded={onClipEnded}
|
onClipEnded={onValidateClipEnd}
|
||||||
onPlaying={() => {
|
onPlaying={() => {
|
||||||
if (isScrubbing) {
|
if (isScrubbing) {
|
||||||
playerRef.current?.pause();
|
playerRef.current?.pause();
|
||||||
|
Loading…
Reference in New Issue
Block a user