mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-11 17:47:37 +03:00
tracking details seeking fixes
This commit is contained in:
parent
787668ba0c
commit
1d991f6c7c
@ -55,7 +55,11 @@ export function TrackingDetails({
|
|||||||
const { t } = useTranslation(["views/explore"]);
|
const { t } = useTranslation(["views/explore"]);
|
||||||
const { setSelectedObjectIds, annotationOffset, setAnnotationOffset } =
|
const { setSelectedObjectIds, annotationOffset, setAnnotationOffset } =
|
||||||
useDetailStream();
|
useDetailStream();
|
||||||
const [currentTime, setCurrentTime] = useState(event.start_time ?? 0);
|
|
||||||
|
// event.start_time is detect time, convert to record, then subtract padding
|
||||||
|
const [currentTime, setCurrentTime] = useState(
|
||||||
|
(event.start_time ?? 0) + annotationOffset / 1000 - REVIEW_PADDING,
|
||||||
|
);
|
||||||
|
|
||||||
const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>([
|
const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>([
|
||||||
"timeline",
|
"timeline",
|
||||||
@ -66,11 +70,9 @@ export function TrackingDetails({
|
|||||||
|
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
// Calculate effective time (currentTime + annotation offset)
|
const effectiveTime = useMemo(() => {
|
||||||
const effectiveTime = useMemo(
|
return currentTime - annotationOffset / 1000;
|
||||||
() => currentTime + annotationOffset / 1000,
|
}, [currentTime, annotationOffset]);
|
||||||
[currentTime, annotationOffset],
|
|
||||||
);
|
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [_selectedZone, setSelectedZone] = useState("");
|
const [_selectedZone, setSelectedZone] = useState("");
|
||||||
@ -111,14 +113,23 @@ export function TrackingDetails({
|
|||||||
setSelectedObjectIds([event.id]);
|
setSelectedObjectIds([event.id]);
|
||||||
}, [event.id, setSelectedObjectIds]);
|
}, [event.id, setSelectedObjectIds]);
|
||||||
|
|
||||||
const handleLifecycleClick = useCallback((item: TrackingDetailsSequence) => {
|
const handleLifecycleClick = useCallback(
|
||||||
const timestamp = item.timestamp ?? 0;
|
(item: TrackingDetailsSequence) => {
|
||||||
setLifecycleZones(item.data.zones);
|
if (!videoRef.current) return;
|
||||||
setSelectedZone("");
|
|
||||||
|
|
||||||
// Set the target timestamp to seek to
|
// Convert lifecycle timestamp (detect stream) to record stream time
|
||||||
setSeekToTimestamp(timestamp);
|
const targetTimeRecord = item.timestamp + annotationOffset / 1000;
|
||||||
}, []);
|
|
||||||
|
// Convert to video-relative time for seeking
|
||||||
|
const eventStartRecord =
|
||||||
|
(event.start_time ?? 0) + annotationOffset / 1000;
|
||||||
|
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
||||||
|
const relativeTime = targetTimeRecord - videoStartTime;
|
||||||
|
|
||||||
|
videoRef.current.currentTime = relativeTime;
|
||||||
|
},
|
||||||
|
[event.start_time, annotationOffset],
|
||||||
|
);
|
||||||
|
|
||||||
const formattedStart = config
|
const formattedStart = config
|
||||||
? formatUnixTimestampToDateTime(event.start_time ?? 0, {
|
? formatUnixTimestampToDateTime(event.start_time ?? 0, {
|
||||||
@ -157,22 +168,21 @@ export function TrackingDetails({
|
|||||||
setLifecycleZones(eventSequence[0]?.data.zones);
|
setLifecycleZones(eventSequence[0]?.data.zones);
|
||||||
}, [eventSequence]);
|
}, [eventSequence]);
|
||||||
|
|
||||||
// Handle seeking when seekToTimestamp is set
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (seekToTimestamp === null || !videoRef.current) return;
|
if (seekToTimestamp === null || !videoRef.current) return;
|
||||||
|
|
||||||
const relativeTime =
|
// seekToTimestamp is a record stream timestamp
|
||||||
seekToTimestamp -
|
// event.start_time is detect stream time, convert to record
|
||||||
event.start_time +
|
// The video clip starts at (eventStartRecord - REVIEW_PADDING)
|
||||||
REVIEW_PADDING +
|
const eventStartRecord = event.start_time + annotationOffset / 1000;
|
||||||
annotationOffset / 1000;
|
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
||||||
|
const relativeTime = seekToTimestamp - videoStartTime;
|
||||||
if (relativeTime >= 0) {
|
if (relativeTime >= 0) {
|
||||||
videoRef.current.currentTime = relativeTime;
|
videoRef.current.currentTime = relativeTime;
|
||||||
}
|
}
|
||||||
setSeekToTimestamp(null);
|
setSeekToTimestamp(null);
|
||||||
}, [seekToTimestamp, event.start_time, annotationOffset]);
|
}, [seekToTimestamp, event.start_time, annotationOffset]);
|
||||||
|
|
||||||
// Check if current time is within the event's start/stop range
|
|
||||||
const isWithinEventRange =
|
const isWithinEventRange =
|
||||||
effectiveTime !== undefined &&
|
effectiveTime !== undefined &&
|
||||||
event.start_time !== undefined &&
|
event.start_time !== undefined &&
|
||||||
@ -229,13 +239,20 @@ export function TrackingDetails({
|
|||||||
const blueLineHeight = calculateLineHeight();
|
const blueLineHeight = calculateLineHeight();
|
||||||
|
|
||||||
const videoSource = useMemo(() => {
|
const videoSource = useMemo(() => {
|
||||||
const startTime = event.start_time - REVIEW_PADDING;
|
// event.start_time and event.end_time are in DETECT stream time
|
||||||
const endTime = (event.end_time ?? Date.now() / 1000) + REVIEW_PADDING;
|
// Convert to record stream time, then create video clip with padding
|
||||||
|
const eventStartRecord = event.start_time + annotationOffset / 1000;
|
||||||
|
const eventEndRecord =
|
||||||
|
(event.end_time ?? Date.now() / 1000) + annotationOffset / 1000;
|
||||||
|
const startTime = eventStartRecord - REVIEW_PADDING;
|
||||||
|
const endTime = eventEndRecord + REVIEW_PADDING;
|
||||||
|
const playlist = `${baseUrl}vod/${event.camera}/start/${startTime}/end/${endTime}/index.m3u8`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
playlist: `${baseUrl}vod/${event.camera}/start/${startTime}/end/${endTime}/index.m3u8`,
|
playlist,
|
||||||
startPosition: 0,
|
startPosition: 0,
|
||||||
};
|
};
|
||||||
}, [event]);
|
}, [event, annotationOffset]);
|
||||||
|
|
||||||
// Determine camera aspect ratio category
|
// Determine camera aspect ratio category
|
||||||
const cameraAspect = useMemo(() => {
|
const cameraAspect = useMemo(() => {
|
||||||
@ -257,10 +274,14 @@ export function TrackingDetails({
|
|||||||
|
|
||||||
const handleTimeUpdate = useCallback(
|
const handleTimeUpdate = useCallback(
|
||||||
(time: number) => {
|
(time: number) => {
|
||||||
const absoluteTime = time - REVIEW_PADDING + event.start_time;
|
// event.start_time is detect stream time, convert to record
|
||||||
|
const eventStartRecord = event.start_time + annotationOffset / 1000;
|
||||||
|
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
||||||
|
const absoluteTime = time + videoStartTime;
|
||||||
|
|
||||||
setCurrentTime(absoluteTime);
|
setCurrentTime(absoluteTime);
|
||||||
},
|
},
|
||||||
[event.start_time],
|
[event.start_time, annotationOffset],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@ -384,7 +405,10 @@ export function TrackingDetails({
|
|||||||
className="flex items-center gap-2 font-medium"
|
className="flex items-center gap-2 font-medium"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleSeekToTime(event.start_time ?? 0);
|
// event.start_time is detect time, convert to record
|
||||||
|
handleSeekToTime(
|
||||||
|
(event.start_time ?? 0) + annotationOffset / 1000,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
@ -428,11 +452,11 @@ export function TrackingDetails({
|
|||||||
{t("detail.noObjectDetailData", { ns: "views/events" })}
|
{t("detail.noObjectDetailData", { ns: "views/events" })}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="-pb-2 relative mx-2">
|
<div className="-pb-2 relative mx-0">
|
||||||
<div className="absolute -top-2 bottom-8 left-4 z-0 w-0.5 -translate-x-1/2 bg-secondary-foreground" />
|
<div className="absolute -top-2 bottom-8 left-6 z-0 w-0.5 -translate-x-1/2 bg-secondary-foreground" />
|
||||||
{isWithinEventRange && (
|
{isWithinEventRange && (
|
||||||
<div
|
<div
|
||||||
className="absolute left-4 top-2 z-[5] max-h-[calc(100%-3rem)] w-0.5 -translate-x-1/2 bg-selected transition-all duration-300"
|
className="absolute left-6 top-2 z-[5] max-h-[calc(100%-3rem)] w-0.5 -translate-x-1/2 bg-selected transition-all duration-300"
|
||||||
style={{ height: `${blueLineHeight}%` }}
|
style={{ height: `${blueLineHeight}%` }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -552,16 +576,16 @@ function LifecycleIconRow({
|
|||||||
role="button"
|
role="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-md p-2 text-sm text-primary-variant",
|
"rounded-md p-2 pr-0 text-sm text-primary-variant",
|
||||||
isActive && "bg-secondary-highlight font-semibold text-primary",
|
isActive && "bg-secondary-highlight font-semibold text-primary",
|
||||||
!isActive && "duration-500",
|
!isActive && "duration-500",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative flex size-4 items-center justify-center">
|
<div className="relative ml-2 flex size-4 items-center justify-center">
|
||||||
<LuCircle
|
<LuCircle
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-10 ml-[1px] size-2.5 fill-secondary-foreground stroke-none",
|
"relative z-10 size-2.5 fill-secondary-foreground stroke-none",
|
||||||
(isActive || (effectiveTime ?? 0) >= (item?.timestamp ?? 0)) &&
|
(isActive || (effectiveTime ?? 0) >= (item?.timestamp ?? 0)) &&
|
||||||
isTimelineActive &&
|
isTimelineActive &&
|
||||||
"fill-selected duration-300",
|
"fill-selected duration-300",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user