Add Detail stream in History view (#20525)

* new type

* activity stream panel

* use context provider for activity stream

* new activity stream panel in history view

* overlay for object tracking details in history view

* use overlay in video player

* don't refetch timeline

* fix activity stream group from being highlighted prematurely

* use annotation offset

* fix scrolling and use custom hook for interaction

* memoize to prevent unnecessary renders

* i18n and timestamp formatting

* add annotation offset slider

* bg color

* add collapsible component

* refactor

* rename activity to detail

* fix merge conflicts

* i18n

* more i18n
This commit is contained in:
Josh Hawkins
2025-10-16 07:24:14 -06:00
committed by GitHub
parent 2e7a2fd780
commit b52044aecc
16 changed files with 1818 additions and 326 deletions
+47 -6
View File
@@ -19,6 +19,8 @@ import { usePersistence } from "@/hooks/use-persistence";
import { cn } from "@/lib/utils";
import { ASPECT_VERTICAL_LAYOUT, RecordingPlayerError } from "@/types/record";
import { useTranslation } from "react-i18next";
import ObjectTrackOverlay from "@/components/overlay/ObjectTrackOverlay";
import { useDetailStream } from "@/context/detail-stream-context";
// Android native hls does not seek correctly
const USE_NATIVE_HLS = !isAndroid;
@@ -47,6 +49,7 @@ type HlsVideoPlayerProps = {
onPlayerLoaded?: () => void;
onTimeUpdate?: (time: number) => void;
onPlaying?: () => void;
onSeekToTime?: (timestamp: number) => void;
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined;
toggleFullscreen?: () => void;
@@ -66,6 +69,7 @@ export default function HlsVideoPlayer({
onPlayerLoaded,
onTimeUpdate,
onPlaying,
onSeekToTime,
setFullResolution,
onUploadFrame,
toggleFullscreen,
@@ -73,6 +77,13 @@ export default function HlsVideoPlayer({
}: HlsVideoPlayerProps) {
const { t } = useTranslation("components/player");
const { data: config } = useSWR<FrigateConfig>("config");
const {
selectedObjectId,
selectedObjectTimeline,
currentTime,
camera,
isDetailMode,
} = useDetailStream();
// playback
@@ -84,17 +95,19 @@ export default function HlsVideoPlayer({
const handleLoadedMetadata = useCallback(() => {
setLoadedMetadata(true);
if (videoRef.current) {
const width = videoRef.current.videoWidth;
const height = videoRef.current.videoHeight;
if (setFullResolution) {
setFullResolution({
width: videoRef.current.videoWidth,
height: videoRef.current.videoHeight,
width,
height,
});
}
setTallCamera(
videoRef.current.videoWidth / videoRef.current.videoHeight <
ASPECT_VERTICAL_LAYOUT,
);
setVideoDimensions({ width, height });
setTallCamera(width / height < ASPECT_VERTICAL_LAYOUT);
}
}, [videoRef, setFullResolution]);
@@ -174,6 +187,10 @@ export default function HlsVideoPlayer({
const [controls, setControls] = useState(isMobile);
const [controlsOpen, setControlsOpen] = useState(false);
const [zoomScale, setZoomScale] = useState(1.0);
const [videoDimensions, setVideoDimensions] = useState<{
width: number;
height: number;
}>({ width: 0, height: 0 });
useEffect(() => {
if (!isDesktop) {
@@ -296,6 +313,30 @@ export default function HlsVideoPlayer({
height: isMobile ? "100%" : undefined,
}}
>
{isDetailMode &&
selectedObjectId &&
camera &&
currentTime &&
videoDimensions.width > 0 &&
videoDimensions.height > 0 && (
<div className="absolute z-50 size-full">
<ObjectTrackOverlay
key={`${selectedObjectId}-${currentTime}`}
camera={camera}
selectedObjectId={selectedObjectId}
currentTime={currentTime}
videoWidth={videoDimensions.width}
videoHeight={videoDimensions.height}
className="absolute inset-0 z-10"
onSeekToTime={(timestamp) => {
if (onSeekToTime) {
onSeekToTime(timestamp);
}
}}
objectTimeline={selectedObjectTimeline}
/>
</div>
)}
<video
ref={videoRef}
className={`size-full rounded-lg bg-black md:rounded-2xl ${loadedMetadata ? "" : "invisible"} cursor-pointer`}
@@ -32,6 +32,7 @@ type DynamicVideoPlayerProps = {
onControllerReady: (controller: DynamicVideoController) => void;
onTimestampUpdate?: (timestamp: number) => void;
onClipEnded?: () => void;
onSeekToTime?: (timestamp: number) => void;
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
toggleFullscreen: () => void;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
@@ -49,6 +50,7 @@ export default function DynamicVideoPlayer({
onControllerReady,
onTimestampUpdate,
onClipEnded,
onSeekToTime,
setFullResolution,
toggleFullscreen,
containerRef,
@@ -265,6 +267,7 @@ export default function DynamicVideoPlayer({
onTimeUpdate={onTimeUpdate}
onPlayerLoaded={onPlayerLoaded}
onClipEnded={onValidateClipEnd}
onSeekToTime={onSeekToTime}
onPlaying={() => {
if (isScrubbing) {
playerRef.current?.pause();