diff --git a/web/public/locales/en/views/live.json b/web/public/locales/en/views/live.json index f20fc5118..356c896ae 100644 --- a/web/public/locales/en/views/live.json +++ b/web/public/locales/en/views/live.json @@ -124,6 +124,9 @@ "available": "Audio is available for this stream", "unavailable": "Audio is not available for this stream" }, + "debug": { + "picker": "Stream selection unavailable in debug mode. Debug view always uses the stream assigned the detect role." + }, "twoWayTalk": { "tips": "Your device must support the feature and WebRTC must be configured for two-way talk.", "available": "Two-way talk is available for this stream", diff --git a/web/src/components/overlay/PtzControlPanel.tsx b/web/src/components/overlay/PtzControlPanel.tsx new file mode 100644 index 000000000..b4219842b --- /dev/null +++ b/web/src/components/overlay/PtzControlPanel.tsx @@ -0,0 +1,324 @@ +import { usePtzCommand } from "@/api/ws"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import useKeyboardListener from "@/hooks/use-keyboard-listener"; +import { CameraPtzInfo } from "@/types/ptz"; +import React, { useCallback } from "react"; +import { isDesktop, isMobile } from "react-device-detect"; +import { BsThreeDotsVertical } from "react-icons/bs"; +import { + FaAngleDown, + FaAngleLeft, + FaAngleRight, + FaAngleUp, +} from "react-icons/fa"; +import { TbViewfinder } from "react-icons/tb"; +import { + MdCenterFocusStrong, + MdCenterFocusWeak, + MdZoomIn, + MdZoomOut, +} from "react-icons/md"; +import useSWR from "swr"; +import { cn } from "@/lib/utils"; + +import { useTranslation } from "react-i18next"; +import TooltipButton from "@/views/button/TooltipButton"; + +export default function PtzControlPanel({ + className, + camera, + enabled, + clickOverlay, + setClickOverlay, +}: { + className?: string; + camera: string; + enabled: boolean; + clickOverlay: boolean; + setClickOverlay: React.Dispatch>; +}) { + const { t } = useTranslation(["views/live"]); + const { data: ptz } = useSWR( + enabled ? `${camera}/ptz/info` : null, + ); + + const { send: sendPtz } = usePtzCommand(camera); + + const onStop = useCallback( + (e: React.SyntheticEvent) => { + e.preventDefault(); + sendPtz("STOP"); + }, + [sendPtz], + ); + + useKeyboardListener( + [ + "ArrowLeft", + "ArrowRight", + "ArrowUp", + "ArrowDown", + "+", + "-", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + ], + (key, modifiers) => { + if (modifiers.repeat || !key) { + return; + } + + if (["1", "2", "3", "4", "5", "6", "7", "8", "9"].includes(key)) { + const presetNumber = parseInt(key); + if ( + ptz && + (ptz.presets?.length ?? 0) > 0 && + presetNumber <= ptz.presets.length + ) { + sendPtz(`preset_${ptz.presets[presetNumber - 1]}`); + } + return; + } + + if (!modifiers.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(modifiers.shift ? "FOCUS_IN" : "ZOOM_IN"); + break; + case "-": + sendPtz(modifiers.shift ? "FOCUS_OUT" : "ZOOM_OUT"); + break; + } + }, + ); + + return ( +
+ {ptz?.features?.includes("pt") && ( + <> + { + e.preventDefault(); + sendPtz("MOVE_LEFT"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("MOVE_LEFT"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + { + e.preventDefault(); + sendPtz("MOVE_UP"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("MOVE_UP"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + { + e.preventDefault(); + sendPtz("MOVE_DOWN"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("MOVE_DOWN"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + { + e.preventDefault(); + sendPtz("MOVE_RIGHT"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("MOVE_RIGHT"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + + )} + {ptz?.features?.includes("zoom") && ( + <> + { + e.preventDefault(); + sendPtz("ZOOM_IN"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("ZOOM_IN"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + { + e.preventDefault(); + sendPtz("ZOOM_OUT"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("ZOOM_OUT"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + + )} + {ptz?.features?.includes("focus") && ( + <> + { + e.preventDefault(); + sendPtz("FOCUS_IN"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("FOCUS_IN"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + { + e.preventDefault(); + sendPtz("FOCUS_OUT"); + }} + onTouchStart={(e) => { + e.preventDefault(); + sendPtz("FOCUS_OUT"); + }} + onMouseUp={onStop} + onTouchEnd={onStop} + > + + + + )} + + {ptz?.features?.includes("pt-r-fov") && ( + + + + + +

+ {clickOverlay + ? t("ptz.move.clickMove.disable") + : t("ptz.move.clickMove.enable")} +

+
+
+ )} + {(ptz?.presets?.length ?? 0) > 0 && ( + + + + + + + + +

{t("ptz.presets")}

+
+
+ + e.preventDefault()} + > + {ptz?.presets.map((preset) => ( + sendPtz(`preset_${preset}`)} + > + {preset} + + ))} + +
+ )} +
+ ); +} diff --git a/web/src/views/button/TooltipButton.tsx b/web/src/views/button/TooltipButton.tsx new file mode 100644 index 000000000..5203c9e60 --- /dev/null +++ b/web/src/views/button/TooltipButton.tsx @@ -0,0 +1,52 @@ +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { ReactNode } from "react"; + +type TooltipButtonProps = { + label: string; + onClick?: () => void; + onMouseDown?: (e: React.MouseEvent) => void; + onMouseUp?: (e: React.MouseEvent) => void; + onTouchStart?: (e: React.TouchEvent) => void; + onTouchEnd?: (e: React.TouchEvent) => void; + children: ReactNode; + className?: string; +}; + +export default function TooltipButton({ + label, + onClick, + onMouseDown, + onMouseUp, + onTouchStart, + onTouchEnd, + children, + className, + ...props +}: TooltipButtonProps) { + return ( + + + + + +

{label}

+
+
+ ); +} diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index bcc18c765..eef1c663a 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -17,7 +17,6 @@ import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; import { DropdownMenu, DropdownMenuContent, - DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { @@ -25,11 +24,6 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { useResizeObserver } from "@/hooks/resize-observer"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; @@ -38,10 +32,8 @@ import { LiveStreamMetadata, VideoResolutionType, } from "@/types/live"; -import { CameraPtzInfo } from "@/types/ptz"; import { RecordingStartingPoint } from "@/types/record"; import React, { - ReactNode, useCallback, useEffect, useMemo, @@ -56,12 +48,7 @@ import { isTablet, useMobileOrientation, } from "react-device-detect"; -import { BsThreeDotsVertical } from "react-icons/bs"; import { - FaAngleDown, - FaAngleLeft, - FaAngleRight, - FaAngleUp, FaCog, FaCompress, FaExpand, @@ -91,8 +78,6 @@ import { LuX, } from "react-icons/lu"; import { - MdCenterFocusStrong, - MdCenterFocusWeak, MdClosedCaption, MdClosedCaptionDisabled, MdNoPhotography, @@ -100,8 +85,6 @@ import { MdPersonOff, MdPersonSearch, MdPhotoCamera, - MdZoomIn, - MdZoomOut, } from "react-icons/md"; import { Link, useNavigate } from "react-router-dom"; import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"; @@ -126,6 +109,8 @@ import { Toaster } from "@/components/ui/sonner"; import { useIsAdmin } from "@/hooks/use-is-admin"; import { Trans, useTranslation } from "react-i18next"; import { useDocDomain } from "@/hooks/use-doc-domain"; +import PtzControlPanel from "@/components/overlay/PtzControlPanel"; +import ObjectSettingsView from "../settings/ObjectSettingsView"; type LiveCameraViewProps = { config?: FrigateConfig; @@ -287,6 +272,7 @@ export default function LiveCameraView({ ); const [showStats, setShowStats] = useState(false); + const [debug, setDebug] = useState(false); const [fullResolution, setFullResolution] = useState({ width: 0, @@ -437,7 +423,11 @@ export default function LiveCameraView({ ); return ( - +
)} {supports2WayTalk && ( @@ -570,7 +561,7 @@ export default function LiveCameraView({ setAudio(true); } }} - disabled={!cameraEnabled} + disabled={!cameraEnabled || debug} /> )} {supportsAudioOutput && preferredLiveMode != "jsmpeg" && ( @@ -585,7 +576,7 @@ export default function LiveCameraView({ : t("cameraAudio.enable", { ns: "views/live" }) } onClick={() => setAudio(!audio)} - disabled={!cameraEnabled} + disabled={!cameraEnabled || debug} /> )}
-
+ {!debug ? ( +
+ +
+ +
+
+ {camera?.audio?.enabled_in_config && + audioTranscriptionState == "ON" && + transcription != null && ( +
+ {transcription} +
+ )} +
+ ) : ( -
- -
+
- {camera?.audio?.enabled_in_config && - audioTranscriptionState == "ON" && - transcription != null && ( -
- {transcription} -
- )} -
+ )} {camera.onvif.host != "" && (
void; - onMouseDown?: (e: React.MouseEvent) => void; - onMouseUp?: (e: React.MouseEvent) => void; - onTouchStart?: (e: React.TouchEvent) => void; - onTouchEnd?: (e: React.TouchEvent) => void; - children: ReactNode; - className?: string; -}; - -function TooltipButton({ - label, - onClick, - onMouseDown, - onMouseUp, - onTouchStart, - onTouchEnd, - children, - className, - ...props -}: TooltipButtonProps) { - return ( - - - - - -

{label}

-
-
- ); -} - -function PtzControlPanel({ - camera, - enabled, - clickOverlay, - setClickOverlay, -}: { - camera: string; - enabled: boolean; - clickOverlay: boolean; - setClickOverlay: React.Dispatch>; -}) { - const { t } = useTranslation(["views/live"]); - const { data: ptz } = useSWR( - enabled ? `${camera}/ptz/info` : null, - ); - - const { send: sendPtz } = usePtzCommand(camera); - - const onStop = useCallback( - (e: React.SyntheticEvent) => { - e.preventDefault(); - sendPtz("STOP"); - }, - [sendPtz], - ); - - useKeyboardListener( - [ - "ArrowLeft", - "ArrowRight", - "ArrowUp", - "ArrowDown", - "+", - "-", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - ], - (key, modifiers) => { - if (modifiers.repeat || !key) { - return; - } - - if (["1", "2", "3", "4", "5", "6", "7", "8", "9"].includes(key)) { - const presetNumber = parseInt(key); - if ( - ptz && - (ptz.presets?.length ?? 0) > 0 && - presetNumber <= ptz.presets.length - ) { - sendPtz(`preset_${ptz.presets[presetNumber - 1]}`); - } - return; - } - - if (!modifiers.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(modifiers.shift ? "FOCUS_IN" : "ZOOM_IN"); - break; - case "-": - sendPtz(modifiers.shift ? "FOCUS_OUT" : "ZOOM_OUT"); - break; - } - }, - ); - - return ( -
- {ptz?.features?.includes("pt") && ( - <> - { - e.preventDefault(); - sendPtz("MOVE_LEFT"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("MOVE_LEFT"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - { - e.preventDefault(); - sendPtz("MOVE_UP"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("MOVE_UP"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - { - e.preventDefault(); - sendPtz("MOVE_DOWN"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("MOVE_DOWN"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - { - e.preventDefault(); - sendPtz("MOVE_RIGHT"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("MOVE_RIGHT"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - - )} - {ptz?.features?.includes("zoom") && ( - <> - { - e.preventDefault(); - sendPtz("ZOOM_IN"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("ZOOM_IN"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - { - e.preventDefault(); - sendPtz("ZOOM_OUT"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("ZOOM_OUT"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - - )} - {ptz?.features?.includes("focus") && ( - <> - { - e.preventDefault(); - sendPtz("FOCUS_IN"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("FOCUS_IN"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - { - e.preventDefault(); - sendPtz("FOCUS_OUT"); - }} - onTouchStart={(e) => { - e.preventDefault(); - sendPtz("FOCUS_OUT"); - }} - onMouseUp={onStop} - onTouchEnd={onStop} - > - - - - )} - - {ptz?.features?.includes("pt-r-fov") && ( - - - - - -

- {clickOverlay - ? t("ptz.move.clickMove.disable") - : t("ptz.move.clickMove.enable")} -

-
-
- )} - {(ptz?.presets?.length ?? 0) > 0 && ( - - - - - - - - -

{t("ptz.presets")}

-
-
- - e.preventDefault()} - > - {ptz?.presets.map((preset) => ( - sendPtz(`preset_${preset}`)} - > - {preset} - - ))} - -
- )} -
- ); -} - function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) { const { t } = useTranslation(["views/live", "views/events"]); const rankMap = { all: 0, motion: 1, active_objects: 2 }; @@ -1065,6 +745,8 @@ type FrigateCameraFeaturesProps = { supportsAudioOutput: boolean; supports2WayTalk: boolean; cameraEnabled: boolean; + debug: boolean; + setDebug: (debug: boolean) => void; }; function FrigateCameraFeatures({ camera, @@ -1085,6 +767,8 @@ function FrigateCameraFeatures({ supportsAudioOutput, supports2WayTalk, cameraEnabled, + debug, + setDebug, }: FrigateCameraFeaturesProps) { const { t } = useTranslation(["views/live", "components/dialog"]); const { getLocaleDocUrl } = useDocDomain(); @@ -1196,10 +880,6 @@ function FrigateCameraFeatures({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // navigate for debug view - - const navigate = useNavigate(); - // desktop shows icons part of row if (isDesktop || isTablet) { return ( @@ -1215,7 +895,7 @@ function FrigateCameraFeatures({ enabledState == "ON" ? t("camera.disable") : t("camera.enable") } onClick={() => sendEnabled(enabledState == "ON" ? "OFF" : "ON")} - disabled={false} + disabled={debug} /> -
- {preferredLiveMode != "jsmpeg" && isRestreamed && ( + {debug && (
- {supportsAudioOutput ? ( - <> - -
{t("stream.audio.available")}
- - ) : ( - <> - -
{t("stream.audio.unavailable")}
- - -
- - - {t("button.info", { ns: "common" })} - -
-
- - {t("stream.audio.tips.title")} -
- - {t("readTheDocumentation", { - ns: "common", - })} - - -
-
-
- - )} + <> + +
{t("stream.debug.picker")}
+
)} + {preferredLiveMode != "jsmpeg" && + !debug && + isRestreamed && ( +
+ {supportsAudioOutput ? ( + <> + +
{t("stream.audio.available")}
+ + ) : ( + <> + +
{t("stream.audio.unavailable")}
+ + +
+ + + {t("button.info", { ns: "common" })} + +
+
+ + {t("stream.audio.tips.title")} +
+ + {t("readTheDocumentation", { + ns: "common", + })} + + +
+
+
+ + )} +
+ )} + {preferredLiveMode != "jsmpeg" && + !debug && isRestreamed && supportsAudioOutput && (
@@ -1503,29 +1195,31 @@ function FrigateCameraFeatures({
)} - {preferredLiveMode == "jsmpeg" && isRestreamed && ( -
-
- + {preferredLiveMode == "jsmpeg" && + !debug && + isRestreamed && ( +
+
+ -

- {t("stream.lowBandwidth.tips")} -

-
- -
- )} + +
+ )}
)} {isRestreamed && ( @@ -1540,6 +1234,7 @@ function FrigateCameraFeatures({ setPlayInBackground(checked) @@ -1564,6 +1259,7 @@ function FrigateCameraFeatures({ setShowStats(checked)} /> @@ -1574,17 +1270,22 @@ function FrigateCameraFeatures({ })}

-
- navigate(`/settings?page=debug&camera=${camera.name}`) - } - > -
- {t("streaming.debugView", { - ns: "components/dialog", - })} - +
+
+ + setDebug(checked)} + />
@@ -1724,6 +1425,7 @@ function FrigateCameraFeatures({ onValueChange={(value) => { setStreamName?.(value); }} + disabled={debug} > @@ -1749,7 +1451,17 @@ function FrigateCameraFeatures({ - {preferredLiveMode != "jsmpeg" && isRestreamed && ( + + {debug && ( +
+ <> + +
{t("stream.debug.picker")}
+ +
+ )} + + {preferredLiveMode != "jsmpeg" && !debug && isRestreamed && (
{supportsAudioOutput ? ( <> @@ -1789,6 +1501,7 @@ function FrigateCameraFeatures({
)} {preferredLiveMode != "jsmpeg" && + !debug && isRestreamed && supportsAudioOutput && (
@@ -1835,7 +1548,6 @@ function FrigateCameraFeatures({
-

{t("stream.lowBandwidth.tips")}

@@ -1880,6 +1594,7 @@ function FrigateCameraFeatures({ onCheckedChange={(checked) => { setPlayInBackground(checked); }} + disabled={debug} />

{t("manualRecording.playInBackground.desc")} @@ -1892,6 +1607,7 @@ function FrigateCameraFeatures({ onCheckedChange={(checked) => { setShowStats(checked); }} + disabled={debug} />

{t("manualRecording.showStats.desc")} @@ -1899,16 +1615,12 @@ function FrigateCameraFeatures({

)} -
-
- {t("manualRecording.debugView")} - - navigate(`/settings?page=debug&camera=${camera.name}`) - } - className="ml-2 inline-flex size-5 cursor-pointer" - /> -
+
+ setDebug(checked)} + />