mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-21 12:36:42 +03:00
Compare commits
3 Commits
de704ef211
...
c01443e555
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c01443e555 | ||
|
|
739de1a4e1 | ||
|
|
6286d2aa32 |
@ -56,6 +56,7 @@ export function TrackingDetails({
|
|||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
const imgRef = useRef<HTMLImageElement | null>(null);
|
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||||
const [imgLoaded, setImgLoaded] = useState(false);
|
const [imgLoaded, setImgLoaded] = useState(false);
|
||||||
|
const [isVideoLoading, setIsVideoLoading] = useState(true);
|
||||||
const [displaySource, _setDisplaySource] = useState<"video" | "image">(
|
const [displaySource, _setDisplaySource] = useState<"video" | "image">(
|
||||||
"video",
|
"video",
|
||||||
);
|
);
|
||||||
@ -70,6 +71,10 @@ export function TrackingDetails({
|
|||||||
(event.start_time ?? 0) + annotationOffset / 1000 - REVIEW_PADDING,
|
(event.start_time ?? 0) + annotationOffset / 1000 - REVIEW_PADDING,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsVideoLoading(true);
|
||||||
|
}, [event.id]);
|
||||||
|
|
||||||
const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>([
|
const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>([
|
||||||
"timeline",
|
"timeline",
|
||||||
{
|
{
|
||||||
@ -527,22 +532,28 @@ export function TrackingDetails({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{displaySource == "video" && (
|
{displaySource == "video" && (
|
||||||
<HlsVideoPlayer
|
<>
|
||||||
videoRef={videoRef}
|
<HlsVideoPlayer
|
||||||
containerRef={containerRef}
|
videoRef={videoRef}
|
||||||
visible={true}
|
containerRef={containerRef}
|
||||||
currentSource={videoSource}
|
visible={true}
|
||||||
hotKeys={false}
|
currentSource={videoSource}
|
||||||
supportsFullscreen={false}
|
hotKeys={false}
|
||||||
fullscreen={false}
|
supportsFullscreen={false}
|
||||||
frigateControls={true}
|
fullscreen={false}
|
||||||
onTimeUpdate={handleTimeUpdate}
|
frigateControls={true}
|
||||||
onSeekToTime={handleSeekToTime}
|
onTimeUpdate={handleTimeUpdate}
|
||||||
onUploadFrame={onUploadFrameToPlus}
|
onSeekToTime={handleSeekToTime}
|
||||||
isDetailMode={true}
|
onUploadFrame={onUploadFrameToPlus}
|
||||||
camera={event.camera}
|
onPlaying={() => setIsVideoLoading(false)}
|
||||||
currentTimeOverride={currentTime}
|
isDetailMode={true}
|
||||||
/>
|
camera={event.camera}
|
||||||
|
currentTimeOverride={currentTime}
|
||||||
|
/>
|
||||||
|
{isVideoLoading && (
|
||||||
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{displaySource == "image" && (
|
{displaySource == "image" && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -130,6 +130,8 @@ export default function HlsVideoPlayer({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoadedMetadata(false);
|
||||||
|
|
||||||
const currentPlaybackRate = videoRef.current.playbackRate;
|
const currentPlaybackRate = videoRef.current.playbackRate;
|
||||||
|
|
||||||
if (!useHlsCompat) {
|
if (!useHlsCompat) {
|
||||||
|
|||||||
@ -309,6 +309,7 @@ function PreviewVideoPlayer({
|
|||||||
playsInline
|
playsInline
|
||||||
muted
|
muted
|
||||||
disableRemotePlayback
|
disableRemotePlayback
|
||||||
|
disablePictureInPicture
|
||||||
onSeeked={onPreviewSeeked}
|
onSeeked={onPreviewSeeked}
|
||||||
onLoadedData={() => {
|
onLoadedData={() => {
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
|
|||||||
@ -2,7 +2,10 @@ import { Recording } from "@/types/record";
|
|||||||
import { DynamicPlayback } from "@/types/playback";
|
import { DynamicPlayback } from "@/types/playback";
|
||||||
import { PreviewController } from "../PreviewPlayer";
|
import { PreviewController } from "../PreviewPlayer";
|
||||||
import { TimeRange, TrackingDetailsSequence } from "@/types/timeline";
|
import { TimeRange, TrackingDetailsSequence } from "@/types/timeline";
|
||||||
import { calculateInpointOffset } from "@/utils/videoUtil";
|
import {
|
||||||
|
calculateInpointOffset,
|
||||||
|
calculateSeekPosition,
|
||||||
|
} from "@/utils/videoUtil";
|
||||||
|
|
||||||
type PlayerMode = "playback" | "scrubbing";
|
type PlayerMode = "playback" | "scrubbing";
|
||||||
|
|
||||||
@ -72,38 +75,20 @@ export class DynamicVideoController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
this.recordings.length == 0 ||
|
|
||||||
time < this.recordings[0].start_time ||
|
|
||||||
time > this.recordings[this.recordings.length - 1].end_time
|
|
||||||
) {
|
|
||||||
this.setNoRecording(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.playerMode != "playback") {
|
if (this.playerMode != "playback") {
|
||||||
this.playerMode = "playback";
|
this.playerMode = "playback";
|
||||||
}
|
}
|
||||||
|
|
||||||
let seekSeconds = 0;
|
const seekSeconds = calculateSeekPosition(
|
||||||
(this.recordings || []).every((segment) => {
|
time,
|
||||||
// if the next segment is past the desired time, stop calculating
|
this.recordings,
|
||||||
if (segment.start_time > time) {
|
this.inpointOffset,
|
||||||
return false;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (segment.end_time < time) {
|
if (seekSeconds === undefined) {
|
||||||
seekSeconds += segment.end_time - segment.start_time;
|
this.setNoRecording(true);
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
seekSeconds +=
|
|
||||||
segment.end_time - segment.start_time - (segment.end_time - time);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// adjust for HLS inpoint offset
|
|
||||||
seekSeconds -= this.inpointOffset;
|
|
||||||
|
|
||||||
if (seekSeconds != 0) {
|
if (seekSeconds != 0) {
|
||||||
this.playerController.currentTime = seekSeconds;
|
this.playerController.currentTime = seekSeconds;
|
||||||
|
|||||||
@ -14,7 +14,10 @@ import { VideoResolutionType } from "@/types/live";
|
|||||||
import axios from "axios";
|
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,
|
||||||
|
calculateSeekPosition,
|
||||||
|
} from "@/utils/videoUtil";
|
||||||
import { isFirefox } from "react-device-detect";
|
import { isFirefox } from "react-device-detect";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,10 +112,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<HlsSource>({
|
|
||||||
playlist: `${apiHost}vod/${camera}/start/${timeRange.after}/end/${timeRange.before}/master.m3u8`,
|
// Don't set source until recordings load - we need accurate startPosition
|
||||||
startPosition: startTimestamp ? startTimestamp - timeRange.after : 0,
|
// to avoid hls.js clamping to video end when startPosition exceeds duration
|
||||||
});
|
const [source, setSource] = useState<HlsSource | undefined>(undefined);
|
||||||
|
|
||||||
// start at correct time
|
// start at correct time
|
||||||
|
|
||||||
@ -184,7 +187,7 @@ export default function DynamicVideoPlayer({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!controller || !recordings?.length) {
|
if (!recordings?.length) {
|
||||||
if (recordings?.length == 0) {
|
if (recordings?.length == 0) {
|
||||||
setNoRecording(true);
|
setNoRecording(true);
|
||||||
}
|
}
|
||||||
@ -192,10 +195,6 @@ export default function DynamicVideoPlayer({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerRef.current) {
|
|
||||||
playerRef.current.autoplay = !isScrubbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
let startPosition = undefined;
|
let startPosition = undefined;
|
||||||
|
|
||||||
if (startTimestamp) {
|
if (startTimestamp) {
|
||||||
@ -203,14 +202,12 @@ export default function DynamicVideoPlayer({
|
|||||||
recordingParams.after,
|
recordingParams.after,
|
||||||
(recordings || [])[0],
|
(recordings || [])[0],
|
||||||
);
|
);
|
||||||
const idealStartPosition = Math.max(
|
|
||||||
0,
|
|
||||||
startTimestamp - timeRange.after - inpointOffset,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (idealStartPosition >= recordings[0].start_time - timeRange.after) {
|
startPosition = calculateSeekPosition(
|
||||||
startPosition = idealStartPosition;
|
startTimestamp,
|
||||||
}
|
recordings,
|
||||||
|
inpointOffset,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSource({
|
setSource({
|
||||||
@ -218,6 +215,18 @@ export default function DynamicVideoPlayer({
|
|||||||
startPosition,
|
startPosition,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [recordings]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!controller || !recordings?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerRef.current) {
|
||||||
|
playerRef.current.autoplay = !isScrubbing;
|
||||||
|
}
|
||||||
|
|
||||||
setLoadingTimeout(setTimeout(() => setIsLoading(true), 1000));
|
setLoadingTimeout(setTimeout(() => setIsLoading(true), 1000));
|
||||||
|
|
||||||
controller.newPlayback({
|
controller.newPlayback({
|
||||||
@ -225,7 +234,7 @@ export default function DynamicVideoPlayer({
|
|||||||
timeRange,
|
timeRange,
|
||||||
});
|
});
|
||||||
|
|
||||||
// we only want this to change when recordings update
|
// we only want this to change when controller or recordings update
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [controller, recordings]);
|
}, [controller, recordings]);
|
||||||
|
|
||||||
@ -263,46 +272,48 @@ export default function DynamicVideoPlayer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HlsVideoPlayer
|
{source && (
|
||||||
videoRef={playerRef}
|
<HlsVideoPlayer
|
||||||
containerRef={containerRef}
|
videoRef={playerRef}
|
||||||
visible={!(isScrubbing || isLoading)}
|
containerRef={containerRef}
|
||||||
currentSource={source}
|
visible={!(isScrubbing || isLoading)}
|
||||||
hotKeys={hotKeys}
|
currentSource={source}
|
||||||
supportsFullscreen={supportsFullscreen}
|
hotKeys={hotKeys}
|
||||||
fullscreen={fullscreen}
|
supportsFullscreen={supportsFullscreen}
|
||||||
inpointOffset={inpointOffset}
|
fullscreen={fullscreen}
|
||||||
onTimeUpdate={onTimeUpdate}
|
inpointOffset={inpointOffset}
|
||||||
onPlayerLoaded={onPlayerLoaded}
|
onTimeUpdate={onTimeUpdate}
|
||||||
onClipEnded={onValidateClipEnd}
|
onPlayerLoaded={onPlayerLoaded}
|
||||||
onSeekToTime={(timestamp, play) => {
|
onClipEnded={onValidateClipEnd}
|
||||||
if (onSeekToTime) {
|
onSeekToTime={(timestamp, play) => {
|
||||||
onSeekToTime(timestamp, play);
|
if (onSeekToTime) {
|
||||||
}
|
onSeekToTime(timestamp, play);
|
||||||
}}
|
}
|
||||||
onPlaying={() => {
|
}}
|
||||||
if (isScrubbing) {
|
onPlaying={() => {
|
||||||
playerRef.current?.pause();
|
if (isScrubbing) {
|
||||||
}
|
playerRef.current?.pause();
|
||||||
|
}
|
||||||
|
|
||||||
if (loadingTimeout) {
|
if (loadingTimeout) {
|
||||||
clearTimeout(loadingTimeout);
|
clearTimeout(loadingTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
setNoRecording(false);
|
setNoRecording(false);
|
||||||
}}
|
}}
|
||||||
setFullResolution={setFullResolution}
|
setFullResolution={setFullResolution}
|
||||||
onUploadFrame={onUploadFrameToPlus}
|
onUploadFrame={onUploadFrameToPlus}
|
||||||
toggleFullscreen={toggleFullscreen}
|
toggleFullscreen={toggleFullscreen}
|
||||||
onError={(error) => {
|
onError={(error) => {
|
||||||
if (error == "stalled" && !isScrubbing) {
|
if (error == "stalled" && !isScrubbing) {
|
||||||
setIsBuffering(true);
|
setIsBuffering(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
isDetailMode={isDetailMode}
|
isDetailMode={isDetailMode}
|
||||||
camera={contextCamera || camera}
|
camera={contextCamera || camera}
|
||||||
currentTimeOverride={currentTime}
|
currentTimeOverride={currentTime}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<PreviewPlayer
|
<PreviewPlayer
|
||||||
className={cn(
|
className={cn(
|
||||||
className,
|
className,
|
||||||
|
|||||||
@ -24,3 +24,57 @@ export function calculateInpointOffset(
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the video player time (in seconds) for a given timestamp
|
||||||
|
* by iterating through recording segments and summing their durations.
|
||||||
|
* This accounts for the fact that the video is a concatenation of segments,
|
||||||
|
* not a single continuous stream.
|
||||||
|
*
|
||||||
|
* @param timestamp - The target timestamp to seek to
|
||||||
|
* @param recordings - Array of recording segments
|
||||||
|
* @param inpointOffset - HLS inpoint offset to subtract from the result
|
||||||
|
* @returns The calculated seek position in seconds, or undefined if timestamp is out of range
|
||||||
|
*/
|
||||||
|
export function calculateSeekPosition(
|
||||||
|
timestamp: number,
|
||||||
|
recordings: Recording[],
|
||||||
|
inpointOffset: number = 0,
|
||||||
|
): number | undefined {
|
||||||
|
if (!recordings || recordings.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if timestamp is within the recordings range
|
||||||
|
if (
|
||||||
|
timestamp < recordings[0].start_time ||
|
||||||
|
timestamp > recordings[recordings.length - 1].end_time
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let seekSeconds = 0;
|
||||||
|
|
||||||
|
(recordings || []).every((segment) => {
|
||||||
|
// if the next segment is past the desired time, stop calculating
|
||||||
|
if (segment.start_time > timestamp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.end_time < timestamp) {
|
||||||
|
// Add the full duration of this segment
|
||||||
|
seekSeconds += segment.end_time - segment.start_time;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're in this segment - calculate position within it
|
||||||
|
seekSeconds +=
|
||||||
|
segment.end_time - segment.start_time - (segment.end_time - timestamp);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adjust for HLS inpoint offset
|
||||||
|
seekSeconds -= inpointOffset;
|
||||||
|
|
||||||
|
return seekSeconds >= 0 ? seekSeconds : undefined;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user