mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-27 08:54:42 +03:00
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
* Bump the radix group in /web with 22 updates Bumps the radix group in /web with 22 updates: | Package | From | To | | --- | --- | --- | | [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.24` | `1.2.1` | | [@radix-ui/react-aspect-ratio](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/aspect-ratio) | `1.1.16` | `1.2.0` | | [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.12` | `1.4.1` | | [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/collapsible) | `1.1.21` | `1.2.1` | | [@radix-ui/react-context-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/context-menu) | `2.3.8` | `2.4.1` | | [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.2.0` | `1.3.1` | | [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.25` | `2.2.1` | | [@radix-ui/react-hover-card](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/hover-card) | `1.1.24` | `1.2.1` | | [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.16` | `2.1.17` | | [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.2.0` | `1.3.1` | | [@radix-ui/react-progress](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/progress) | `1.1.17` | `1.2.1` | | [@radix-ui/react-radio-group](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radio-group) | `1.4.8` | `1.5.1` | | [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.3.0` | `1.4.0` | | [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.8` | `2.4.1` | | [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.16` | `1.2.0` | | [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.5.0` | `1.6.1` | | [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.4.0` | `1.5.0` | | [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.8` | `1.4.1` | | [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.22` | `1.2.1` | | [@radix-ui/react-toggle](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toggle) | `1.1.19` | `1.2.1` | | [@radix-ui/react-toggle-group](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toggle-group) | `1.1.20` | `1.2.1` | | [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.3.0` | `1.4.1` | Updates `@radix-ui/react-alert-dialog` from 1.1.24 to 1.2.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog) Updates `@radix-ui/react-aspect-ratio` from 1.1.16 to 1.2.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/aspect-ratio/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/aspect-ratio) Updates `@radix-ui/react-checkbox` from 1.3.12 to 1.4.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox) Updates `@radix-ui/react-collapsible` from 1.1.21 to 1.2.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/collapsible/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/collapsible) Updates `@radix-ui/react-context-menu` from 2.3.8 to 2.4.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/context-menu/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/context-menu) Updates `@radix-ui/react-dialog` from 1.2.0 to 1.3.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog) Updates `@radix-ui/react-dropdown-menu` from 2.1.25 to 2.2.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu) Updates `@radix-ui/react-hover-card` from 1.1.24 to 1.2.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/hover-card/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/hover-card) Updates `@radix-ui/react-label` from 2.1.16 to 2.1.17 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label) Updates `@radix-ui/react-popover` from 1.2.0 to 1.3.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover) Updates `@radix-ui/react-progress` from 1.1.17 to 1.2.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/progress/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/progress) Updates `@radix-ui/react-radio-group` from 1.4.8 to 1.5.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radio-group/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/radio-group) Updates `@radix-ui/react-scroll-area` from 1.3.0 to 1.4.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area) Updates `@radix-ui/react-select` from 2.3.8 to 2.4.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select) Updates `@radix-ui/react-separator` from 1.1.16 to 1.2.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator) Updates `@radix-ui/react-slider` from 1.5.0 to 1.6.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider) Updates `@radix-ui/react-slot` from 1.4.0 to 1.5.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot) Updates `@radix-ui/react-switch` from 1.3.8 to 1.4.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch) Updates `@radix-ui/react-tabs` from 1.1.22 to 1.2.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs) Updates `@radix-ui/react-toggle` from 1.1.19 to 1.2.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toggle/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toggle) Updates `@radix-ui/react-toggle-group` from 1.1.20 to 1.2.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toggle-group/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toggle-group) Updates `@radix-ui/react-tooltip` from 1.3.0 to 1.4.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip) --- updated-dependencies: - dependency-name: "@radix-ui/react-alert-dialog" dependency-version: 1.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-aspect-ratio" dependency-version: 1.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-checkbox" dependency-version: 1.4.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-collapsible" dependency-version: 1.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-context-menu" dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-dialog" dependency-version: 1.3.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-dropdown-menu" dependency-version: 2.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-hover-card" dependency-version: 1.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-label" dependency-version: 2.1.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: radix - dependency-name: "@radix-ui/react-popover" dependency-version: 1.3.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-progress" dependency-version: 1.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-radio-group" dependency-version: 1.5.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-scroll-area" dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-select" dependency-version: 2.4.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-separator" dependency-version: 1.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-slider" dependency-version: 1.6.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-slot" dependency-version: 1.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-switch" dependency-version: 1.4.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-tabs" dependency-version: 1.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-toggle" dependency-version: 1.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-toggle-group" dependency-version: 1.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix - dependency-name: "@radix-ui/react-tooltip" dependency-version: 1.4.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: radix ... Signed-off-by: dependabot[bot] <support@github.com> * fix radix bump type error and drop popper override Slider 1.6 types its root as a span, so the progress hover handlers now take HTMLElement. The react-popper 1.2.8 override pinned an older popper that nested its own copies of primitive, context and the use-* hooks; without it every radix package resolves to a single copy, and compose-refs stays on the patched 1.1.2 through its own override. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
486 lines
12 KiB
TypeScript
486 lines
12 KiB
TypeScript
import React, {
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { useApiHost } from "@/api";
|
|
import useSWR from "swr";
|
|
import { isFirefox, isMobile, isSafari } from "react-device-detect";
|
|
import { TimelineScrubMode, TimeRange } from "@/types/timeline";
|
|
import { NoThumbSlider } from "../ui/slider";
|
|
import { PREVIEW_FPS, PREVIEW_PADDING, Preview } from "@/types/preview";
|
|
import { baseUrl } from "@/api/baseUrl";
|
|
|
|
type VideoPreviewProps = {
|
|
relevantPreview: Preview;
|
|
startTime: number;
|
|
endTime?: number;
|
|
showProgress?: boolean;
|
|
loop?: boolean;
|
|
setReviewed: () => void;
|
|
setIgnoreClick: (ignore: boolean) => void;
|
|
isPlayingBack: (ended: boolean) => void;
|
|
onTimeUpdate?: (time: number | undefined) => void;
|
|
windowVisible: boolean;
|
|
};
|
|
export function VideoPreview({
|
|
relevantPreview,
|
|
startTime,
|
|
endTime,
|
|
showProgress = true,
|
|
loop = false,
|
|
setReviewed,
|
|
setIgnoreClick,
|
|
isPlayingBack,
|
|
onTimeUpdate,
|
|
windowVisible,
|
|
}: VideoPreviewProps) {
|
|
const playerRef = useRef<HTMLVideoElement | null>(null);
|
|
const sliderRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
// keep track of playback state
|
|
|
|
const [progress, setProgress] = useState(0);
|
|
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout>();
|
|
const playerStartTime = useMemo(() => {
|
|
if (!relevantPreview) {
|
|
return 0;
|
|
}
|
|
|
|
// start with a bit of padding
|
|
return Math.max(0, startTime - relevantPreview.start - PREVIEW_PADDING);
|
|
|
|
// we know that these deps are correct
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
const playerDuration = useMemo(
|
|
() => (endTime ?? relevantPreview.end) - startTime + PREVIEW_PADDING,
|
|
// we know that these deps are correct
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[],
|
|
);
|
|
const [lastPercent, setLastPercent] = useState(0.0);
|
|
|
|
// initialize player correctly
|
|
|
|
useEffect(() => {
|
|
if (!playerRef.current) {
|
|
return;
|
|
}
|
|
|
|
if (isSafari || (isFirefox && isMobile)) {
|
|
playerRef.current.pause();
|
|
setPlaybackMode("compat");
|
|
} else {
|
|
playerRef.current.currentTime = playerStartTime;
|
|
playerRef.current.playbackRate = PREVIEW_FPS;
|
|
}
|
|
|
|
// we know that these deps are correct
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [playerRef]);
|
|
|
|
// time progress update
|
|
|
|
const onProgress = useCallback(() => {
|
|
if (!windowVisible) {
|
|
return;
|
|
}
|
|
|
|
if (onTimeUpdate) {
|
|
onTimeUpdate(
|
|
relevantPreview.start + (playerRef.current?.currentTime || 0),
|
|
);
|
|
}
|
|
|
|
const playerProgress =
|
|
(playerRef.current?.currentTime || 0) - playerStartTime;
|
|
|
|
// end with a bit of padding
|
|
const playerPercent = (playerProgress / playerDuration) * 100;
|
|
|
|
if (setReviewed && lastPercent < 50 && playerPercent > 50) {
|
|
setReviewed();
|
|
}
|
|
|
|
setLastPercent(playerPercent);
|
|
|
|
if (playerPercent > 100) {
|
|
setReviewed();
|
|
|
|
if (loop && playerRef.current) {
|
|
if (playbackMode != "auto") {
|
|
setPlaybackMode("auto");
|
|
setTimeout(() => setPlaybackMode("compat"), 100);
|
|
}
|
|
|
|
playerRef.current.currentTime = playerStartTime;
|
|
return;
|
|
}
|
|
|
|
if (isMobile) {
|
|
isPlayingBack(false);
|
|
|
|
if (onTimeUpdate) {
|
|
onTimeUpdate(undefined);
|
|
}
|
|
} else {
|
|
playerRef.current?.pause();
|
|
}
|
|
|
|
setPlaybackMode("auto");
|
|
setProgress(100.0);
|
|
} else {
|
|
setProgress(playerPercent);
|
|
}
|
|
|
|
// we know that these deps are correct
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [setProgress, lastPercent, windowVisible]);
|
|
|
|
// manual playback
|
|
// safari is incapable of playing at a speed > 2x
|
|
// so manual seeking is required on iOS
|
|
|
|
const [playbackMode, setPlaybackMode] = useState<TimelineScrubMode>("auto");
|
|
|
|
useEffect(() => {
|
|
if (playbackMode != "compat" || !playerRef.current) {
|
|
return;
|
|
}
|
|
|
|
let counter = 0;
|
|
const intervalId: NodeJS.Timeout = setInterval(() => {
|
|
if (playerRef.current) {
|
|
playerRef.current.currentTime = playerStartTime + counter;
|
|
counter += 1;
|
|
}
|
|
}, 1000 / PREVIEW_FPS);
|
|
return () => clearInterval(intervalId);
|
|
|
|
// we know that these deps are correct
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [playbackMode, playerRef]);
|
|
|
|
// user interaction
|
|
|
|
useEffect(() => {
|
|
setIgnoreClick(playbackMode != "auto" && playbackMode != "compat");
|
|
}, [playbackMode, setIgnoreClick]);
|
|
|
|
const onManualSeek = useCallback(
|
|
(values: number[]) => {
|
|
const value = values[0];
|
|
|
|
if (!playerRef.current) {
|
|
return;
|
|
}
|
|
|
|
if (playerRef.current.paused == false) {
|
|
playerRef.current.pause();
|
|
}
|
|
|
|
if (setReviewed) {
|
|
setReviewed();
|
|
}
|
|
|
|
setProgress(value);
|
|
playerRef.current.currentTime =
|
|
playerStartTime + (value / 100.0) * playerDuration;
|
|
},
|
|
|
|
// we know that these deps are correct
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[playerDuration, playerRef, playerStartTime, setIgnoreClick],
|
|
);
|
|
|
|
const onStopManualSeek = useCallback(() => {
|
|
setTimeout(() => {
|
|
setHoverTimeout(undefined);
|
|
|
|
if (isSafari || (isFirefox && isMobile)) {
|
|
setPlaybackMode("compat");
|
|
} else {
|
|
setPlaybackMode("auto");
|
|
playerRef.current?.play();
|
|
}
|
|
}, 500);
|
|
}, [playerRef]);
|
|
|
|
const onProgressHover = useCallback(
|
|
(event: React.MouseEvent<HTMLElement>) => {
|
|
if (!sliderRef.current) {
|
|
return;
|
|
}
|
|
|
|
const rect = sliderRef.current.getBoundingClientRect();
|
|
const positionX = event.clientX - rect.left;
|
|
const width = sliderRef.current.clientWidth;
|
|
onManualSeek([Math.round((positionX / width) * 100)]);
|
|
|
|
if (hoverTimeout) {
|
|
clearTimeout(hoverTimeout);
|
|
}
|
|
},
|
|
[sliderRef, hoverTimeout, onManualSeek],
|
|
);
|
|
|
|
return (
|
|
<div className="relative aspect-video size-full bg-black">
|
|
<video
|
|
ref={playerRef}
|
|
className="pointer-events-none aspect-video size-full bg-black"
|
|
autoPlay
|
|
playsInline
|
|
preload="auto"
|
|
muted
|
|
onTimeUpdate={onProgress}
|
|
>
|
|
<source
|
|
src={`${baseUrl}${relevantPreview.src.substring(1)}`}
|
|
type={relevantPreview.type}
|
|
/>
|
|
</video>
|
|
{showProgress && (
|
|
<NoThumbSlider
|
|
ref={sliderRef}
|
|
className={`absolute inset-x-0 bottom-0 z-30 cursor-col-resize ${playbackMode == "hover" || playbackMode == "drag" ? "h-4" : "h-2"}`}
|
|
value={[progress]}
|
|
onValueChange={(event) => {
|
|
setPlaybackMode("drag");
|
|
onManualSeek(event);
|
|
}}
|
|
onValueCommit={onStopManualSeek}
|
|
min={0}
|
|
step={1}
|
|
max={100}
|
|
onMouseMove={
|
|
isMobile
|
|
? undefined
|
|
: (event) => {
|
|
if (playbackMode != "drag") {
|
|
setPlaybackMode("hover");
|
|
onProgressHover(event);
|
|
}
|
|
}
|
|
}
|
|
onMouseLeave={
|
|
isMobile
|
|
? undefined
|
|
: () => {
|
|
if (!sliderRef.current) {
|
|
return;
|
|
}
|
|
|
|
setHoverTimeout(setTimeout(() => onStopManualSeek(), 500));
|
|
}
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const MIN_LOAD_TIMEOUT_MS = 200;
|
|
type InProgressPreviewProps = {
|
|
camera: string;
|
|
startTime: number;
|
|
endTime?: number;
|
|
timeRange: TimeRange;
|
|
showProgress?: boolean;
|
|
loop?: boolean;
|
|
defaultImageUrl?: string;
|
|
setReviewed: () => void;
|
|
setIgnoreClick: (ignore: boolean) => void;
|
|
isPlayingBack: (ended: boolean) => void;
|
|
onTimeUpdate?: (time: number | undefined) => void;
|
|
windowVisible: boolean;
|
|
};
|
|
export function InProgressPreview({
|
|
camera,
|
|
startTime,
|
|
endTime,
|
|
timeRange,
|
|
showProgress = true,
|
|
loop = false,
|
|
defaultImageUrl,
|
|
setReviewed,
|
|
setIgnoreClick,
|
|
isPlayingBack,
|
|
onTimeUpdate,
|
|
windowVisible,
|
|
}: InProgressPreviewProps) {
|
|
const apiHost = useApiHost();
|
|
const sliderRef = useRef<HTMLDivElement | null>(null);
|
|
const { data: previewFrames } = useSWR<string[]>(
|
|
`preview/${camera}/start/${Math.floor(startTime) - PREVIEW_PADDING}/end/${Math.ceil(
|
|
endTime ?? timeRange.before,
|
|
)}/frames`,
|
|
{ revalidateOnFocus: false },
|
|
);
|
|
|
|
const [playbackMode, setPlaybackMode] = useState<TimelineScrubMode>("auto");
|
|
const [hoverTimeout, setHoverTimeout] = useState<NodeJS.Timeout>();
|
|
const [key, setKey] = useState(0);
|
|
|
|
const handleLoad = useCallback(() => {
|
|
if (!previewFrames || !windowVisible) {
|
|
return;
|
|
}
|
|
|
|
if (onTimeUpdate) {
|
|
onTimeUpdate(startTime - PREVIEW_PADDING + key);
|
|
}
|
|
|
|
if (playbackMode != "auto") {
|
|
return;
|
|
}
|
|
|
|
if (key == previewFrames.length - 1) {
|
|
setReviewed();
|
|
|
|
if (loop) {
|
|
setKey(0);
|
|
return;
|
|
}
|
|
|
|
if (isMobile) {
|
|
isPlayingBack(false);
|
|
|
|
if (onTimeUpdate) {
|
|
onTimeUpdate(undefined);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
setTimeout(() => {
|
|
if (setReviewed && key == Math.floor(previewFrames.length / 2)) {
|
|
setReviewed();
|
|
}
|
|
|
|
if (previewFrames[key + 1]) {
|
|
setKey(key + 1);
|
|
}
|
|
}, MIN_LOAD_TIMEOUT_MS);
|
|
|
|
// we know that these deps are correct
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [key, playbackMode, previewFrames]);
|
|
|
|
// user interaction
|
|
|
|
useEffect(() => {
|
|
setIgnoreClick(playbackMode != "auto");
|
|
}, [playbackMode, setIgnoreClick]);
|
|
|
|
const onManualSeek = useCallback(
|
|
(values: number[]) => {
|
|
const value = values[0];
|
|
setReviewed();
|
|
setKey(value);
|
|
},
|
|
|
|
// we know that these deps are correct
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
[setIgnoreClick, setKey],
|
|
);
|
|
|
|
const onStopManualSeek = useCallback(
|
|
(values: number[]) => {
|
|
const value = values[0];
|
|
setTimeout(() => {
|
|
setPlaybackMode("auto");
|
|
setKey(value - 1);
|
|
}, 500);
|
|
},
|
|
[setPlaybackMode],
|
|
);
|
|
|
|
const onProgressHover = useCallback(
|
|
(event: React.MouseEvent<HTMLElement>) => {
|
|
if (!sliderRef.current || !previewFrames) {
|
|
return;
|
|
}
|
|
|
|
const rect = sliderRef.current.getBoundingClientRect();
|
|
const positionX = event.clientX - rect.left;
|
|
const width = sliderRef.current.clientWidth;
|
|
const progress = [Math.round((positionX / width) * previewFrames.length)];
|
|
onManualSeek(progress);
|
|
|
|
if (hoverTimeout) {
|
|
clearTimeout(hoverTimeout);
|
|
}
|
|
},
|
|
[sliderRef, hoverTimeout, previewFrames, onManualSeek],
|
|
);
|
|
|
|
if (!previewFrames || previewFrames.length == 0) {
|
|
return (
|
|
<img
|
|
className="size-full"
|
|
src={defaultImageUrl} //{`${apiHost}${review.thumb_path.replace("/media/frigate/", "")}`}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="relative flex size-full items-center bg-black">
|
|
<img
|
|
className="pointer-events-none size-full object-contain"
|
|
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`}
|
|
onLoad={handleLoad}
|
|
/>
|
|
{showProgress && (
|
|
<NoThumbSlider
|
|
ref={sliderRef}
|
|
className={`absolute inset-x-0 bottom-0 z-30 cursor-col-resize ${playbackMode != "auto" ? "h-4" : "h-2"}`}
|
|
value={[key]}
|
|
onValueChange={(event) => {
|
|
setPlaybackMode("drag");
|
|
onManualSeek(event);
|
|
}}
|
|
onValueCommit={onStopManualSeek}
|
|
min={0}
|
|
step={1}
|
|
max={previewFrames.length - 1}
|
|
onMouseMove={
|
|
isMobile
|
|
? undefined
|
|
: (event) => {
|
|
if (playbackMode != "drag") {
|
|
setPlaybackMode("hover");
|
|
onProgressHover(event);
|
|
}
|
|
}
|
|
}
|
|
onMouseLeave={
|
|
isMobile
|
|
? undefined
|
|
: (event) => {
|
|
if (!sliderRef.current || !previewFrames) {
|
|
return;
|
|
}
|
|
|
|
const rect = sliderRef.current.getBoundingClientRect();
|
|
const positionX = event.clientX - rect.left;
|
|
const width = sliderRef.current.clientWidth;
|
|
const progress = [
|
|
Math.round((positionX / width) * previewFrames.length),
|
|
];
|
|
|
|
setHoverTimeout(
|
|
setTimeout(() => onStopManualSeek(progress), 500),
|
|
);
|
|
}
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|