import { useCallback, useMemo, useRef, useState } from "react"; import { isDesktop, isMobileOnly, 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 useKeyboardListener, { KeyModifiers, } from "@/hooks/use-keyboard-listener"; import { VolumeSlider } from "../ui/slider"; import FrigatePlusIcon from "../icons/FrigatePlusIcon"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "../ui/alert-dialog"; import { cn } from "@/lib/utils"; import { FaCompress, FaExpand } from "react-icons/fa"; import { useTranslation } from "react-i18next"; type VideoControls = { volume?: boolean; seek?: boolean; playbackRate?: boolean; plusUpload?: boolean; fullscreen?: boolean; }; const CONTROLS_DEFAULT: VideoControls = { volume: true, seek: true, playbackRate: true, plusUpload: false, fullscreen: false, }; const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16]; const MIN_ITEMS_WRAP = 6; type VideoControlsProps = { className?: string; video?: HTMLVideoElement | null; features?: VideoControls; isPlaying: boolean; show: boolean; muted?: boolean; volume?: number; playbackRates?: number[]; playbackRate: number; hotKeys?: boolean; fullscreen?: boolean; setControlsOpen?: (open: boolean) => void; setMuted?: (muted: boolean) => void; onPlayPause: (play: boolean) => void; onSeek: (diff: number) => void; onSetPlaybackRate: (rate: number) => void; onUploadFrame?: () => void; toggleFullscreen?: () => void; containerRef?: React.MutableRefObject; }; export default function VideoControls({ className, video, features = CONTROLS_DEFAULT, isPlaying, show, muted, volume, playbackRates = PLAYBACK_RATE_DEFAULT, playbackRate, hotKeys = true, fullscreen, setControlsOpen, setMuted, onPlayPause, onSeek, onSetPlaybackRate, onUploadFrame, toggleFullscreen, containerRef, }: VideoControlsProps) { // layout const controlsContainerRef = useRef(null); // controls const onReplay = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); onSeek(-10); }, [onSeek], ); const onSkip = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); onSeek(10); }, [onSeek], ); const onTogglePlay = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); onPlayPause(!isPlaying); }, [isPlaying, onPlayPause], ); // volume control const VolumeIcon = useMemo(() => { if (!volume || volume == 0.0 || muted) { return MdVolumeOff; } else if (volume <= 0.33) { return MdVolumeMute; } else if (volume <= 0.67) { return MdVolumeDown; } else { return MdVolumeUp; } // only update when specific fields change // eslint-disable-next-line react-hooks/exhaustive-deps }, [volume, muted]); const onKeyboardShortcut = useCallback( (key: string | null, modifiers: KeyModifiers) => { if (!modifiers.down) { return; } switch (key) { case "ArrowDown": onSeek(-1); break; case "ArrowLeft": onSeek(-10); break; case "ArrowRight": onSeek(10); break; case "ArrowUp": onSeek(1); break; case "f": if (toggleFullscreen && !modifiers.repeat) { toggleFullscreen(); } break; case "m": if (setMuted && !modifiers.repeat && video) { setMuted(!muted); } break; case " ": onPlayPause(!isPlaying); break; } }, // only update when preview only changes // eslint-disable-next-line react-hooks/exhaustive-deps [video, isPlaying, fullscreen, toggleFullscreen, onSeek], ); useKeyboardListener( hotKeys ? ["ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp", "f", "m", " "] : [], onKeyboardShortcut, ); if (!show) { return; } return (
feat).length > MIN_ITEMS_WRAP && "min-w-[75%] flex-wrap", )} ref={controlsContainerRef} > {video && features.volume && (
{ e.stopPropagation(); if (setMuted) { setMuted(!muted); } }} /> {muted == false && ( (video.volume = value[0])} /> )}
)} {features.seek && ( )}
{isPlaying ? ( ) : ( )}
{features.seek && ( )} {features.playbackRate && ( { if (setControlsOpen) { setControlsOpen(open); } }} > {`${playbackRate}x`} onSetPlaybackRate(parseFloat(rate))} > {playbackRates.map((rate) => ( {rate}x ))} )} {features.plusUpload && onUploadFrame && ( { if (setControlsOpen) { setControlsOpen(false); } }} onOpen={() => { onPlayPause(false); if (setControlsOpen) { setControlsOpen(true); } }} onUploadFrame={onUploadFrame} containerRef={containerRef} /> )} {features.fullscreen && toggleFullscreen && (
{fullscreen ? : }
)}
); } type FrigatePlusUploadButtonProps = { video?: HTMLVideoElement | null; onOpen: () => void; onClose: () => void; onUploadFrame: () => void; containerRef?: React.MutableRefObject; }; function FrigatePlusUploadButton({ video, onOpen, onClose, onUploadFrame, containerRef, }: FrigatePlusUploadButtonProps) { const { t } = useTranslation(["components/player"]); const [videoImg, setVideoImg] = useState(); return ( { if (!open) { onClose(); } }} > { onOpen(); if (video) { const videoSize = [video.clientWidth, video.clientHeight]; const canvas = document.createElement("canvas"); canvas.width = videoSize[0]; canvas.height = videoSize[1]; const context = canvas?.getContext("2d"); if (context) { context.drawImage(video, 0, 0, videoSize[0], videoSize[1]); setVideoImg(canvas.toDataURL("image/webp")); } } }} /> {t("submitFrigatePlus.title")} {t("submitFrigatePlus.submit")} {t("button.cancel", { ns: "common" })} ); }