Skip to content

Motion review #10221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 90 additions & 48 deletions web/src/components/player/DynamicVideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { Recording } from "@/types/record";
import { Preview } from "@/types/preview";
import { DynamicPlayback } from "@/types/playback";

type PlayerMode = "playback" | "scrubbing";

/**
* Dynamically switches between video playback and scrubbing preview player.
*/
Expand All @@ -26,13 +28,15 @@ type DynamicVideoPlayerProps = {
camera: string;
timeRange: { start: number; end: number };
cameraPreviews: Preview[];
previewOnly?: boolean;
onControllerReady?: (controller: DynamicVideoController) => void;
};
export default function DynamicVideoPlayer({
className,
camera,
timeRange,
cameraPreviews,
previewOnly = false,
onControllerReady,
}: DynamicVideoPlayerProps) {
const apiHost = useApiHost();
Expand Down Expand Up @@ -60,7 +64,7 @@ export default function DynamicVideoPlayer({

const playerRef = useRef<Player | undefined>(undefined);
const previewRef = useRef<Player | undefined>(undefined);
const [isScrubbing, setIsScrubbing] = useState(false);
const [isScrubbing, setIsScrubbing] = useState(previewOnly);
const [hasPreview, setHasPreview] = useState(false);
const [focusedItem, setFocusedItem] = useState<Timeline | undefined>(
undefined,
Expand All @@ -74,10 +78,11 @@ export default function DynamicVideoPlayer({
playerRef,
previewRef,
(config.cameras[camera]?.detect?.annotation_offset || 0) / 1000,
previewOnly ? "scrubbing" : "playback",
setIsScrubbing,
setFocusedItem,
);
}, [camera, config]);
}, [camera, config, previewOnly]);

// keyboard control

Expand Down Expand Up @@ -144,6 +149,7 @@ export default function DynamicVideoPlayer({
const initialPreviewSource = useMemo(() => {
const preview = cameraPreviews.find(
(preview) =>
preview.camera == camera &&
Math.round(preview.start) >= timeRange.start &&
Math.floor(preview.end) <= timeRange.end,
);
Expand Down Expand Up @@ -172,12 +178,12 @@ export default function DynamicVideoPlayer({
};
}, [timeRange]);
const { data: recordings } = useSWR<Recording[]>(
[`${camera}/recordings`, recordingParams],
previewOnly ? null : [`${camera}/recordings`, recordingParams],
{ revalidateOnFocus: false },
);

useEffect(() => {
if (!controller || !recordings || recordings.length == 0) {
if (!controller || (!previewOnly && !recordings)) {
return;
}

Expand All @@ -191,13 +197,14 @@ export default function DynamicVideoPlayer({

const preview = cameraPreviews.find(
(preview) =>
preview.camera == camera &&
Math.round(preview.start) >= timeRange.start &&
Math.floor(preview.end) <= timeRange.end,
);
setHasPreview(preview != undefined);

controller.newPlayback({
recordings,
recordings: recordings ?? [],
playbackUri,
preview,
});
Expand All @@ -212,49 +219,53 @@ export default function DynamicVideoPlayer({

return (
<div className={className}>
<div
className={`w-full relative ${
hasPreview && isScrubbing ? "hidden" : "visible"
}`}
>
<VideoPlayer
options={{
preload: "auto",
autoplay: true,
sources: [initialPlaybackSource],
aspectRatio: tallVideo ? "16:9" : undefined,
controlBar: {
remainingTimeDisplay: false,
progressControl: {
seekBar: false,
},
},
}}
seekOptions={{ forward: 10, backward: 5 }}
onReady={(player) => {
playerRef.current = player;
player.on("playing", () => setFocusedItem(undefined));
player.on("timeupdate", () => {
controller.updateProgress(player.currentTime() || 0);
});
player.on("ended", () => controller.fireClipChangeEvent("forward"));

if (onControllerReady) {
onControllerReady(controller);
}
}}
onDispose={() => {
playerRef.current = undefined;
}}
{!previewOnly && (
<div
className={`w-full relative ${
hasPreview && isScrubbing ? "hidden" : "visible"
}`}
>
{config && focusedItem && (
<TimelineEventOverlay
timeline={focusedItem}
cameraConfig={config.cameras[camera]}
/>
)}
</VideoPlayer>
</div>
<VideoPlayer
options={{
preload: "auto",
autoplay: true,
sources: [initialPlaybackSource],
aspectRatio: tallVideo ? "16:9" : undefined,
controlBar: {
remainingTimeDisplay: false,
progressControl: {
seekBar: false,
},
},
}}
seekOptions={{ forward: 10, backward: 5 }}
onReady={(player) => {
playerRef.current = player;
player.on("playing", () => setFocusedItem(undefined));
player.on("timeupdate", () => {
controller.updateProgress(player.currentTime() || 0);
});
player.on("ended", () =>
controller.fireClipChangeEvent("forward"),
);

if (onControllerReady) {
onControllerReady(controller);
}
}}
onDispose={() => {
playerRef.current = undefined;
}}
>
{config && focusedItem && (
<TimelineEventOverlay
timeline={focusedItem}
cameraConfig={config.cameras[camera]}
/>
)}
</VideoPlayer>
</div>
)}
<div
className={`w-full ${hasPreview && isScrubbing ? "visible" : "hidden"}`}
>
Expand All @@ -274,6 +285,10 @@ export default function DynamicVideoPlayer({
player.pause();
player.on("seeked", () => controller.finishedSeeking());
player.on("loadeddata", () => controller.previewReady());

if (previewOnly && onControllerReady) {
onControllerReady(controller);
}
}}
onDispose={() => {
previewRef.current = undefined;
Expand All @@ -290,7 +305,7 @@ export class DynamicVideoController {
private previewRef: MutableRefObject<Player | undefined>;
private setScrubbing: (isScrubbing: boolean) => void;
private setFocusedItem: (timeline: Timeline) => void;
private playerMode: "playback" | "scrubbing" = "playback";
private playerMode: PlayerMode = "playback";

// playback
private recordings: Recording[] = [];
Expand All @@ -299,6 +314,7 @@ export class DynamicVideoController {
undefined;
private annotationOffset: number;
private timeToStart: number | undefined = undefined;
private clipChangeLockout: boolean = true;

// preview
private preview: Preview | undefined = undefined;
Expand All @@ -310,12 +326,14 @@ export class DynamicVideoController {
playerRef: MutableRefObject<Player | undefined>,
previewRef: MutableRefObject<Player | undefined>,
annotationOffset: number,
defaultMode: PlayerMode,
setScrubbing: (isScrubbing: boolean) => void,
setFocusedItem: (timeline: Timeline) => void,
) {
this.playerRef = playerRef;
this.previewRef = previewRef;
this.annotationOffset = annotationOffset;
this.playerMode = defaultMode;
this.setScrubbing = setScrubbing;
this.setFocusedItem = setFocusedItem;
}
Expand Down Expand Up @@ -427,17 +445,39 @@ export class DynamicVideoController {
}

if (time > this.preview.end) {
if (this.clipChangeLockout) {
return;
}

if (this.playerMode == "scrubbing") {
this.playerMode = "playback";
this.setScrubbing(false);
this.timeToSeek = undefined;
this.seeking = false;
this.readyToScrub = false;
this.clipChangeLockout = true;
this.fireClipChangeEvent("forward");
}
return;
}

if (time < this.preview.start) {
if (this.clipChangeLockout) {
return;
}

if (this.playerMode == "scrubbing") {
this.playerMode = "playback";
this.setScrubbing(false);
this.timeToSeek = undefined;
this.seeking = false;
this.readyToScrub = false;
this.clipChangeLockout = true;
this.fireClipChangeEvent("backward");
}
return;
}

if (this.playerMode != "scrubbing") {
this.playerMode = "scrubbing";
this.playerRef.current?.pause();
Expand All @@ -459,6 +499,8 @@ export class DynamicVideoController {
return;
}

this.clipChangeLockout = false;

if (
this.timeToSeek &&
this.timeToSeek != this.previewRef.current?.currentTime()
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/player/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default function VideoPlayer({

return (
<div data-vjs-player>
<div ref={videoRef} />
<div className="rounded-2xl overflow-hidden" ref={videoRef} />
{children}
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions web/src/pages/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import useOverlayState from "@/hooks/use-overlay-state";
import { FrigateConfig } from "@/types/frigateConfig";
import { Preview } from "@/types/preview";
import { ReviewFilter, ReviewSegment, ReviewSeverity } from "@/types/review";
import DesktopRecordingView from "@/views/events/DesktopRecordingView";
import EventView from "@/views/events/EventView";
import RecordingView from "@/views/events/RecordingView";
import axios from "axios";
import { useCallback, useMemo, useState } from "react";
import useSWR from "swr";
Expand Down Expand Up @@ -220,7 +220,7 @@ export default function Events() {

if (selectedData) {
return (
<DesktopRecordingView
<RecordingView
reviewItems={selectedData.cameraSegments}
selectedReview={selectedData.selected}
relevantPreviews={selectedData.cameraPreviews}
Expand Down
32 changes: 31 additions & 1 deletion web/src/utils/timelineUtil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export function getTimelineItemDescription(timelineItem: Timeline) {
}
}

export function getChunkedTimeRange(timestamp: number) {
export function getChunkedTimeDay(timestamp: number) {
const endOfThisHour = new Date();
endOfThisHour.setHours(endOfThisHour.getHours() + 1, 0, 0, 0);
const data: { start: number; end: number }[] = [];
Expand All @@ -147,3 +147,33 @@ export function getChunkedTimeRange(timestamp: number) {

return { start: startTimestamp, end, ranges: data };
}

export function getChunkedTimeRange(
startTimestamp: number,
endTimestamp: number,
) {
const endOfThisHour = new Date();
endOfThisHour.setHours(endOfThisHour.getHours() + 1, 0, 0, 0);
const data: { start: number; end: number }[] = [];
const startDay = new Date(startTimestamp * 1000);
startDay.setMinutes(0, 0, 0);
let start = startDay.getTime() / 1000;
let end = 0;

while (end < endTimestamp) {
startDay.setHours(startDay.getHours() + 1);

if (startDay > endOfThisHour || startDay.getTime() / 1000 > endTimestamp) {
break;
}

end = endOfHourOrCurrentTime(startDay.getTime() / 1000);
data.push({
start,
end,
});
start = startDay.getTime() / 1000;
}

return { start: startTimestamp, end: endTimestamp, ranges: data };
}
Loading