mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 05:24:11 +03:00
Detail Stream tweaks (#20533)
Some checks are pending
CI / ARM Extra Build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
Some checks are pending
CI / ARM Extra Build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
This commit is contained in:
parent
9599450cff
commit
60789f7096
@ -15,17 +15,19 @@ import { useTranslation } from "react-i18next";
|
|||||||
type ObjectTrackOverlayProps = {
|
type ObjectTrackOverlayProps = {
|
||||||
camera: string;
|
camera: string;
|
||||||
selectedObjectId: string;
|
selectedObjectId: string;
|
||||||
|
showBoundingBoxes?: boolean;
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
videoWidth: number;
|
videoWidth: number;
|
||||||
videoHeight: number;
|
videoHeight: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
onSeekToTime?: (timestamp: number) => void;
|
onSeekToTime?: (timestamp: number, play?: boolean) => void;
|
||||||
objectTimeline?: ObjectLifecycleSequence[];
|
objectTimeline?: ObjectLifecycleSequence[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ObjectTrackOverlay({
|
export default function ObjectTrackOverlay({
|
||||||
camera,
|
camera,
|
||||||
selectedObjectId,
|
selectedObjectId,
|
||||||
|
showBoundingBoxes = false,
|
||||||
currentTime,
|
currentTime,
|
||||||
videoWidth,
|
videoWidth,
|
||||||
videoHeight,
|
videoHeight,
|
||||||
@ -227,7 +229,7 @@ export default function ObjectTrackOverlay({
|
|||||||
|
|
||||||
const handlePointClick = useCallback(
|
const handlePointClick = useCallback(
|
||||||
(timestamp: number) => {
|
(timestamp: number) => {
|
||||||
onSeekToTime?.(timestamp);
|
onSeekToTime?.(timestamp, false);
|
||||||
},
|
},
|
||||||
[onSeekToTime],
|
[onSeekToTime],
|
||||||
);
|
);
|
||||||
@ -366,7 +368,7 @@ export default function ObjectTrackOverlay({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{currentBoundingBox && (
|
{currentBoundingBox && showBoundingBoxes && (
|
||||||
<g>
|
<g>
|
||||||
<rect
|
<rect
|
||||||
x={currentBoundingBox.left * videoWidth}
|
x={currentBoundingBox.left * videoWidth}
|
||||||
|
|||||||
@ -49,7 +49,7 @@ type HlsVideoPlayerProps = {
|
|||||||
onPlayerLoaded?: () => void;
|
onPlayerLoaded?: () => void;
|
||||||
onTimeUpdate?: (time: number) => void;
|
onTimeUpdate?: (time: number) => void;
|
||||||
onPlaying?: () => void;
|
onPlaying?: () => void;
|
||||||
onSeekToTime?: (timestamp: number) => void;
|
onSeekToTime?: (timestamp: number, play?: boolean) => void;
|
||||||
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||||
onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined;
|
onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined;
|
||||||
toggleFullscreen?: () => void;
|
toggleFullscreen?: () => void;
|
||||||
@ -324,13 +324,14 @@ export default function HlsVideoPlayer({
|
|||||||
key={`${selectedObjectId}-${currentTime}`}
|
key={`${selectedObjectId}-${currentTime}`}
|
||||||
camera={camera}
|
camera={camera}
|
||||||
selectedObjectId={selectedObjectId}
|
selectedObjectId={selectedObjectId}
|
||||||
|
showBoundingBoxes={!isPlaying}
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
videoWidth={videoDimensions.width}
|
videoWidth={videoDimensions.width}
|
||||||
videoHeight={videoDimensions.height}
|
videoHeight={videoDimensions.height}
|
||||||
className="absolute inset-0 z-10"
|
className="absolute inset-0 z-10"
|
||||||
onSeekToTime={(timestamp) => {
|
onSeekToTime={(timestamp, play) => {
|
||||||
if (onSeekToTime) {
|
if (onSeekToTime) {
|
||||||
onSeekToTime(timestamp);
|
onSeekToTime(timestamp, play);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
objectTimeline={selectedObjectTimeline}
|
objectTimeline={selectedObjectTimeline}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ type DynamicVideoPlayerProps = {
|
|||||||
onControllerReady: (controller: DynamicVideoController) => void;
|
onControllerReady: (controller: DynamicVideoController) => void;
|
||||||
onTimestampUpdate?: (timestamp: number) => void;
|
onTimestampUpdate?: (timestamp: number) => void;
|
||||||
onClipEnded?: () => void;
|
onClipEnded?: () => void;
|
||||||
onSeekToTime?: (timestamp: number) => void;
|
onSeekToTime?: (timestamp: number, play?: boolean) => void;
|
||||||
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||||
toggleFullscreen: () => void;
|
toggleFullscreen: () => void;
|
||||||
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
|
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
@ -267,7 +267,11 @@ export default function DynamicVideoPlayer({
|
|||||||
onTimeUpdate={onTimeUpdate}
|
onTimeUpdate={onTimeUpdate}
|
||||||
onPlayerLoaded={onPlayerLoaded}
|
onPlayerLoaded={onPlayerLoaded}
|
||||||
onClipEnded={onValidateClipEnd}
|
onClipEnded={onValidateClipEnd}
|
||||||
onSeekToTime={onSeekToTime}
|
onSeekToTime={(timestamp, play) => {
|
||||||
|
if (onSeekToTime) {
|
||||||
|
onSeekToTime(timestamp, play);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onPlaying={() => {
|
onPlaying={() => {
|
||||||
if (isScrubbing) {
|
if (isScrubbing) {
|
||||||
playerRef.current?.pause();
|
playerRef.current?.pause();
|
||||||
|
|||||||
@ -5,7 +5,10 @@ import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
|
|||||||
import { useDetailStream } from "@/context/detail-stream-context";
|
import { useDetailStream } from "@/context/detail-stream-context";
|
||||||
import scrollIntoView from "scroll-into-view-if-needed";
|
import scrollIntoView from "scroll-into-view-if-needed";
|
||||||
import useUserInteraction from "@/hooks/use-user-interaction";
|
import useUserInteraction from "@/hooks/use-user-interaction";
|
||||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
import {
|
||||||
|
formatUnixTimestampToDateTime,
|
||||||
|
formatSecondsToDuration,
|
||||||
|
} from "@/utils/dateUtil";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import AnnotationOffsetSlider from "@/components/overlay/detail/AnnotationOffsetSlider";
|
import AnnotationOffsetSlider from "@/components/overlay/detail/AnnotationOffsetSlider";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
@ -13,7 +16,7 @@ import useSWR from "swr";
|
|||||||
import ActivityIndicator from "../indicators/activity-indicator";
|
import ActivityIndicator from "../indicators/activity-indicator";
|
||||||
import { Event } from "@/types/event";
|
import { Event } from "@/types/event";
|
||||||
import { getIconForLabel } from "@/utils/iconUtil";
|
import { getIconForLabel } from "@/utils/iconUtil";
|
||||||
import { ReviewSegment, REVIEW_PADDING } from "@/types/review";
|
import { ReviewSegment } from "@/types/review";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
@ -28,7 +31,7 @@ import { cn } from "@/lib/utils";
|
|||||||
type DetailStreamProps = {
|
type DetailStreamProps = {
|
||||||
reviewItems?: ReviewSegment[];
|
reviewItems?: ReviewSegment[];
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
onSeek: (timestamp: number) => void;
|
onSeek: (timestamp: number, play?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DetailStream({
|
export default function DetailStream({
|
||||||
@ -49,7 +52,6 @@ export default function DetailStream({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const effectiveTime = currentTime + annotationOffset / 1000;
|
const effectiveTime = currentTime + annotationOffset / 1000;
|
||||||
const PAD = 0; // REVIEW_PADDING ?? 2;
|
|
||||||
const [upload, setUpload] = useState<Event | undefined>(undefined);
|
const [upload, setUpload] = useState<Event | undefined>(undefined);
|
||||||
|
|
||||||
// Ensure we initialize the active review when reviewItems first arrive.
|
// Ensure we initialize the active review when reviewItems first arrive.
|
||||||
@ -64,8 +66,8 @@ export default function DetailStream({
|
|||||||
let closest: { r: ReviewSegment; diff: number } | undefined;
|
let closest: { r: ReviewSegment; diff: number } | undefined;
|
||||||
|
|
||||||
for (const r of reviewItems) {
|
for (const r of reviewItems) {
|
||||||
const start = (r.start_time ?? 0) - PAD;
|
const start = r.start_time ?? 0;
|
||||||
const end = (r.end_time ?? r.start_time ?? start) + PAD;
|
const end = r.end_time ?? r.start_time ?? start;
|
||||||
if (effectiveTime >= start && effectiveTime <= end) {
|
if (effectiveTime >= start && effectiveTime <= end) {
|
||||||
target = r;
|
target = r;
|
||||||
break;
|
break;
|
||||||
@ -78,12 +80,12 @@ export default function DetailStream({
|
|||||||
if (!target && closest) target = closest.r;
|
if (!target && closest) target = closest.r;
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
const start = (target.start_time ?? 0) - PAD;
|
const start = target.start_time ?? 0;
|
||||||
setActiveReviewId(
|
setActiveReviewId(
|
||||||
`review-${target.id ?? target.start_time ?? Math.floor(start)}`,
|
`review-${target.id ?? target.start_time ?? Math.floor(start)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [reviewItems, activeReviewId, effectiveTime, PAD]);
|
}, [reviewItems, activeReviewId, effectiveTime]);
|
||||||
|
|
||||||
// Auto-scroll to current time
|
// Auto-scroll to current time
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -99,8 +101,8 @@ export default function DetailStream({
|
|||||||
let closest: { r: ReviewSegment; diff: number } | undefined;
|
let closest: { r: ReviewSegment; diff: number } | undefined;
|
||||||
|
|
||||||
for (const r of items) {
|
for (const r of items) {
|
||||||
const start = (r.start_time ?? 0) - PAD;
|
const start = r.start_time ?? 0;
|
||||||
const end = (r.end_time ?? r.start_time ?? start) + PAD;
|
const end = r.end_time ?? r.start_time ?? start;
|
||||||
if (effectiveTime >= start && effectiveTime <= end) {
|
if (effectiveTime >= start && effectiveTime <= end) {
|
||||||
target = r;
|
target = r;
|
||||||
break;
|
break;
|
||||||
@ -113,7 +115,7 @@ export default function DetailStream({
|
|||||||
if (!target && closest) target = closest.r;
|
if (!target && closest) target = closest.r;
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
const start = (target.start_time ?? 0) - PAD;
|
const start = target.start_time ?? 0;
|
||||||
const id = `review-${target.id ?? target.start_time ?? Math.floor(start)}`;
|
const id = `review-${target.id ?? target.start_time ?? Math.floor(start)}`;
|
||||||
const element = scrollRef.current.querySelector(
|
const element = scrollRef.current.querySelector(
|
||||||
`[data-review-id="${id}"]`,
|
`[data-review-id="${id}"]`,
|
||||||
@ -132,15 +134,14 @@ export default function DetailStream({
|
|||||||
annotationOffset,
|
annotationOffset,
|
||||||
userInteracting,
|
userInteracting,
|
||||||
setProgrammaticScroll,
|
setProgrammaticScroll,
|
||||||
PAD,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Auto-select active review based on effectiveTime (if inside a review range)
|
// Auto-select active review based on effectiveTime (if inside a review range)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!reviewItems || reviewItems.length === 0) return;
|
if (!reviewItems || reviewItems.length === 0) return;
|
||||||
for (const r of reviewItems) {
|
for (const r of reviewItems) {
|
||||||
const start = (r.start_time ?? 0) - PAD;
|
const start = r.start_time ?? 0;
|
||||||
const end = (r.end_time ?? r.start_time ?? start) + PAD;
|
const end = r.end_time ?? r.start_time ?? start;
|
||||||
if (effectiveTime >= start && effectiveTime <= end) {
|
if (effectiveTime >= start && effectiveTime <= end) {
|
||||||
setActiveReviewId(
|
setActiveReviewId(
|
||||||
`review-${r.id ?? r.start_time ?? Math.floor(start)}`,
|
`review-${r.id ?? r.start_time ?? Math.floor(start)}`,
|
||||||
@ -148,7 +149,7 @@ export default function DetailStream({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [effectiveTime, reviewItems, PAD]);
|
}, [effectiveTime, reviewItems]);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
@ -173,7 +174,7 @@ export default function DetailStream({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
reviewItems?.map((review: ReviewSegment) => {
|
reviewItems?.map((review: ReviewSegment) => {
|
||||||
const id = `review-${review.id ?? review.start_time ?? Math.floor((review.start_time ?? 0) - PAD)}`;
|
const id = `review-${review.id ?? review.start_time ?? Math.floor(review.start_time ?? 0)}`;
|
||||||
return (
|
return (
|
||||||
<ReviewGroup
|
<ReviewGroup
|
||||||
key={id}
|
key={id}
|
||||||
@ -201,7 +202,7 @@ type ReviewGroupProps = {
|
|||||||
review: ReviewSegment;
|
review: ReviewSegment;
|
||||||
id: string;
|
id: string;
|
||||||
config: FrigateConfig;
|
config: FrigateConfig;
|
||||||
onSeek: (timestamp: number) => void;
|
onSeek: (timestamp: number, play?: boolean) => void;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
onActivate?: () => void;
|
onActivate?: () => void;
|
||||||
onOpenUpload?: (e: Event) => void;
|
onOpenUpload?: (e: Event) => void;
|
||||||
@ -219,18 +220,14 @@ function ReviewGroup({
|
|||||||
effectiveTime,
|
effectiveTime,
|
||||||
}: ReviewGroupProps) {
|
}: ReviewGroupProps) {
|
||||||
const { t } = useTranslation("views/events");
|
const { t } = useTranslation("views/events");
|
||||||
const PAD = REVIEW_PADDING ?? 2;
|
const start = review.start_time ?? 0;
|
||||||
|
|
||||||
// derive start timestamp from the review
|
|
||||||
const start = (review.start_time ?? 0) - PAD;
|
|
||||||
|
|
||||||
// display time first in the header
|
|
||||||
const displayTime = formatUnixTimestampToDateTime(start, {
|
const displayTime = formatUnixTimestampToDateTime(start, {
|
||||||
timezone: config.ui.timezone,
|
timezone: config.ui.timezone,
|
||||||
date_format:
|
date_format:
|
||||||
config.ui.time_format == "24hour"
|
config.ui.time_format == "24hour"
|
||||||
? t("time.formattedTimestamp.24hour", { ns: "common" })
|
? t("time.formattedTimestampHourMinuteSecond.24hour", { ns: "common" })
|
||||||
: t("time.formattedTimestamp.12hour", { ns: "common" }),
|
: t("time.formattedTimestampHourMinuteSecond.12hour", { ns: "common" }),
|
||||||
time_style: "medium",
|
time_style: "medium",
|
||||||
date_style: "medium",
|
date_style: "medium",
|
||||||
});
|
});
|
||||||
@ -268,6 +265,13 @@ function ReviewGroup({
|
|||||||
}
|
}
|
||||||
}, [review, t, fetchedEvents]);
|
}, [review, t, fetchedEvents]);
|
||||||
|
|
||||||
|
const reviewDuration =
|
||||||
|
review.end_time != null
|
||||||
|
? formatSecondsToDuration(
|
||||||
|
Math.max(0, Math.floor((review.end_time ?? 0) - start)),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-review-id={id}
|
data-review-id={id}
|
||||||
@ -287,6 +291,11 @@ function ReviewGroup({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="text-sm font-medium">{displayTime}</div>
|
<div className="text-sm font-medium">{displayTime}</div>
|
||||||
|
{reviewDuration && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{reviewDuration}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="text-xs text-muted-foreground">{reviewInfo}</div>
|
<div className="text-xs text-muted-foreground">{reviewInfo}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -325,7 +334,7 @@ function ReviewGroup({
|
|||||||
type EventCollapsibleProps = {
|
type EventCollapsibleProps = {
|
||||||
event: Event;
|
event: Event;
|
||||||
effectiveTime?: number;
|
effectiveTime?: number;
|
||||||
onSeek: (ts: number) => void;
|
onSeek: (ts: number, play?: boolean) => void;
|
||||||
onOpenUpload?: (e: Event) => void;
|
onOpenUpload?: (e: Event) => void;
|
||||||
};
|
};
|
||||||
function EventCollapsible({
|
function EventCollapsible({
|
||||||
@ -398,7 +407,7 @@ function EventCollapsible({
|
|||||||
event.id != selectedObjectId &&
|
event.id != selectedObjectId &&
|
||||||
(effectiveTime ?? 0) >= (event.start_time ?? 0) &&
|
(effectiveTime ?? 0) >= (event.start_time ?? 0) &&
|
||||||
(effectiveTime ?? 0) <= (event.end_time ?? event.start_time ?? 0) &&
|
(effectiveTime ?? 0) <= (event.end_time ?? event.start_time ?? 0) &&
|
||||||
"bg-secondary-highlight/80 outline-[1px] -outline-offset-[0.8px] outline-primary/40",
|
"bg-secondary-highlight outline-[1.5px] -outline-offset-[1.1px] outline-primary/40",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
@ -450,9 +459,7 @@ function EventCollapsible({
|
|||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<ObjectTimeline
|
<ObjectTimeline
|
||||||
eventId={event.id}
|
eventId={event.id}
|
||||||
onSeek={(ts) => {
|
onSeek={onSeek}
|
||||||
onSeek(ts);
|
|
||||||
}}
|
|
||||||
effectiveTime={effectiveTime}
|
effectiveTime={effectiveTime}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -464,11 +471,11 @@ function EventCollapsible({
|
|||||||
|
|
||||||
type LifecycleItemProps = {
|
type LifecycleItemProps = {
|
||||||
event: ObjectLifecycleSequence;
|
event: ObjectLifecycleSequence;
|
||||||
onSeek: (timestamp: number) => void;
|
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
onSeek?: (timestamp: number, play?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function LifecycleItem({ event, isActive }: LifecycleItemProps) {
|
function LifecycleItem({ event, isActive, onSeek }: LifecycleItemProps) {
|
||||||
const { t } = useTranslation("views/events");
|
const { t } = useTranslation("views/events");
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
@ -490,9 +497,15 @@ function LifecycleItem({ event, isActive }: LifecycleItemProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
role="button"
|
||||||
|
onClick={() => {
|
||||||
|
onSeek?.(event.timestamp ?? 0, false);
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 text-sm text-primary-variant",
|
"flex cursor-pointer items-center gap-2 text-sm text-primary-variant",
|
||||||
isActive ? "text-white" : "duration-500",
|
isActive
|
||||||
|
? "font-semibold text-primary dark:font-normal"
|
||||||
|
: "duration-500",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex size-4 items-center justify-center">
|
<div className="flex size-4 items-center justify-center">
|
||||||
@ -513,7 +526,7 @@ function ObjectTimeline({
|
|||||||
effectiveTime,
|
effectiveTime,
|
||||||
}: {
|
}: {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
onSeek: (ts: number) => void;
|
onSeek: (ts: number, play?: boolean) => void;
|
||||||
effectiveTime?: number;
|
effectiveTime?: number;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation("views/events");
|
const { t } = useTranslation("views/events");
|
||||||
@ -542,14 +555,12 @@ function ObjectTimeline({
|
|||||||
const isActive =
|
const isActive =
|
||||||
Math.abs((effectiveTime ?? 0) - (event.timestamp ?? 0)) <= 0.5;
|
Math.abs((effectiveTime ?? 0) - (event.timestamp ?? 0)) <= 0.5;
|
||||||
return (
|
return (
|
||||||
<div
|
<LifecycleItem
|
||||||
key={`${event.timestamp}-${event.source_id ?? idx}`}
|
key={`${event.timestamp}-${event.source_id ?? ""}-${idx}`}
|
||||||
onClick={() => {
|
event={event}
|
||||||
onSeek(event.timestamp);
|
onSeek={onSeek}
|
||||||
}}
|
isActive={isActive}
|
||||||
>
|
/>
|
||||||
<LifecycleItem event={event} onSeek={onSeek} isActive={isActive} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -283,15 +283,14 @@ export function RecordingView({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const manuallySetCurrentTime = useCallback(
|
const manuallySetCurrentTime = useCallback(
|
||||||
(time: number) => {
|
(time: number, play: boolean = false) => {
|
||||||
if (!currentTimeRange) {
|
if (!currentTimeRange) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentTime(time);
|
setCurrentTime(time);
|
||||||
|
|
||||||
if (currentTimeRange.after <= time && currentTimeRange.before >= time) {
|
if (currentTimeRange.after <= time && currentTimeRange.before >= time) {
|
||||||
mainControllerRef.current?.seekToTimestamp(time, true);
|
mainControllerRef.current?.seekToTimestamp(time, play);
|
||||||
} else {
|
} else {
|
||||||
updateSelectedSegment(time, true);
|
updateSelectedSegment(time, true);
|
||||||
}
|
}
|
||||||
@ -310,7 +309,7 @@ export function RecordingView({
|
|||||||
} else {
|
} else {
|
||||||
updateSelectedSegment(currentTime, true);
|
updateSelectedSegment(currentTime, true);
|
||||||
}
|
}
|
||||||
} else if (playerTime != currentTime) {
|
} else if (playerTime != currentTime && timelineType != "detail") {
|
||||||
mainControllerRef.current?.play();
|
mainControllerRef.current?.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1006,7 +1005,9 @@ function Timeline({
|
|||||||
) : timelineType == "detail" ? (
|
) : timelineType == "detail" ? (
|
||||||
<DetailStream
|
<DetailStream
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
onSeek={(timestamp) => manuallySetCurrentTime(timestamp, true)}
|
onSeek={(timestamp, play) =>
|
||||||
|
manuallySetCurrentTime(timestamp, play ?? true)
|
||||||
|
}
|
||||||
reviewItems={mainCameraReviewItems}
|
reviewItems={mainCameraReviewItems}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -36,8 +36,8 @@
|
|||||||
--secondary-foreground: hsl(222.2, 17.4%, 36.2%);
|
--secondary-foreground: hsl(222.2, 17.4%, 36.2%);
|
||||||
--secondary-foreground: 222.2 17.4% 36.2%;
|
--secondary-foreground: 222.2 17.4% 36.2%;
|
||||||
|
|
||||||
--secondary-highlight: hsl(0, 0%, 94%);
|
--secondary-highlight: hsl(210, 17.4%, 94%);
|
||||||
--secondary-highlight: 0 0% 94%;
|
--secondary-highlight: 210 17.4% 94%;
|
||||||
|
|
||||||
--neutral: hsl(0, 0%, 45.1%);
|
--neutral: hsl(0, 0%, 45.1%);
|
||||||
--neutral: 0 0% 45.1%;
|
--neutral: 0 0% 45.1%;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user