tracking details seeking fixes

This commit is contained in:
Josh Hawkins 2025-11-01 07:35:54 -05:00
parent 787668ba0c
commit 1d991f6c7c

View File

@ -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",