mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-19 11:36:43 +03:00
Compare commits
No commits in common. "d44340eca611e1fbc27457e39e9ce69b6554125e" and "1e50d83d06e3f540cb6af28d07b83ae1ed8db1a6" have entirely different histories.
d44340eca6
...
1e50d83d06
@ -348,26 +348,6 @@ export function GeneralFilterContent({
|
|||||||
onClose,
|
onClose,
|
||||||
}: GeneralFilterContentProps) {
|
}: GeneralFilterContentProps) {
|
||||||
const { t } = useTranslation(["components/filter"]);
|
const { t } = useTranslation(["components/filter"]);
|
||||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const allAudioListenLabels = useMemo<string[]>(() => {
|
|
||||||
if (!config) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const labels = new Set<string>();
|
|
||||||
Object.values(config.cameras).forEach((camera) => {
|
|
||||||
if (camera?.audio?.enabled) {
|
|
||||||
camera.audio.listen.forEach((label) => {
|
|
||||||
labels.add(label);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return [...labels].sort();
|
|
||||||
}, [config]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="overflow-x-hidden">
|
<div className="overflow-x-hidden">
|
||||||
@ -393,10 +373,7 @@ export function GeneralFilterContent({
|
|||||||
{allLabels.map((item) => (
|
{allLabels.map((item) => (
|
||||||
<FilterSwitch
|
<FilterSwitch
|
||||||
key={item}
|
key={item}
|
||||||
label={getTranslatedLabel(
|
label={getTranslatedLabel(item)}
|
||||||
item,
|
|
||||||
allAudioListenLabels.includes(item) ? "audio" : "object",
|
|
||||||
)}
|
|
||||||
isChecked={currentLabels?.includes(item) ?? false}
|
isChecked={currentLabels?.includes(item) ?? false}
|
||||||
onCheckedChange={(isChecked) => {
|
onCheckedChange={(isChecked) => {
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
|
|||||||
@ -58,47 +58,6 @@ export default function ObjectTrackOverlay({
|
|||||||
|
|
||||||
const effectiveCurrentTime = currentTime - annotationOffset / 1000;
|
const effectiveCurrentTime = currentTime - annotationOffset / 1000;
|
||||||
|
|
||||||
const {
|
|
||||||
pathStroke,
|
|
||||||
pointRadius,
|
|
||||||
pointStroke,
|
|
||||||
zoneStroke,
|
|
||||||
boxStroke,
|
|
||||||
highlightRadius,
|
|
||||||
} = useMemo(() => {
|
|
||||||
const BASE_WIDTH = 1280;
|
|
||||||
const BASE_HEIGHT = 720;
|
|
||||||
const BASE_PATH_STROKE = 5;
|
|
||||||
const BASE_POINT_RADIUS = 7;
|
|
||||||
const BASE_POINT_STROKE = 3;
|
|
||||||
const BASE_ZONE_STROKE = 5;
|
|
||||||
const BASE_BOX_STROKE = 5;
|
|
||||||
const BASE_HIGHLIGHT_RADIUS = 5;
|
|
||||||
|
|
||||||
const scale = Math.sqrt(
|
|
||||||
(videoWidth * videoHeight) / (BASE_WIDTH * BASE_HEIGHT),
|
|
||||||
);
|
|
||||||
|
|
||||||
const pathStroke = Math.max(1, Math.round(BASE_PATH_STROKE * scale));
|
|
||||||
const pointRadius = Math.max(2, Math.round(BASE_POINT_RADIUS * scale));
|
|
||||||
const pointStroke = Math.max(1, Math.round(BASE_POINT_STROKE * scale));
|
|
||||||
const zoneStroke = Math.max(1, Math.round(BASE_ZONE_STROKE * scale));
|
|
||||||
const boxStroke = Math.max(1, Math.round(BASE_BOX_STROKE * scale));
|
|
||||||
const highlightRadius = Math.max(
|
|
||||||
2,
|
|
||||||
Math.round(BASE_HIGHLIGHT_RADIUS * scale),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
pathStroke,
|
|
||||||
pointRadius,
|
|
||||||
pointStroke,
|
|
||||||
zoneStroke,
|
|
||||||
boxStroke,
|
|
||||||
highlightRadius,
|
|
||||||
};
|
|
||||||
}, [videoWidth, videoHeight]);
|
|
||||||
|
|
||||||
// Fetch all event data in a single request (CSV ids)
|
// Fetch all event data in a single request (CSV ids)
|
||||||
const { data: eventsData } = useSWR<Event[]>(
|
const { data: eventsData } = useSWR<Event[]>(
|
||||||
selectedObjectIds.length > 0
|
selectedObjectIds.length > 0
|
||||||
@ -239,21 +198,16 @@ export default function ObjectTrackOverlay({
|
|||||||
b.timestamp - a.timestamp,
|
b.timestamp - a.timestamp,
|
||||||
)[0]?.data?.zones || [];
|
)[0]?.data?.zones || [];
|
||||||
|
|
||||||
// bounding box - only show if there's a timeline event at/near the current time with a box
|
// bounding box (with tolerance for browsers with seek precision by-design issues)
|
||||||
// Search all timeline events (not just those before current time) to find one matching the seek position
|
const boxCandidates = timelineData?.filter(
|
||||||
const nearbyTimelineEvent = timelineData
|
(event: TrackingDetailsSequence) =>
|
||||||
?.filter((event: TrackingDetailsSequence) => event.data.box)
|
event.timestamp <= effectiveCurrentTime + TOLERANCE &&
|
||||||
.sort(
|
event.data.box,
|
||||||
(a: TrackingDetailsSequence, b: TrackingDetailsSequence) =>
|
);
|
||||||
Math.abs(a.timestamp - effectiveCurrentTime) -
|
const currentBox = boxCandidates?.sort(
|
||||||
Math.abs(b.timestamp - effectiveCurrentTime),
|
(a: TrackingDetailsSequence, b: TrackingDetailsSequence) =>
|
||||||
)
|
b.timestamp - a.timestamp,
|
||||||
.find(
|
)[0]?.data?.box;
|
||||||
(event: TrackingDetailsSequence) =>
|
|
||||||
Math.abs(event.timestamp - effectiveCurrentTime) <= TOLERANCE,
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentBox = nearbyTimelineEvent?.data?.box;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
objectId,
|
objectId,
|
||||||
@ -379,7 +333,7 @@ export default function ObjectTrackOverlay({
|
|||||||
points={zone.points}
|
points={zone.points}
|
||||||
fill={zone.fill}
|
fill={zone.fill}
|
||||||
stroke={zone.stroke}
|
stroke={zone.stroke}
|
||||||
strokeWidth={zoneStroke}
|
strokeWidth="5"
|
||||||
opacity="0.7"
|
opacity="0.7"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -399,7 +353,7 @@ export default function ObjectTrackOverlay({
|
|||||||
d={generateStraightPath(absolutePositions)}
|
d={generateStraightPath(absolutePositions)}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={objData.color}
|
stroke={objData.color}
|
||||||
strokeWidth={pathStroke}
|
strokeWidth="5"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
@ -411,13 +365,13 @@ export default function ObjectTrackOverlay({
|
|||||||
<circle
|
<circle
|
||||||
cx={pos.x}
|
cx={pos.x}
|
||||||
cy={pos.y}
|
cy={pos.y}
|
||||||
r={pointRadius}
|
r="7"
|
||||||
fill={getPointColor(
|
fill={getPointColor(
|
||||||
objData.color,
|
objData.color,
|
||||||
pos.lifecycle_item?.class_type,
|
pos.lifecycle_item?.class_type,
|
||||||
)}
|
)}
|
||||||
stroke="white"
|
stroke="white"
|
||||||
strokeWidth={pointStroke}
|
strokeWidth="3"
|
||||||
style={{ cursor: onSeekToTime ? "pointer" : "default" }}
|
style={{ cursor: onSeekToTime ? "pointer" : "default" }}
|
||||||
onClick={() => handlePointClick(pos.timestamp)}
|
onClick={() => handlePointClick(pos.timestamp)}
|
||||||
/>
|
/>
|
||||||
@ -446,7 +400,7 @@ export default function ObjectTrackOverlay({
|
|||||||
height={objData.currentBox[3] * videoHeight}
|
height={objData.currentBox[3] * videoHeight}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={objData.color}
|
stroke={objData.color}
|
||||||
strokeWidth={boxStroke}
|
strokeWidth="5"
|
||||||
opacity="0.9"
|
opacity="0.9"
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
@ -458,10 +412,10 @@ export default function ObjectTrackOverlay({
|
|||||||
(objData.currentBox[1] + objData.currentBox[3]) *
|
(objData.currentBox[1] + objData.currentBox[3]) *
|
||||||
videoHeight
|
videoHeight
|
||||||
}
|
}
|
||||||
r={highlightRadius}
|
r="5"
|
||||||
fill="rgb(255, 255, 0)" // yellow highlight
|
fill="rgb(255, 255, 0)" // yellow highlight
|
||||||
stroke={objData.color}
|
stroke={objData.color}
|
||||||
strokeWidth={boxStroke}
|
strokeWidth="5"
|
||||||
opacity="1"
|
opacity="1"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import Heading from "@/components/ui/heading";
|
|||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||||
import { getIconForLabel } from "@/utils/iconUtil";
|
import { getIconForLabel } from "@/utils/iconUtil";
|
||||||
import { LuCircle, LuFolderX, LuSettings } from "react-icons/lu";
|
import { LuCircle, LuSettings } from "react-icons/lu";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@ -37,12 +37,9 @@ import { HiDotsHorizontal } from "react-icons/hi";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useDetailStream } from "@/context/detail-stream-context";
|
import { useDetailStream } from "@/context/detail-stream-context";
|
||||||
import { isDesktop, isIOS, isMobileOnly, isSafari } from "react-device-detect";
|
import { isDesktop, isIOS } from "react-device-detect";
|
||||||
import Chip from "@/components/indicators/Chip";
|
import Chip from "@/components/indicators/Chip";
|
||||||
import { FaDownload, FaHistory } from "react-icons/fa";
|
import { FaDownload, FaHistory } from "react-icons/fa";
|
||||||
import { useApiHost } from "@/api";
|
|
||||||
import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator";
|
|
||||||
import ObjectTrackOverlay from "../ObjectTrackOverlay";
|
|
||||||
|
|
||||||
type TrackingDetailsProps = {
|
type TrackingDetailsProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -59,19 +56,9 @@ export function TrackingDetails({
|
|||||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
const { t } = useTranslation(["views/explore"]);
|
const { t } = useTranslation(["views/explore"]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const apiHost = useApiHost();
|
|
||||||
const imgRef = useRef<HTMLImageElement | null>(null);
|
|
||||||
const [imgLoaded, setImgLoaded] = useState(false);
|
|
||||||
const [displaySource, _setDisplaySource] = useState<"video" | "image">(
|
|
||||||
"video",
|
|
||||||
);
|
|
||||||
const { setSelectedObjectIds, annotationOffset, setAnnotationOffset } =
|
const { setSelectedObjectIds, annotationOffset, setAnnotationOffset } =
|
||||||
useDetailStream();
|
useDetailStream();
|
||||||
|
|
||||||
// manualOverride holds a record-stream timestamp explicitly chosen by the
|
|
||||||
// user (eg, clicking a lifecycle row). When null we display `currentTime`.
|
|
||||||
const [manualOverride, setManualOverride] = useState<number | null>(null);
|
|
||||||
|
|
||||||
// event.start_time is detect time, convert to record, then subtract padding
|
// event.start_time is detect time, convert to record, then subtract padding
|
||||||
const [currentTime, setCurrentTime] = useState(
|
const [currentTime, setCurrentTime] = useState(
|
||||||
(event.start_time ?? 0) + annotationOffset / 1000 - REVIEW_PADDING,
|
(event.start_time ?? 0) + annotationOffset / 1000 - REVIEW_PADDING,
|
||||||
@ -86,13 +73,9 @@ export function TrackingDetails({
|
|||||||
|
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
// Use manualOverride (set when seeking in image mode) if present so
|
|
||||||
// lifecycle rows and overlays follow image-mode seeks. Otherwise fall
|
|
||||||
// back to currentTime used for video mode.
|
|
||||||
const effectiveTime = useMemo(() => {
|
const effectiveTime = useMemo(() => {
|
||||||
const displayedRecordTime = manualOverride ?? currentTime;
|
return currentTime - annotationOffset / 1000;
|
||||||
return displayedRecordTime - annotationOffset / 1000;
|
}, [currentTime, annotationOffset]);
|
||||||
}, [manualOverride, currentTime, annotationOffset]);
|
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [_selectedZone, setSelectedZone] = useState("");
|
const [_selectedZone, setSelectedZone] = useState("");
|
||||||
@ -135,30 +118,20 @@ export function TrackingDetails({
|
|||||||
|
|
||||||
const handleLifecycleClick = useCallback(
|
const handleLifecycleClick = useCallback(
|
||||||
(item: TrackingDetailsSequence) => {
|
(item: TrackingDetailsSequence) => {
|
||||||
if (!videoRef.current && !imgRef.current) return;
|
if (!videoRef.current) return;
|
||||||
|
|
||||||
// Convert lifecycle timestamp (detect stream) to record stream time
|
// Convert lifecycle timestamp (detect stream) to record stream time
|
||||||
const targetTimeRecord = item.timestamp + annotationOffset / 1000;
|
const targetTimeRecord = item.timestamp + annotationOffset / 1000;
|
||||||
|
|
||||||
if (displaySource === "image") {
|
// Convert to video-relative time for seeking
|
||||||
// For image mode: set a manual override timestamp and update
|
|
||||||
// currentTime so overlays render correctly.
|
|
||||||
setManualOverride(targetTimeRecord);
|
|
||||||
setCurrentTime(targetTimeRecord);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For video mode: convert to video-relative time and seek player
|
|
||||||
const eventStartRecord =
|
const eventStartRecord =
|
||||||
(event.start_time ?? 0) + annotationOffset / 1000;
|
(event.start_time ?? 0) + annotationOffset / 1000;
|
||||||
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
||||||
const relativeTime = targetTimeRecord - videoStartTime;
|
const relativeTime = targetTimeRecord - videoStartTime;
|
||||||
|
|
||||||
if (videoRef.current) {
|
videoRef.current.currentTime = relativeTime;
|
||||||
videoRef.current.currentTime = relativeTime;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[event.start_time, annotationOffset, displaySource],
|
[event.start_time, annotationOffset],
|
||||||
);
|
);
|
||||||
|
|
||||||
const formattedStart = config
|
const formattedStart = config
|
||||||
@ -199,20 +172,11 @@ export function TrackingDetails({
|
|||||||
}, [eventSequence]);
|
}, [eventSequence]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (seekToTimestamp === null) return;
|
if (seekToTimestamp === null || !videoRef.current) return;
|
||||||
|
|
||||||
if (displaySource === "image") {
|
|
||||||
// For image mode, set the manual override so the snapshot updates to
|
|
||||||
// the exact record timestamp.
|
|
||||||
setManualOverride(seekToTimestamp);
|
|
||||||
setSeekToTimestamp(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// seekToTimestamp is a record stream timestamp
|
// seekToTimestamp is a record stream timestamp
|
||||||
// event.start_time is detect stream time, convert to record
|
// event.start_time is detect stream time, convert to record
|
||||||
// The video clip starts at (eventStartRecord - REVIEW_PADDING)
|
// The video clip starts at (eventStartRecord - REVIEW_PADDING)
|
||||||
if (!videoRef.current) return;
|
|
||||||
const eventStartRecord = event.start_time + annotationOffset / 1000;
|
const eventStartRecord = event.start_time + annotationOffset / 1000;
|
||||||
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
||||||
const relativeTime = seekToTimestamp - videoStartTime;
|
const relativeTime = seekToTimestamp - videoStartTime;
|
||||||
@ -220,14 +184,7 @@ export function TrackingDetails({
|
|||||||
videoRef.current.currentTime = relativeTime;
|
videoRef.current.currentTime = relativeTime;
|
||||||
}
|
}
|
||||||
setSeekToTimestamp(null);
|
setSeekToTimestamp(null);
|
||||||
}, [
|
}, [seekToTimestamp, event.start_time, annotationOffset]);
|
||||||
seekToTimestamp,
|
|
||||||
event.start_time,
|
|
||||||
annotationOffset,
|
|
||||||
apiHost,
|
|
||||||
event.camera,
|
|
||||||
displaySource,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const isWithinEventRange =
|
const isWithinEventRange =
|
||||||
effectiveTime !== undefined &&
|
effectiveTime !== undefined &&
|
||||||
@ -330,27 +287,6 @@ export function TrackingDetails({
|
|||||||
[event.start_time, annotationOffset],
|
[event.start_time, annotationOffset],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [src, setSrc] = useState(
|
|
||||||
`${apiHost}api/${event.camera}/recordings/${currentTime + REVIEW_PADDING}/snapshot.jpg?height=500`,
|
|
||||||
);
|
|
||||||
const [hasError, setHasError] = useState(false);
|
|
||||||
|
|
||||||
// Derive the record timestamp to display: manualOverride if present,
|
|
||||||
// otherwise use currentTime.
|
|
||||||
const displayedRecordTime = manualOverride ?? currentTime;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (displayedRecordTime) {
|
|
||||||
const newSrc = `${apiHost}api/${event.camera}/recordings/${displayedRecordTime}/snapshot.jpg?height=500`;
|
|
||||||
setSrc(newSrc);
|
|
||||||
}
|
|
||||||
setImgLoaded(false);
|
|
||||||
setHasError(false);
|
|
||||||
|
|
||||||
// we know that these deps are correct
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [displayedRecordTime]);
|
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
@ -368,10 +304,9 @@ export function TrackingDetails({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-center",
|
"flex w-full items-center justify-center",
|
||||||
isDesktop && "overflow-hidden",
|
isDesktop && "overflow-hidden",
|
||||||
cameraAspect === "tall" ? "max-h-[50dvh] lg:max-h-[70dvh]" : "w-full",
|
cameraAspect === "tall" ? "max-h-[50dvh] lg:max-h-[70dvh]" : "w-full",
|
||||||
cameraAspect === "tall" && isMobileOnly && "w-full",
|
|
||||||
cameraAspect !== "tall" && isDesktop && "flex-[3]",
|
cameraAspect !== "tall" && isDesktop && "flex-[3]",
|
||||||
)}
|
)}
|
||||||
style={{ aspectRatio: aspectRatio }}
|
style={{ aspectRatio: aspectRatio }}
|
||||||
@ -383,75 +318,21 @@ export function TrackingDetails({
|
|||||||
cameraAspect === "tall" ? "h-full" : "w-full",
|
cameraAspect === "tall" ? "h-full" : "w-full",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{displaySource == "video" && (
|
<HlsVideoPlayer
|
||||||
<HlsVideoPlayer
|
videoRef={videoRef}
|
||||||
videoRef={videoRef}
|
containerRef={containerRef}
|
||||||
containerRef={containerRef}
|
visible={true}
|
||||||
visible={true}
|
currentSource={videoSource}
|
||||||
currentSource={videoSource}
|
hotKeys={false}
|
||||||
hotKeys={false}
|
supportsFullscreen={false}
|
||||||
supportsFullscreen={false}
|
fullscreen={false}
|
||||||
fullscreen={false}
|
frigateControls={true}
|
||||||
frigateControls={true}
|
onTimeUpdate={handleTimeUpdate}
|
||||||
onTimeUpdate={handleTimeUpdate}
|
onSeekToTime={handleSeekToTime}
|
||||||
onSeekToTime={handleSeekToTime}
|
isDetailMode={true}
|
||||||
isDetailMode={true}
|
camera={event.camera}
|
||||||
camera={event.camera}
|
currentTimeOverride={currentTime}
|
||||||
currentTimeOverride={currentTime}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{displaySource == "image" && (
|
|
||||||
<>
|
|
||||||
<ImageLoadingIndicator
|
|
||||||
className="absolute inset-0"
|
|
||||||
imgLoaded={imgLoaded}
|
|
||||||
/>
|
|
||||||
{hasError && (
|
|
||||||
<div className="relative aspect-video">
|
|
||||||
<div className="flex flex-col items-center justify-center p-20 text-center">
|
|
||||||
<LuFolderX className="size-16" />
|
|
||||||
{t("objectLifecycle.noImageFound")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn("relative", imgLoaded ? "visible" : "invisible")}
|
|
||||||
>
|
|
||||||
<div className="absolute z-50 size-full">
|
|
||||||
<ObjectTrackOverlay
|
|
||||||
key={`overlay-${displayedRecordTime}`}
|
|
||||||
camera={event.camera}
|
|
||||||
showBoundingBoxes={true}
|
|
||||||
currentTime={displayedRecordTime}
|
|
||||||
videoWidth={imgRef?.current?.naturalWidth ?? 0}
|
|
||||||
videoHeight={imgRef?.current?.naturalHeight ?? 0}
|
|
||||||
className="absolute inset-0 z-10"
|
|
||||||
onSeekToTime={handleSeekToTime}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
key={event.id}
|
|
||||||
ref={imgRef}
|
|
||||||
className={cn(
|
|
||||||
"max-h-[50dvh] max-w-full select-none rounded-lg object-contain",
|
|
||||||
)}
|
|
||||||
loading={isSafari ? "eager" : "lazy"}
|
|
||||||
style={
|
|
||||||
isIOS
|
|
||||||
? {
|
|
||||||
WebkitUserSelect: "none",
|
|
||||||
WebkitTouchCallout: "none",
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
draggable={false}
|
|
||||||
src={src}
|
|
||||||
onLoad={() => setImgLoaded(true)}
|
|
||||||
onError={() => setHasError(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute top-2 z-[5] flex items-center gap-2",
|
"absolute top-2 z-[5] flex items-center gap-2",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user