import { useAudioState, useDetectState, usePtzCommand, useRecordingsState, useSnapshotsState, } from "@/api/ws"; import CameraFeatureToggle from "@/components/dynamic/CameraFeatureToggle"; import LivePlayer from "@/components/player/LivePlayer"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { TooltipProvider } from "@/components/ui/tooltip"; import { useResizeObserver } from "@/hooks/resize-observer"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { CameraConfig } from "@/types/frigateConfig"; import { CameraPtzInfo } from "@/types/ptz"; import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { isDesktop, isMobile, isSafari, useMobileOrientation, } from "react-device-detect"; import { BsThreeDotsVertical } from "react-icons/bs"; import { FaAngleDown, FaAngleLeft, FaAngleRight, FaAngleUp, FaCompress, FaExpand, } from "react-icons/fa"; import { GiSpeaker, GiSpeakerOff } from "react-icons/gi"; import { IoMdArrowBack } from "react-icons/io"; import { LuEar, LuEarOff, LuVideo, LuVideoOff } from "react-icons/lu"; import { MdNoPhotography, MdPersonOff, MdPersonSearch, MdPhotoCamera, MdZoomIn, MdZoomOut, } from "react-icons/md"; import { useNavigate } from "react-router-dom"; import useSWR from "swr"; type LiveCameraViewProps = { camera: CameraConfig; }; export default function LiveCameraView({ camera }: LiveCameraViewProps) { const navigate = useNavigate(); const { isPortrait } = useMobileOrientation(); const mainRef = useRef(null); const [{ width: windowWidth, height: windowHeight }] = useResizeObserver(window); // camera features const { payload: detectState, send: sendDetect } = useDetectState( camera.name, ); const { payload: recordState, send: sendRecord } = useRecordingsState( camera.name, ); const { payload: snapshotState, send: sendSnapshot } = useSnapshotsState( camera.name, ); const { payload: audioState, send: sendAudio } = useAudioState(camera.name); // fullscreen state useEffect(() => { if (mainRef.current == null) { return; } const listener = () => { setFullscreen(document.fullscreenElement != null); }; document.addEventListener("fullscreenchange", listener); return () => { document.removeEventListener("fullscreenchange", listener); }; }, [mainRef]); // playback state const [audio, setAudio] = useState(false); const [fullscreen, setFullscreen] = useState(false); const growClassName = useMemo(() => { const aspect = camera.detect.width / camera.detect.height; if (isMobile) { if (isPortrait) { return "absolute left-2 right-2 top-[50%] -translate-y-[50%]"; } else { if (aspect > 16 / 9) { return "absolute left-0 top-[50%] -translate-y-[50%]"; } else { return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]"; } } } if (fullscreen) { if (aspect > 16 / 9) { return "absolute inset-x-2 top-[50%] -translate-y-[50%]"; } else { return "absolute inset-y-2 left-[50%] -translate-x-[50%]"; } } else { return "absolute top-2 bottom-2 left-[50%] -translate-x-[50%]"; } }, [camera, fullscreen, isPortrait]); const windowAspectRatio = useMemo(() => { return windowWidth / windowHeight; }, [windowWidth, windowHeight]); const cameraAspectRatio = useMemo(() => { return camera.detect.width / camera.detect.height; }, [camera]); const aspectRatio = useMemo(() => { if (isMobile || fullscreen) { return cameraAspectRatio; } else { return windowAspectRatio < cameraAspectRatio ? windowAspectRatio - 0.05 : cameraAspectRatio - 0.03; } }, [cameraAspectRatio, windowAspectRatio, fullscreen]); return (
{!fullscreen ? ( ) : (
)}
{ if (fullscreen) { document.exitFullscreen(); } else { mainRef.current?.requestFullscreen(); } }} /> setAudio(!audio)} /> sendDetect(detectState == "ON" ? "OFF" : "ON")} /> sendRecord(recordState == "ON" ? "OFF" : "ON")} /> sendSnapshot(snapshotState == "ON" ? "OFF" : "ON")} /> {camera.audio.enabled_in_config && ( sendAudio(audioState == "ON" ? "OFF" : "ON")} /> )}
{camera.onvif.host != "" && }
); } function PtzControlPanel({ camera }: { camera: string }) { const { data: ptz } = useSWR(`${camera}/ptz/info`); const { send: sendPtz } = usePtzCommand(camera); const onStop = useCallback( (e: React.SyntheticEvent) => { e.preventDefault(); sendPtz("STOP"); }, [sendPtz], ); useKeyboardListener( ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "+", "-"], (key, down, repeat) => { if (repeat) { return; } if (!down) { sendPtz("STOP"); return; } switch (key) { case "ArrowLeft": sendPtz("MOVE_LEFT"); break; case "ArrowRight": sendPtz("MOVE_RIGHT"); break; case "ArrowUp": sendPtz("MOVE_UP"); break; case "ArrowDown": sendPtz("MOVE_DOWN"); break; case "+": sendPtz("ZOOM_IN"); break; case "-": sendPtz("ZOOM_OUT"); break; } }, ); return (
{ptz?.features?.includes("pt") && ( <> )} {ptz?.features?.includes("zoom") && ( <> )} {(ptz?.presets?.length ?? 0) > 0 && ( {ptz?.presets.map((preset) => { return ( sendPtz(`preset_${preset}`)}> {preset} ); })} )}
); }