diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index d1d7186fe..7d7bfdcd0 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 "./PlayerControls"; const HLS_MIME_TYPE = "application/vnd.apple.mpegurl" as const; const unsupportedErrorCodes = [ @@ -221,6 +204,7 @@ export default function HlsVideoPlayer({ ); } - -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/PlayerControls.tsx b/web/src/components/player/PlayerControls.tsx new file mode 100644 index 000000000..7c929b83e --- /dev/null +++ b/web/src/components/player/PlayerControls.tsx @@ -0,0 +1,187 @@ +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; +}; +export default function VideoControls({ + className, + video, + features = CONTROLS_DEFAULT, + 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 ( +
+ {features.volume && ( +
+ { + e.stopPropagation(); + video.muted = !video.muted; + }} + /> + {video.muted == false && ( + (video.volume = value[0])} + /> + )} +
+ )} + {features.seek && ( + + )} +
+ {isPlaying ? ( + + ) : ( + + )} +
+ {features.seek && ( + + )} + {features.playbackRate && ( + { + setControlsOpen(open); + }} + > + {`${video.playbackRate}x`} + + (video.playbackRate = parseInt(rate))} + > + {playbackRates.map((rate) => ( + + {rate}x + + ))} + + + + )} +
+ ); +}