import { useCallback, useMemo, useState } from "react"; import { 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 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"; 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; setFullscreen?: (full: boolean) => void; }; 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, setFullscreen, }: VideoControlsProps) { 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, down: boolean, repeat: boolean) => { switch (key) { case "ArrowLeft": if (down) { onSeek(-10); } break; case "ArrowRight": if (down) { onSeek(10); } break; case "f": if (setFullscreen && down && !repeat) { setFullscreen(!fullscreen); } break; case "m": if (setMuted && down && !repeat && video) { setMuted(!muted); } break; case " ": if (down) { onPlayPause(!isPlaying); } break; } }, // only update when preview only changes // eslint-disable-next-line react-hooks/exhaustive-deps [video, isPlaying, fullscreen, setFullscreen, onSeek], ); useKeyboardListener( hotKeys ? ["ArrowLeft", "ArrowRight", "f", "m", " "] : [], onKeyboardShortcut, ); if (!show) { return; } return (
feat).length > MIN_ITEMS_WRAP && "min-w-[75%] flex-wrap", )} > {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} /> )} {features.fullscreen && setFullscreen && (
setFullscreen(!fullscreen)} > {fullscreen ? : }
)}
); } type FrigatePlusUploadButtonProps = { video?: HTMLVideoElement | null; onOpen: () => void; onClose: () => void; onUploadFrame: () => void; }; function FrigatePlusUploadButton({ video, onOpen, onClose, onUploadFrame, }: FrigatePlusUploadButtonProps) { 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")); } } }} /> Submit this frame to Frigate+? Submit Cancel ); }