diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index 7d7bfdcd0..1a9b1d8b9 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -10,7 +10,7 @@ import Hls from "hls.js"; import { isDesktop, isMobile } from "react-device-detect"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; -import VideoControls from "./PlayerControls"; +import VideoControls from "./VideoControls"; const HLS_MIME_TYPE = "application/vnd.apple.mpegurl" as const; const unsupportedErrorCodes = [ @@ -210,6 +210,26 @@ export default function HlsVideoPlayer({ show={controls} controlsOpen={controlsOpen} 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} diff --git a/web/src/components/player/PlayerControls.tsx b/web/src/components/player/VideoControls.tsx similarity index 82% rename from web/src/components/player/PlayerControls.tsx rename to web/src/components/player/VideoControls.tsx index 7c929b83e..a37da2ca9 100644 --- a/web/src/components/player/PlayerControls.tsx +++ b/web/src/components/player/VideoControls.tsx @@ -32,12 +32,14 @@ const CONTROLS_DEFAULT: VideoControls = { type VideoControlsProps = { className?: string; - video: HTMLVideoElement | null; + video?: HTMLVideoElement | null; features?: VideoControls; isPlaying: boolean; show: boolean; - controlsOpen: boolean; - setControlsOpen: (open: boolean) => void; + controlsOpen?: boolean; + setControlsOpen?: (open: boolean) => void; + onPlayPause: (play: boolean) => void; + onSeek: (diff: number) => void; }; export default function VideoControls({ className, @@ -47,6 +49,8 @@ export default function VideoControls({ show, controlsOpen, setControlsOpen, + onPlayPause, + onSeek, }: VideoControlsProps) { const playbackRates = useMemo(() => { if (isSafari) { @@ -59,48 +63,25 @@ export default function VideoControls({ const onReplay = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - - const currentTime = video?.currentTime; - - if (!video || !currentTime) { - return; - } - - video.currentTime = Math.max(0, currentTime - 10); + onSeek(-10); }, - [video], + [onSeek], ); const onSkip = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - - const currentTime = video?.currentTime; - - if (!video || !currentTime) { - return; - } - - video.currentTime = currentTime + 10; + onSeek(10); }, - [video], + [onSeek], ); const onTogglePlay = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - - if (!video) { - return; - } - - if (isPlaying) { - video.pause(); - } else { - video.play(); - } + onPlayPause(!isPlaying); }, - [isPlaying, video], + [isPlaying, onPlayPause], ); // volume control @@ -119,7 +100,7 @@ export default function VideoControls({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [video?.volume, video?.muted]); - if (!video || !show) { + if (!show) { return; } @@ -127,7 +108,7 @@ export default function VideoControls({
- {features.volume && ( + {video && features.volume && (
)} - {features.playbackRate && ( + {video && features.playbackRate && ( { - setControlsOpen(open); + if (setControlsOpen) { + setControlsOpen(open); + } }} > {`${video.playbackRate}x`} (video.playbackRate = parseInt(rate))} + onValueChange={(rate) => (video.playbackRate = parseFloat(rate))} > {playbackRates.map((rate) => ( diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index 5c555f402..4d0ef7907 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -38,6 +38,7 @@ import PreviewPlayer, { } from "@/components/player/PreviewPlayer"; import SummaryTimeline from "@/components/timeline/SummaryTimeline"; import { RecordingStartingPoint } from "@/types/record"; +import VideoControls from "@/components/player/VideoControls"; type EventViewProps = { reviews?: ReviewSegment[]; @@ -678,6 +679,7 @@ function MotionReview({ ); const [scrubbing, setScrubbing] = useState(false); + const [playing, setPlaying] = useState(false); // move to next clip @@ -704,6 +706,33 @@ function MotionReview({ }); }, [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) { return ; } @@ -762,9 +791,30 @@ function MotionReview({ motion_events={motionData ?? []} severityType="significant_motion" contentRef={contentRef} - onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)} + onHandlebarDraggingChange={(scrubbing) => { + if (playing && scrubbing) { + setPlaying(false); + } + + setScrubbing(scrubbing); + }} />
+ + { + setCurrentTime(currentTime + diff); + }} + show={currentTime < timeRange.before - 4} + /> ); }