From 622dddd2c4dbbb0071f9e8e96213ac76bf160c16 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 22 Mar 2024 10:56:53 -0600 Subject: [PATCH] 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 --- web/src/components/player/HlsVideoPlayer.tsx | 190 +++--------------- web/src/components/player/PreviewPlayer.tsx | 2 +- .../player/PreviewThumbnailPlayer.tsx | 10 +- web/src/components/player/VideoControls.tsx | 170 ++++++++++++++++ .../player/dynamic/DynamicVideoPlayer.tsx | 49 +++-- web/src/views/events/EventView.tsx | 62 +++++- web/src/views/events/RecordingView.tsx | 2 +- 7 files changed, 287 insertions(+), 198 deletions(-) create mode 100644 web/src/components/player/VideoControls.tsx diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index d1d7186fe..f8cb3b34c 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -3,31 +3,14 @@ import { ReactNode, useCallback, useEffect, - useMemo, useRef, useState, } from "react"; import Hls from "hls.js"; -import { isDesktop, isMobile, 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 { isDesktop, isMobile } from "react-device-detect"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; -import { Slider } from "../ui/slider-volume"; import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; +import VideoControls from "./VideoControls"; const HLS_MIME_TYPE = "application/vnd.apple.mpegurl" as const; const unsupportedErrorCodes = [ @@ -39,6 +22,7 @@ type HlsVideoPlayerProps = { className: string; children?: ReactNode; videoRef: MutableRefObject; + visible: boolean; currentSource: string; onClipEnded?: () => void; onPlayerLoaded?: () => void; @@ -49,6 +33,7 @@ export default function HlsVideoPlayer({ className, children, videoRef, + visible, currentSource, onClipEnded, onPlayerLoaded, @@ -154,7 +139,7 @@ export default function HlsVideoPlayer({ return (
{ @@ -221,157 +206,34 @@ export default function HlsVideoPlayer({ { + 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}
); } - -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) => { - e.stopPropagation(); - - const currentTime = video?.currentTime; - - if (!video || !currentTime) { - return; - } - - video.currentTime = Math.max(0, currentTime - 10); - }, - [video], - ); - - const onSkip = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - - const currentTime = video?.currentTime; - - if (!video || !currentTime) { - return; - } - - video.currentTime = currentTime + 10; - }, - [video], - ); - - const onTogglePlay = useCallback( - (e: React.MouseEvent) => { - 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 ( -
-
- { - e.stopPropagation(); - video.muted = !video.muted; - }} - /> - {video.muted == false && ( - (video.volume = value[0])} - /> - )} -
- -
- {isPlaying ? ( - - ) : ( - - )} -
- - { - setControlsOpen(open); - }} - > - {`${video.playbackRate}x`} - - (video.playbackRate = parseInt(rate))} - > - {playbackRates.map((rate) => ( - - {rate}x - - ))} - - - -
- ); -} diff --git a/web/src/components/player/PreviewPlayer.tsx b/web/src/components/player/PreviewPlayer.tsx index 212779f3a..6c4a5a479 100644 --- a/web/src/components/player/PreviewPlayer.tsx +++ b/web/src/components/player/PreviewPlayer.tsx @@ -353,7 +353,7 @@ function PreviewFramesPlayer({ return previewFrames.map((frame) => // @ts-expect-error we know this item will exist - parseFloat(frame.split("-").slice(undefined, -5)), + parseFloat(frame.split("-").at(-1).slice(undefined, -5)), ); }, [previewFrames]); diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index c57e1041c..3c1648e9d 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -171,7 +171,7 @@ export default function PreviewThumbnailPlayer({ {...swipeHandlers} > {playingBack && ( -
+
+