mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-08 20:25:26 +03:00
Support preview player that shows current hour images
This commit is contained in:
parent
98686af504
commit
0b8859c76c
@ -527,6 +527,7 @@ function InProgressPreview({
|
|||||||
`preview/${review.camera}/start/${Math.floor(review.start_time) - PREVIEW_PADDING}/end/${
|
`preview/${review.camera}/start/${Math.floor(review.start_time) - PREVIEW_PADDING}/end/${
|
||||||
Math.ceil(review.end_time) + PREVIEW_PADDING
|
Math.ceil(review.end_time) + PREVIEW_PADDING
|
||||||
}/frames`,
|
}/frames`,
|
||||||
|
{ revalidateOnFocus: false },
|
||||||
);
|
);
|
||||||
const [manualFrame, setManualFrame] = useState(false);
|
const [manualFrame, setManualFrame] = useState(false);
|
||||||
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout>();
|
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout>();
|
||||||
@ -642,7 +643,7 @@ function InProgressPreview({
|
|||||||
<div className="relative size-full flex items-center bg-black">
|
<div className="relative size-full flex items-center bg-black">
|
||||||
<img
|
<img
|
||||||
className="size-full object-contain"
|
className="size-full object-contain"
|
||||||
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.jpg`}
|
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`}
|
||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
/>
|
/>
|
||||||
<Slider
|
<Slider
|
||||||
|
|||||||
@ -1,230 +0,0 @@
|
|||||||
import {
|
|
||||||
MutableRefObject,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
|
||||||
import { Preview } from "@/types/preview";
|
|
||||||
import { PreviewPlayback } from "@/types/playback";
|
|
||||||
|
|
||||||
type PreviewVideoPlayerProps = {
|
|
||||||
className?: string;
|
|
||||||
camera: string;
|
|
||||||
timeRange: { start: number; end: number };
|
|
||||||
cameraPreviews: Preview[];
|
|
||||||
startTime?: number;
|
|
||||||
onControllerReady: (controller: PreviewVideoController) => void;
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
|
||||||
export default function PreviewVideoPlayer({
|
|
||||||
className,
|
|
||||||
camera,
|
|
||||||
timeRange,
|
|
||||||
cameraPreviews,
|
|
||||||
startTime,
|
|
||||||
onControllerReady,
|
|
||||||
onClick,
|
|
||||||
}: PreviewVideoPlayerProps) {
|
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
|
||||||
|
|
||||||
// controlling playback
|
|
||||||
|
|
||||||
const previewRef = useRef<HTMLVideoElement | null>(null);
|
|
||||||
const controller = useMemo(() => {
|
|
||||||
if (!config || !previewRef.current) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new PreviewVideoController(camera, previewRef);
|
|
||||||
// we only care when preview is ready
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [camera, config, previewRef.current]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!controller) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controller) {
|
|
||||||
onControllerReady(controller);
|
|
||||||
}
|
|
||||||
// we only want to fire once when players are ready
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [controller]);
|
|
||||||
|
|
||||||
// initial state
|
|
||||||
|
|
||||||
const initialPreview = useMemo(() => {
|
|
||||||
return cameraPreviews.find(
|
|
||||||
(preview) =>
|
|
||||||
preview.camera == camera &&
|
|
||||||
Math.round(preview.start) >= timeRange.start &&
|
|
||||||
Math.floor(preview.end) <= timeRange.end,
|
|
||||||
);
|
|
||||||
|
|
||||||
// we only want to calculate this once
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [currentPreview, setCurrentPreview] = useState(initialPreview);
|
|
||||||
|
|
||||||
const onPreviewSeeked = useCallback(() => {
|
|
||||||
if (!controller) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.finishedSeeking();
|
|
||||||
}, [controller]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!controller) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const preview = cameraPreviews.find(
|
|
||||||
(preview) =>
|
|
||||||
preview.camera == camera &&
|
|
||||||
Math.round(preview.start) >= timeRange.start &&
|
|
||||||
Math.floor(preview.end) <= timeRange.end,
|
|
||||||
);
|
|
||||||
setCurrentPreview(preview);
|
|
||||||
|
|
||||||
controller.newPlayback({
|
|
||||||
preview,
|
|
||||||
timeRange,
|
|
||||||
});
|
|
||||||
|
|
||||||
// we only want this to change when recordings update
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [controller, timeRange]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!currentPreview || !previewRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
previewRef.current.load();
|
|
||||||
}, [currentPreview, previewRef]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`relative w-full ${className ?? ""} ${onClick ? "cursor-pointer" : ""}`}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<video
|
|
||||||
ref={previewRef}
|
|
||||||
className={`size-full rounded-2xl bg-black`}
|
|
||||||
preload="auto"
|
|
||||||
autoPlay
|
|
||||||
playsInline
|
|
||||||
muted
|
|
||||||
disableRemotePlayback
|
|
||||||
onSeeked={onPreviewSeeked}
|
|
||||||
onLoadedData={() => {
|
|
||||||
if (controller) {
|
|
||||||
controller.previewReady();
|
|
||||||
} else {
|
|
||||||
previewRef.current?.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previewRef.current && startTime && currentPreview) {
|
|
||||||
previewRef.current.currentTime = startTime - currentPreview.start;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{currentPreview != undefined && (
|
|
||||||
<source src={currentPreview.src} type={currentPreview.type} />
|
|
||||||
)}
|
|
||||||
</video>
|
|
||||||
{cameraPreviews && !currentPreview && (
|
|
||||||
<div className="absolute inset-x-0 top-1/2 -y-translate-1/2 bg-black text-white rounded-2xl align-center text-center">
|
|
||||||
No Preview Found
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PreviewVideoController {
|
|
||||||
// main state
|
|
||||||
public camera = "";
|
|
||||||
private previewRef: MutableRefObject<HTMLVideoElement | null>;
|
|
||||||
private timeRange: { start: number; end: number } | undefined = undefined;
|
|
||||||
|
|
||||||
// preview
|
|
||||||
private preview: Preview | undefined = undefined;
|
|
||||||
private timeToSeek: number | undefined = undefined;
|
|
||||||
private seeking = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
camera: string,
|
|
||||||
previewRef: MutableRefObject<HTMLVideoElement | null>,
|
|
||||||
) {
|
|
||||||
this.camera = camera;
|
|
||||||
this.previewRef = previewRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
newPlayback(newPlayback: PreviewPlayback) {
|
|
||||||
this.preview = newPlayback.preview;
|
|
||||||
this.seeking = false;
|
|
||||||
|
|
||||||
this.timeRange = newPlayback.timeRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
scrubToTimestamp(time: number): boolean {
|
|
||||||
if (!this.preview || !this.timeRange) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (time < this.preview.start || time > this.preview.end) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.seeking) {
|
|
||||||
this.timeToSeek = time;
|
|
||||||
} else {
|
|
||||||
if (this.previewRef.current) {
|
|
||||||
this.previewRef.current.currentTime = Math.max(
|
|
||||||
0,
|
|
||||||
time - this.preview.start,
|
|
||||||
);
|
|
||||||
this.seeking = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
setNewPreviewStartTime(time: number) {
|
|
||||||
this.timeToSeek = time;
|
|
||||||
}
|
|
||||||
|
|
||||||
finishedSeeking() {
|
|
||||||
if (!this.previewRef.current || !this.preview) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.timeToSeek &&
|
|
||||||
this.timeToSeek != this.previewRef.current?.currentTime
|
|
||||||
) {
|
|
||||||
this.previewRef.current.currentTime =
|
|
||||||
this.timeToSeek - this.preview.start;
|
|
||||||
} else {
|
|
||||||
this.seeking = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
previewReady() {
|
|
||||||
this.seeking = false;
|
|
||||||
this.previewRef.current?.pause();
|
|
||||||
|
|
||||||
if (this.timeToSeek) {
|
|
||||||
this.finishedSeeking();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -163,7 +163,7 @@ export function getChunkedTimeRange(
|
|||||||
while (end < endTimestamp) {
|
while (end < endTimestamp) {
|
||||||
startDay.setHours(startDay.getHours() + 1);
|
startDay.setHours(startDay.getHours() + 1);
|
||||||
|
|
||||||
if (startDay > endOfThisHour || startDay.getTime() / 1000 > endTimestamp) {
|
if (startDay > endOfThisHour) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user