mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Motion playback (#10609)
* Move controls to separate component and make features configurable * Allow playback on motion screen * Simplify layout * Fix seeking * Fix playback * fix preview scrubbing * Fix player controls visibility * Use opacity for both dark and light mode
This commit is contained in:
parent
83517f59b4
commit
622dddd2c4
@ -3,31 +3,14 @@ import {
|
|||||||
ReactNode,
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import Hls from "hls.js";
|
import Hls from "hls.js";
|
||||||
import { isDesktop, isMobile, isSafari } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
import { LuPause, LuPlay } from "react-icons/lu";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "../ui/dropdown-menu";
|
|
||||||
import {
|
|
||||||
MdForward10,
|
|
||||||
MdReplay10,
|
|
||||||
MdVolumeDown,
|
|
||||||
MdVolumeMute,
|
|
||||||
MdVolumeOff,
|
|
||||||
MdVolumeUp,
|
|
||||||
} from "react-icons/md";
|
|
||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
import { Slider } from "../ui/slider-volume";
|
|
||||||
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||||
|
import VideoControls from "./VideoControls";
|
||||||
|
|
||||||
const HLS_MIME_TYPE = "application/vnd.apple.mpegurl" as const;
|
const HLS_MIME_TYPE = "application/vnd.apple.mpegurl" as const;
|
||||||
const unsupportedErrorCodes = [
|
const unsupportedErrorCodes = [
|
||||||
@ -39,6 +22,7 @@ type HlsVideoPlayerProps = {
|
|||||||
className: string;
|
className: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
videoRef: MutableRefObject<HTMLVideoElement | null>;
|
videoRef: MutableRefObject<HTMLVideoElement | null>;
|
||||||
|
visible: boolean;
|
||||||
currentSource: string;
|
currentSource: string;
|
||||||
onClipEnded?: () => void;
|
onClipEnded?: () => void;
|
||||||
onPlayerLoaded?: () => void;
|
onPlayerLoaded?: () => void;
|
||||||
@ -49,6 +33,7 @@ export default function HlsVideoPlayer({
|
|||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
videoRef,
|
videoRef,
|
||||||
|
visible,
|
||||||
currentSource,
|
currentSource,
|
||||||
onClipEnded,
|
onClipEnded,
|
||||||
onPlayerLoaded,
|
onPlayerLoaded,
|
||||||
@ -154,7 +139,7 @@ export default function HlsVideoPlayer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative`}
|
className={`relative ${visible ? "visible" : "hidden"}`}
|
||||||
onMouseOver={
|
onMouseOver={
|
||||||
isDesktop
|
isDesktop
|
||||||
? () => {
|
? () => {
|
||||||
@ -221,157 +206,34 @@ export default function HlsVideoPlayer({
|
|||||||
</TransformComponent>
|
</TransformComponent>
|
||||||
</TransformWrapper>
|
</TransformWrapper>
|
||||||
<VideoControls
|
<VideoControls
|
||||||
|
className="absolute bottom-5 left-1/2 -translate-x-1/2"
|
||||||
video={videoRef.current}
|
video={videoRef.current}
|
||||||
isPlaying={isPlaying}
|
isPlaying={isPlaying}
|
||||||
show={controls}
|
show={controls}
|
||||||
controlsOpen={controlsOpen}
|
controlsOpen={controlsOpen}
|
||||||
setControlsOpen={setControlsOpen}
|
setControlsOpen={setControlsOpen}
|
||||||
|
onPlayPause={(play) => {
|
||||||
|
if (!videoRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (play) {
|
||||||
|
videoRef.current.play();
|
||||||
|
} else {
|
||||||
|
videoRef.current.pause();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSeek={(diff) => {
|
||||||
|
const currentTime = videoRef.current?.currentTime;
|
||||||
|
|
||||||
|
if (!videoRef.current || !currentTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
videoRef.current.currentTime = Math.max(0, currentTime + diff);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type VideoControlsProps = {
|
|
||||||
video: HTMLVideoElement | null;
|
|
||||||
isPlaying: boolean;
|
|
||||||
show: boolean;
|
|
||||||
controlsOpen: boolean;
|
|
||||||
setControlsOpen: (open: boolean) => void;
|
|
||||||
};
|
|
||||||
function VideoControls({
|
|
||||||
video,
|
|
||||||
isPlaying,
|
|
||||||
show,
|
|
||||||
controlsOpen,
|
|
||||||
setControlsOpen,
|
|
||||||
}: VideoControlsProps) {
|
|
||||||
const playbackRates = useMemo(() => {
|
|
||||||
if (isSafari) {
|
|
||||||
return [0.5, 1, 2];
|
|
||||||
} else {
|
|
||||||
return [0.5, 1, 2, 4, 8, 16];
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onReplay = useCallback(
|
|
||||||
(e: React.MouseEvent<SVGElement>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const currentTime = video?.currentTime;
|
|
||||||
|
|
||||||
if (!video || !currentTime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
video.currentTime = Math.max(0, currentTime - 10);
|
|
||||||
},
|
|
||||||
[video],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSkip = useCallback(
|
|
||||||
(e: React.MouseEvent<SVGElement>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const currentTime = video?.currentTime;
|
|
||||||
|
|
||||||
if (!video || !currentTime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
video.currentTime = currentTime + 10;
|
|
||||||
},
|
|
||||||
[video],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onTogglePlay = useCallback(
|
|
||||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (!video) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlaying) {
|
|
||||||
video.pause();
|
|
||||||
} else {
|
|
||||||
video.play();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isPlaying, video],
|
|
||||||
);
|
|
||||||
|
|
||||||
// volume control
|
|
||||||
|
|
||||||
const VolumeIcon = useMemo(() => {
|
|
||||||
if (!video || video?.muted) {
|
|
||||||
return MdVolumeOff;
|
|
||||||
} else if (video.volume <= 0.33) {
|
|
||||||
return MdVolumeMute;
|
|
||||||
} else if (video.volume <= 0.67) {
|
|
||||||
return MdVolumeDown;
|
|
||||||
} else {
|
|
||||||
return MdVolumeUp;
|
|
||||||
}
|
|
||||||
// only update when specific fields change
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [video?.volume, video?.muted]);
|
|
||||||
|
|
||||||
if (!video || !show) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`absolute bottom-5 left-1/2 -translate-x-1/2 px-4 py-2 flex justify-between items-center gap-8 text-white z-50 bg-black bg-opacity-60 rounded-lg`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-normal items-center gap-2">
|
|
||||||
<VolumeIcon
|
|
||||||
className="size-5"
|
|
||||||
onClick={(e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
video.muted = !video.muted;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{video.muted == false && (
|
|
||||||
<Slider
|
|
||||||
className="w-20"
|
|
||||||
value={[video.volume]}
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.02}
|
|
||||||
onValueChange={(value) => (video.volume = value[0])}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<MdReplay10 className="size-5 cursor-pointer" onClick={onReplay} />
|
|
||||||
<div className="cursor-pointer" onClick={onTogglePlay}>
|
|
||||||
{isPlaying ? (
|
|
||||||
<LuPause className="size-5 fill-white" />
|
|
||||||
) : (
|
|
||||||
<LuPlay className="size-5 fill-white" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<MdForward10 className="size-5 cursor-pointer" onClick={onSkip} />
|
|
||||||
<DropdownMenu
|
|
||||||
open={controlsOpen}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
setControlsOpen(open);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DropdownMenuTrigger>{`${video.playbackRate}x`}</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
<DropdownMenuRadioGroup
|
|
||||||
onValueChange={(rate) => (video.playbackRate = parseInt(rate))}
|
|
||||||
>
|
|
||||||
{playbackRates.map((rate) => (
|
|
||||||
<DropdownMenuRadioItem key={rate} value={rate.toString()}>
|
|
||||||
{rate}x
|
|
||||||
</DropdownMenuRadioItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuRadioGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -353,7 +353,7 @@ function PreviewFramesPlayer({
|
|||||||
|
|
||||||
return previewFrames.map((frame) =>
|
return previewFrames.map((frame) =>
|
||||||
// @ts-expect-error we know this item will exist
|
// @ts-expect-error we know this item will exist
|
||||||
parseFloat(frame.split("-").slice(undefined, -5)),
|
parseFloat(frame.split("-").at(-1).slice(undefined, -5)),
|
||||||
);
|
);
|
||||||
}, [previewFrames]);
|
}, [previewFrames]);
|
||||||
|
|
||||||
|
@ -171,7 +171,7 @@ export default function PreviewThumbnailPlayer({
|
|||||||
{...swipeHandlers}
|
{...swipeHandlers}
|
||||||
>
|
>
|
||||||
{playingBack && (
|
{playingBack && (
|
||||||
<div className="absolute inset-0 animate-in fade-in pointer-events-none">
|
<div className="absolute inset-0 animate-in fade-in">
|
||||||
<PreviewContent
|
<PreviewContent
|
||||||
review={review}
|
review={review}
|
||||||
relevantPreview={relevantPreview}
|
relevantPreview={relevantPreview}
|
||||||
@ -486,7 +486,7 @@ function VideoPreview({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative size-full aspect-video bg-black pointer-events-none">
|
<div className="relative size-full aspect-video bg-black">
|
||||||
<video
|
<video
|
||||||
ref={playerRef}
|
ref={playerRef}
|
||||||
className="size-full aspect-video bg-black pointer-events-none"
|
className="size-full aspect-video bg-black pointer-events-none"
|
||||||
@ -500,7 +500,7 @@ function VideoPreview({
|
|||||||
</video>
|
</video>
|
||||||
<Slider
|
<Slider
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
className="absolute inset-x-0 bottom-0 z-30 pointer-events-none"
|
className="absolute inset-x-0 bottom-0 z-30"
|
||||||
value={[progress]}
|
value={[progress]}
|
||||||
onValueChange={onManualSeek}
|
onValueChange={onManualSeek}
|
||||||
onValueCommit={onStopManualSeek}
|
onValueCommit={onStopManualSeek}
|
||||||
@ -654,9 +654,9 @@ function InProgressPreview({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative size-full flex items-center bg-black pointer-events-none">
|
<div className="relative size-full flex items-center bg-black">
|
||||||
<img
|
<img
|
||||||
className="size-full object-contain"
|
className="size-full object-contain pointer-events-none"
|
||||||
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`}
|
src={`${apiHost}api/preview/${previewFrames[key]}/thumbnail.webp`}
|
||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
/>
|
/>
|
||||||
|
170
web/src/components/player/VideoControls.tsx
Normal file
170
web/src/components/player/VideoControls.tsx
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { isSafari } from "react-device-detect";
|
||||||
|
import { LuPause, LuPlay } from "react-icons/lu";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "../ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
MdForward10,
|
||||||
|
MdReplay10,
|
||||||
|
MdVolumeDown,
|
||||||
|
MdVolumeMute,
|
||||||
|
MdVolumeOff,
|
||||||
|
MdVolumeUp,
|
||||||
|
} from "react-icons/md";
|
||||||
|
import { Slider } from "../ui/slider-volume";
|
||||||
|
|
||||||
|
type VideoControls = {
|
||||||
|
volume?: boolean;
|
||||||
|
seek?: boolean;
|
||||||
|
playbackRate?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONTROLS_DEFAULT: VideoControls = {
|
||||||
|
volume: true,
|
||||||
|
seek: true,
|
||||||
|
playbackRate: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
type VideoControlsProps = {
|
||||||
|
className?: string;
|
||||||
|
video?: HTMLVideoElement | null;
|
||||||
|
features?: VideoControls;
|
||||||
|
isPlaying: boolean;
|
||||||
|
show: boolean;
|
||||||
|
controlsOpen?: boolean;
|
||||||
|
setControlsOpen?: (open: boolean) => void;
|
||||||
|
onPlayPause: (play: boolean) => void;
|
||||||
|
onSeek: (diff: number) => void;
|
||||||
|
};
|
||||||
|
export default function VideoControls({
|
||||||
|
className,
|
||||||
|
video,
|
||||||
|
features = CONTROLS_DEFAULT,
|
||||||
|
isPlaying,
|
||||||
|
show,
|
||||||
|
controlsOpen,
|
||||||
|
setControlsOpen,
|
||||||
|
onPlayPause,
|
||||||
|
onSeek,
|
||||||
|
}: VideoControlsProps) {
|
||||||
|
const playbackRates = useMemo(() => {
|
||||||
|
if (isSafari) {
|
||||||
|
return [0.5, 1, 2];
|
||||||
|
} else {
|
||||||
|
return [0.5, 1, 2, 4, 8, 16];
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onReplay = useCallback(
|
||||||
|
(e: React.MouseEvent<SVGElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onSeek(-10);
|
||||||
|
},
|
||||||
|
[onSeek],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSkip = useCallback(
|
||||||
|
(e: React.MouseEvent<SVGElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onSeek(10);
|
||||||
|
},
|
||||||
|
[onSeek],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onTogglePlay = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onPlayPause(!isPlaying);
|
||||||
|
},
|
||||||
|
[isPlaying, onPlayPause],
|
||||||
|
);
|
||||||
|
|
||||||
|
// volume control
|
||||||
|
|
||||||
|
const VolumeIcon = useMemo(() => {
|
||||||
|
if (!video || video?.muted) {
|
||||||
|
return MdVolumeOff;
|
||||||
|
} else if (video.volume <= 0.33) {
|
||||||
|
return MdVolumeMute;
|
||||||
|
} else if (video.volume <= 0.67) {
|
||||||
|
return MdVolumeDown;
|
||||||
|
} else {
|
||||||
|
return MdVolumeUp;
|
||||||
|
}
|
||||||
|
// only update when specific fields change
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [video?.volume, video?.muted]);
|
||||||
|
|
||||||
|
if (!show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`px-4 py-2 flex justify-between items-center gap-8 text-white z-50 bg-secondary-foreground/60 dark:bg-secondary/60 rounded-lg ${className ?? ""}`}
|
||||||
|
>
|
||||||
|
{video && features.volume && (
|
||||||
|
<div className="flex justify-normal items-center gap-2">
|
||||||
|
<VolumeIcon
|
||||||
|
className="size-5"
|
||||||
|
onClick={(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
video.muted = !video.muted;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{video.muted == false && (
|
||||||
|
<Slider
|
||||||
|
className="w-20"
|
||||||
|
value={[video.volume]}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.02}
|
||||||
|
onValueChange={(value) => (video.volume = value[0])}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{features.seek && (
|
||||||
|
<MdReplay10 className="size-5 cursor-pointer" onClick={onReplay} />
|
||||||
|
)}
|
||||||
|
<div className="cursor-pointer" onClick={onTogglePlay}>
|
||||||
|
{isPlaying ? (
|
||||||
|
<LuPause className="size-5 fill-white" />
|
||||||
|
) : (
|
||||||
|
<LuPlay className="size-5 fill-white" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{features.seek && (
|
||||||
|
<MdForward10 className="size-5 cursor-pointer" onClick={onSkip} />
|
||||||
|
)}
|
||||||
|
{video && features.playbackRate && (
|
||||||
|
<DropdownMenu
|
||||||
|
open={controlsOpen == true}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (setControlsOpen) {
|
||||||
|
setControlsOpen(open);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownMenuTrigger>{`${video.playbackRate}x`}</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuRadioGroup
|
||||||
|
onValueChange={(rate) => (video.playbackRate = parseFloat(rate))}
|
||||||
|
>
|
||||||
|
{playbackRates.map((rate) => (
|
||||||
|
<DropdownMenuRadioItem key={rate} value={rate.toString()}>
|
||||||
|
{rate}x
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -165,33 +165,30 @@ export default function DynamicVideoPlayer({
|
|||||||
}, [controller, recordings]);
|
}, [controller, recordings]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative ${className ?? ""} cursor-pointer`}>
|
<div className={`relative ${className ?? ""}`}>
|
||||||
<div
|
<HlsVideoPlayer
|
||||||
className={`w-full relative ${isScrubbing || isLoading ? "hidden" : "visible"}`}
|
className={`w-full ${grow ?? ""}`}
|
||||||
>
|
videoRef={playerRef}
|
||||||
<HlsVideoPlayer
|
visible={!(isScrubbing || isLoading)}
|
||||||
className={`${grow}`}
|
currentSource={source}
|
||||||
videoRef={playerRef}
|
onTimeUpdate={onTimeUpdate}
|
||||||
currentSource={source}
|
onPlayerLoaded={onPlayerLoaded}
|
||||||
onTimeUpdate={onTimeUpdate}
|
onClipEnded={onClipEnded}
|
||||||
onPlayerLoaded={onPlayerLoaded}
|
onPlaying={() => {
|
||||||
onClipEnded={onClipEnded}
|
if (isScrubbing) {
|
||||||
onPlaying={() => {
|
playerRef.current?.pause();
|
||||||
if (isScrubbing) {
|
}
|
||||||
playerRef.current?.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{config && focusedItem && (
|
{config && focusedItem && (
|
||||||
<TimelineEventOverlay
|
<TimelineEventOverlay
|
||||||
timeline={focusedItem}
|
timeline={focusedItem}
|
||||||
cameraConfig={config.cameras[camera]}
|
cameraConfig={config.cameras[camera]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</HlsVideoPlayer>
|
</HlsVideoPlayer>
|
||||||
</div>
|
|
||||||
<PreviewPlayer
|
<PreviewPlayer
|
||||||
className={`${isScrubbing || isLoading ? "visible" : "hidden"} ${grow}`}
|
className={`${isScrubbing || isLoading ? "visible" : "hidden"} ${grow}`}
|
||||||
camera={camera}
|
camera={camera}
|
||||||
|
@ -38,6 +38,7 @@ import PreviewPlayer, {
|
|||||||
} from "@/components/player/PreviewPlayer";
|
} from "@/components/player/PreviewPlayer";
|
||||||
import SummaryTimeline from "@/components/timeline/SummaryTimeline";
|
import SummaryTimeline from "@/components/timeline/SummaryTimeline";
|
||||||
import { RecordingStartingPoint } from "@/types/record";
|
import { RecordingStartingPoint } from "@/types/record";
|
||||||
|
import VideoControls from "@/components/player/VideoControls";
|
||||||
|
|
||||||
type EventViewProps = {
|
type EventViewProps = {
|
||||||
reviews?: ReviewSegment[];
|
reviews?: ReviewSegment[];
|
||||||
@ -678,6 +679,7 @@ function MotionReview({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [scrubbing, setScrubbing] = useState(false);
|
const [scrubbing, setScrubbing] = useState(false);
|
||||||
|
const [playing, setPlaying] = useState(false);
|
||||||
|
|
||||||
// move to next clip
|
// move to next clip
|
||||||
|
|
||||||
@ -704,6 +706,33 @@ function MotionReview({
|
|||||||
});
|
});
|
||||||
}, [currentTime, currentTimeRange, timeRangeSegments]);
|
}, [currentTime, currentTimeRange, timeRangeSegments]);
|
||||||
|
|
||||||
|
// playback
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!playing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = currentTime;
|
||||||
|
let counter = 0;
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
counter += 0.5;
|
||||||
|
|
||||||
|
if (startTime + counter >= timeRange.before) {
|
||||||
|
setPlaying(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentTime(startTime + counter);
|
||||||
|
}, 60);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
// do not render when current time changes
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [playing]);
|
||||||
|
|
||||||
if (!relevantPreviews) {
|
if (!relevantPreviews) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
@ -762,9 +791,40 @@ function MotionReview({
|
|||||||
motion_events={motionData ?? []}
|
motion_events={motionData ?? []}
|
||||||
severityType="significant_motion"
|
severityType="significant_motion"
|
||||||
contentRef={contentRef}
|
contentRef={contentRef}
|
||||||
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
|
onHandlebarDraggingChange={(scrubbing) => {
|
||||||
|
if (playing && scrubbing) {
|
||||||
|
setPlaying(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setScrubbing(scrubbing);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<VideoControls
|
||||||
|
className="absolute bottom-16 left-1/2 -translate-x-1/2"
|
||||||
|
features={{
|
||||||
|
volume: false,
|
||||||
|
seek: true,
|
||||||
|
playbackRate: false,
|
||||||
|
}}
|
||||||
|
isPlaying={playing}
|
||||||
|
onPlayPause={setPlaying}
|
||||||
|
onSeek={(diff) => {
|
||||||
|
const wasPlaying = playing;
|
||||||
|
|
||||||
|
if (wasPlaying) {
|
||||||
|
setPlaying(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentTime(currentTime + diff);
|
||||||
|
|
||||||
|
if (wasPlaying) {
|
||||||
|
setTimeout(() => setPlaying(true), 100);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
show={currentTime < timeRange.before - 4}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -147,7 +147,7 @@ export function RecordingView({
|
|||||||
} else {
|
} else {
|
||||||
updateSelectedSegment(currentTime, true);
|
updateSelectedSegment(currentTime, true);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (playerTime != currentTime) {
|
||||||
mainControllerRef.current?.play();
|
mainControllerRef.current?.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user