mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 13:34:13 +03:00
Implement debug live view as part of live (#20270)
* Cleanup components * integrate debug view * Refactor menu handling * Cleanup * cleanup * Improve ptz placement for debug view * Cleanup * Cleanup mobile * Always show options * Add info for stream picking being disabled * Add to mobile too * Fix ns * Cleanup
This commit is contained in:
parent
9fdce80729
commit
a08fda62f8
@ -124,6 +124,9 @@
|
|||||||
"available": "Audio is available for this stream",
|
"available": "Audio is available for this stream",
|
||||||
"unavailable": "Audio is not 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": {
|
"twoWayTalk": {
|
||||||
"tips": "Your device must support the feature and WebRTC must be configured for two-way talk.",
|
"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",
|
"available": "Two-way talk is available for this stream",
|
||||||
|
|||||||
324
web/src/components/overlay/PtzControlPanel.tsx
Normal file
324
web/src/components/overlay/PtzControlPanel.tsx
Normal file
@ -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<React.SetStateAction<boolean>>;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation(["views/live"]);
|
||||||
|
const { data: ptz } = useSWR<CameraPtzInfo>(
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-x-2 bottom-[10%] flex select-none flex-wrap items-center justify-center gap-1 md:left-[50%] md:-translate-x-[50%] md:flex-nowrap",
|
||||||
|
className ?? "",
|
||||||
|
isMobile && "landscape:ml-12",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{ptz?.features?.includes("pt") && (
|
||||||
|
<>
|
||||||
|
<TooltipButton
|
||||||
|
label={t("ptz.move.left.label")}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("MOVE_LEFT");
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("MOVE_LEFT");
|
||||||
|
}}
|
||||||
|
onMouseUp={onStop}
|
||||||
|
onTouchEnd={onStop}
|
||||||
|
>
|
||||||
|
<FaAngleLeft />
|
||||||
|
</TooltipButton>
|
||||||
|
<TooltipButton
|
||||||
|
label={t("ptz.move.up.label")}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("MOVE_UP");
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("MOVE_UP");
|
||||||
|
}}
|
||||||
|
onMouseUp={onStop}
|
||||||
|
onTouchEnd={onStop}
|
||||||
|
>
|
||||||
|
<FaAngleUp />
|
||||||
|
</TooltipButton>
|
||||||
|
<TooltipButton
|
||||||
|
label={t("ptz.move.down.label")}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("MOVE_DOWN");
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("MOVE_DOWN");
|
||||||
|
}}
|
||||||
|
onMouseUp={onStop}
|
||||||
|
onTouchEnd={onStop}
|
||||||
|
>
|
||||||
|
<FaAngleDown />
|
||||||
|
</TooltipButton>
|
||||||
|
<TooltipButton
|
||||||
|
label={t("ptz.move.right.label")}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("MOVE_RIGHT");
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("MOVE_RIGHT");
|
||||||
|
}}
|
||||||
|
onMouseUp={onStop}
|
||||||
|
onTouchEnd={onStop}
|
||||||
|
>
|
||||||
|
<FaAngleRight />
|
||||||
|
</TooltipButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{ptz?.features?.includes("zoom") && (
|
||||||
|
<>
|
||||||
|
<TooltipButton
|
||||||
|
label={t("ptz.zoom.in.label")}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("ZOOM_IN");
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("ZOOM_IN");
|
||||||
|
}}
|
||||||
|
onMouseUp={onStop}
|
||||||
|
onTouchEnd={onStop}
|
||||||
|
>
|
||||||
|
<MdZoomIn />
|
||||||
|
</TooltipButton>
|
||||||
|
<TooltipButton
|
||||||
|
label={t("ptz.zoom.out.label")}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("ZOOM_OUT");
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("ZOOM_OUT");
|
||||||
|
}}
|
||||||
|
onMouseUp={onStop}
|
||||||
|
onTouchEnd={onStop}
|
||||||
|
>
|
||||||
|
<MdZoomOut />
|
||||||
|
</TooltipButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{ptz?.features?.includes("focus") && (
|
||||||
|
<>
|
||||||
|
<TooltipButton
|
||||||
|
label={t("ptz.focus.in.label")}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("FOCUS_IN");
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("FOCUS_IN");
|
||||||
|
}}
|
||||||
|
onMouseUp={onStop}
|
||||||
|
onTouchEnd={onStop}
|
||||||
|
>
|
||||||
|
<MdCenterFocusStrong />
|
||||||
|
</TooltipButton>
|
||||||
|
<TooltipButton
|
||||||
|
label={t("ptz.focus.out.label")}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("FOCUS_OUT");
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
sendPtz("FOCUS_OUT");
|
||||||
|
}}
|
||||||
|
onMouseUp={onStop}
|
||||||
|
onTouchEnd={onStop}
|
||||||
|
>
|
||||||
|
<MdCenterFocusWeak />
|
||||||
|
</TooltipButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ptz?.features?.includes("pt-r-fov") && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className={`${clickOverlay ? "text-selected" : "text-primary"}`}
|
||||||
|
aria-label={t("ptz.move.clickMove.label")}
|
||||||
|
onClick={() => setClickOverlay(!clickOverlay)}
|
||||||
|
>
|
||||||
|
<TbViewfinder />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
{clickOverlay
|
||||||
|
? t("ptz.move.clickMove.disable")
|
||||||
|
: t("ptz.move.clickMove.enable")}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{(ptz?.presets?.length ?? 0) > 0 && (
|
||||||
|
<DropdownMenu modal={!isDesktop}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button aria-label={t("ptz.presets")}>
|
||||||
|
<BsThreeDotsVertical />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t("ptz.presets")}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="scrollbar-container max-h-[40dvh] overflow-y-auto"
|
||||||
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{ptz?.presets.map((preset) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={preset}
|
||||||
|
aria-label={preset}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onSelect={() => sendPtz(`preset_${preset}`)}
|
||||||
|
>
|
||||||
|
{preset}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
web/src/views/button/TooltipButton.tsx
Normal file
52
web/src/views/button/TooltipButton.tsx
Normal file
@ -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 (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
aria-label={label}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
onMouseUp={onMouseUp}
|
||||||
|
onTouchStart={onTouchStart}
|
||||||
|
onTouchEnd={onTouchEnd}
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{label}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -17,7 +17,6 @@ import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
|
|||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
@ -25,11 +24,6 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||||
@ -38,10 +32,8 @@ import {
|
|||||||
LiveStreamMetadata,
|
LiveStreamMetadata,
|
||||||
VideoResolutionType,
|
VideoResolutionType,
|
||||||
} from "@/types/live";
|
} from "@/types/live";
|
||||||
import { CameraPtzInfo } from "@/types/ptz";
|
|
||||||
import { RecordingStartingPoint } from "@/types/record";
|
import { RecordingStartingPoint } from "@/types/record";
|
||||||
import React, {
|
import React, {
|
||||||
ReactNode,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@ -56,12 +48,7 @@ import {
|
|||||||
isTablet,
|
isTablet,
|
||||||
useMobileOrientation,
|
useMobileOrientation,
|
||||||
} from "react-device-detect";
|
} from "react-device-detect";
|
||||||
import { BsThreeDotsVertical } from "react-icons/bs";
|
|
||||||
import {
|
import {
|
||||||
FaAngleDown,
|
|
||||||
FaAngleLeft,
|
|
||||||
FaAngleRight,
|
|
||||||
FaAngleUp,
|
|
||||||
FaCog,
|
FaCog,
|
||||||
FaCompress,
|
FaCompress,
|
||||||
FaExpand,
|
FaExpand,
|
||||||
@ -91,8 +78,6 @@ import {
|
|||||||
LuX,
|
LuX,
|
||||||
} from "react-icons/lu";
|
} from "react-icons/lu";
|
||||||
import {
|
import {
|
||||||
MdCenterFocusStrong,
|
|
||||||
MdCenterFocusWeak,
|
|
||||||
MdClosedCaption,
|
MdClosedCaption,
|
||||||
MdClosedCaptionDisabled,
|
MdClosedCaptionDisabled,
|
||||||
MdNoPhotography,
|
MdNoPhotography,
|
||||||
@ -100,8 +85,6 @@ import {
|
|||||||
MdPersonOff,
|
MdPersonOff,
|
||||||
MdPersonSearch,
|
MdPersonSearch,
|
||||||
MdPhotoCamera,
|
MdPhotoCamera,
|
||||||
MdZoomIn,
|
|
||||||
MdZoomOut,
|
|
||||||
} from "react-icons/md";
|
} from "react-icons/md";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
|
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 { useIsAdmin } from "@/hooks/use-is-admin";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||||
|
import PtzControlPanel from "@/components/overlay/PtzControlPanel";
|
||||||
|
import ObjectSettingsView from "../settings/ObjectSettingsView";
|
||||||
|
|
||||||
type LiveCameraViewProps = {
|
type LiveCameraViewProps = {
|
||||||
config?: FrigateConfig;
|
config?: FrigateConfig;
|
||||||
@ -287,6 +272,7 @@ export default function LiveCameraView({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [showStats, setShowStats] = useState(false);
|
const [showStats, setShowStats] = useState(false);
|
||||||
|
const [debug, setDebug] = useState(false);
|
||||||
|
|
||||||
const [fullResolution, setFullResolution] = useState<VideoResolutionType>({
|
const [fullResolution, setFullResolution] = useState<VideoResolutionType>({
|
||||||
width: 0,
|
width: 0,
|
||||||
@ -437,7 +423,11 @@ export default function LiveCameraView({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TransformWrapper minScale={1.0} wheel={{ smoothStep: 0.005 }}>
|
<TransformWrapper
|
||||||
|
minScale={1.0}
|
||||||
|
wheel={{ smoothStep: 0.005 }}
|
||||||
|
disabled={debug}
|
||||||
|
>
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div
|
<div
|
||||||
ref={mainRef}
|
ref={mainRef}
|
||||||
@ -523,6 +513,7 @@ export default function LiveCameraView({
|
|||||||
variant={fullscreen ? "overlay" : "primary"}
|
variant={fullscreen ? "overlay" : "primary"}
|
||||||
Icon={fullscreen ? FaCompress : FaExpand}
|
Icon={fullscreen ? FaCompress : FaExpand}
|
||||||
isActive={fullscreen}
|
isActive={fullscreen}
|
||||||
|
disabled={debug}
|
||||||
title={
|
title={
|
||||||
fullscreen
|
fullscreen
|
||||||
? t("button.close", { ns: "common" })
|
? t("button.close", { ns: "common" })
|
||||||
@ -550,7 +541,7 @@ export default function LiveCameraView({
|
|||||||
setPip(false);
|
setPip(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!cameraEnabled}
|
disabled={!cameraEnabled || debug}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{supports2WayTalk && (
|
{supports2WayTalk && (
|
||||||
@ -570,7 +561,7 @@ export default function LiveCameraView({
|
|||||||
setAudio(true);
|
setAudio(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!cameraEnabled}
|
disabled={!cameraEnabled || debug}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{supportsAudioOutput && preferredLiveMode != "jsmpeg" && (
|
{supportsAudioOutput && preferredLiveMode != "jsmpeg" && (
|
||||||
@ -585,7 +576,7 @@ export default function LiveCameraView({
|
|||||||
: t("cameraAudio.enable", { ns: "views/live" })
|
: t("cameraAudio.enable", { ns: "views/live" })
|
||||||
}
|
}
|
||||||
onClick={() => setAudio(!audio)}
|
onClick={() => setAudio(!audio)}
|
||||||
disabled={!cameraEnabled}
|
disabled={!cameraEnabled || debug}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<FrigateCameraFeatures
|
<FrigateCameraFeatures
|
||||||
@ -609,10 +600,66 @@ export default function LiveCameraView({
|
|||||||
supportsAudioOutput={supportsAudioOutput}
|
supportsAudioOutput={supportsAudioOutput}
|
||||||
supports2WayTalk={supports2WayTalk}
|
supports2WayTalk={supports2WayTalk}
|
||||||
cameraEnabled={cameraEnabled}
|
cameraEnabled={cameraEnabled}
|
||||||
|
debug={debug}
|
||||||
|
setDebug={setDebug}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="player-container" className="size-full" ref={containerRef}>
|
{!debug ? (
|
||||||
|
<div id="player-container" className="size-full" ref={containerRef}>
|
||||||
|
<TransformComponent
|
||||||
|
wrapperStyle={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
contentStyle={{
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
padding: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex flex-col items-center justify-center ${growClassName}`}
|
||||||
|
ref={clickOverlayRef}
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
style={{
|
||||||
|
aspectRatio: constrainedAspectRatio,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LivePlayer
|
||||||
|
key={camera.name}
|
||||||
|
className={`${fullscreen ? "*:rounded-none" : ""}`}
|
||||||
|
windowVisible
|
||||||
|
showStillWithoutActivity={false}
|
||||||
|
cameraConfig={camera}
|
||||||
|
playAudio={audio}
|
||||||
|
playInBackground={playInBackground ?? false}
|
||||||
|
showStats={showStats}
|
||||||
|
micEnabled={mic}
|
||||||
|
iOSCompatFullScreen={isIOS}
|
||||||
|
preferredLiveMode={preferredLiveMode}
|
||||||
|
useWebGL={true}
|
||||||
|
streamName={streamName ?? ""}
|
||||||
|
pip={pip}
|
||||||
|
containerRef={containerRef}
|
||||||
|
setFullResolution={setFullResolution}
|
||||||
|
onError={handleError}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TransformComponent>
|
||||||
|
{camera?.audio?.enabled_in_config &&
|
||||||
|
audioTranscriptionState == "ON" &&
|
||||||
|
transcription != null && (
|
||||||
|
<div
|
||||||
|
ref={transcriptionRef}
|
||||||
|
className="text-md scrollbar-container absolute bottom-4 left-1/2 max-h-[15vh] w-[75%] -translate-x-1/2 overflow-y-auto rounded-lg bg-black/70 p-2 text-white md:w-[50%]"
|
||||||
|
>
|
||||||
|
{transcription}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<TransformComponent
|
<TransformComponent
|
||||||
wrapperStyle={{
|
wrapperStyle={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -622,53 +669,16 @@ export default function LiveCameraView({
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
padding: "8px",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<ObjectSettingsView selectedCamera={camera.name} />
|
||||||
className={`flex flex-col items-center justify-center ${growClassName}`}
|
|
||||||
ref={clickOverlayRef}
|
|
||||||
onClick={handleOverlayClick}
|
|
||||||
style={{
|
|
||||||
aspectRatio: constrainedAspectRatio,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LivePlayer
|
|
||||||
key={camera.name}
|
|
||||||
className={`${fullscreen ? "*:rounded-none" : ""}`}
|
|
||||||
windowVisible
|
|
||||||
showStillWithoutActivity={false}
|
|
||||||
cameraConfig={camera}
|
|
||||||
playAudio={audio}
|
|
||||||
playInBackground={playInBackground ?? false}
|
|
||||||
showStats={showStats}
|
|
||||||
micEnabled={mic}
|
|
||||||
iOSCompatFullScreen={isIOS}
|
|
||||||
preferredLiveMode={preferredLiveMode}
|
|
||||||
useWebGL={true}
|
|
||||||
streamName={streamName ?? ""}
|
|
||||||
pip={pip}
|
|
||||||
containerRef={containerRef}
|
|
||||||
setFullResolution={setFullResolution}
|
|
||||||
onError={handleError}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TransformComponent>
|
</TransformComponent>
|
||||||
{camera?.audio?.enabled_in_config &&
|
)}
|
||||||
audioTranscriptionState == "ON" &&
|
|
||||||
transcription != null && (
|
|
||||||
<div
|
|
||||||
ref={transcriptionRef}
|
|
||||||
className="text-md scrollbar-container absolute bottom-4 left-1/2 max-h-[15vh] w-[75%] -translate-x-1/2 overflow-y-auto rounded-lg bg-black/70 p-2 text-white md:w-[50%]"
|
|
||||||
>
|
|
||||||
{transcription}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{camera.onvif.host != "" && (
|
{camera.onvif.host != "" && (
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<PtzControlPanel
|
<PtzControlPanel
|
||||||
|
className={debug && isMobile ? "bottom-auto top-[25%]" : ""}
|
||||||
camera={camera.name}
|
camera={camera.name}
|
||||||
enabled={cameraEnabled}
|
enabled={cameraEnabled}
|
||||||
clickOverlay={clickOverlay}
|
clickOverlay={clickOverlay}
|
||||||
@ -680,336 +690,6 @@ export default function LiveCameraView({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
function TooltipButton({
|
|
||||||
label,
|
|
||||||
onClick,
|
|
||||||
onMouseDown,
|
|
||||||
onMouseUp,
|
|
||||||
onTouchStart,
|
|
||||||
onTouchEnd,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: TooltipButtonProps) {
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
aria-label={label}
|
|
||||||
onClick={onClick}
|
|
||||||
onMouseDown={onMouseDown}
|
|
||||||
onMouseUp={onMouseUp}
|
|
||||||
onTouchStart={onTouchStart}
|
|
||||||
onTouchEnd={onTouchEnd}
|
|
||||||
className={className}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{label}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PtzControlPanel({
|
|
||||||
camera,
|
|
||||||
enabled,
|
|
||||||
clickOverlay,
|
|
||||||
setClickOverlay,
|
|
||||||
}: {
|
|
||||||
camera: string;
|
|
||||||
enabled: boolean;
|
|
||||||
clickOverlay: boolean;
|
|
||||||
setClickOverlay: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation(["views/live"]);
|
|
||||||
const { data: ptz } = useSWR<CameraPtzInfo>(
|
|
||||||
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 (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"absolute inset-x-2 bottom-[10%] flex select-none flex-wrap items-center justify-center gap-1 md:left-[50%] md:-translate-x-[50%] md:flex-nowrap",
|
|
||||||
isMobile && "landscape:ml-12",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{ptz?.features?.includes("pt") && (
|
|
||||||
<>
|
|
||||||
<TooltipButton
|
|
||||||
label={t("ptz.move.left.label")}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("MOVE_LEFT");
|
|
||||||
}}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("MOVE_LEFT");
|
|
||||||
}}
|
|
||||||
onMouseUp={onStop}
|
|
||||||
onTouchEnd={onStop}
|
|
||||||
>
|
|
||||||
<FaAngleLeft />
|
|
||||||
</TooltipButton>
|
|
||||||
<TooltipButton
|
|
||||||
label={t("ptz.move.up.label")}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("MOVE_UP");
|
|
||||||
}}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("MOVE_UP");
|
|
||||||
}}
|
|
||||||
onMouseUp={onStop}
|
|
||||||
onTouchEnd={onStop}
|
|
||||||
>
|
|
||||||
<FaAngleUp />
|
|
||||||
</TooltipButton>
|
|
||||||
<TooltipButton
|
|
||||||
label={t("ptz.move.down.label")}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("MOVE_DOWN");
|
|
||||||
}}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("MOVE_DOWN");
|
|
||||||
}}
|
|
||||||
onMouseUp={onStop}
|
|
||||||
onTouchEnd={onStop}
|
|
||||||
>
|
|
||||||
<FaAngleDown />
|
|
||||||
</TooltipButton>
|
|
||||||
<TooltipButton
|
|
||||||
label={t("ptz.move.right.label")}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("MOVE_RIGHT");
|
|
||||||
}}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("MOVE_RIGHT");
|
|
||||||
}}
|
|
||||||
onMouseUp={onStop}
|
|
||||||
onTouchEnd={onStop}
|
|
||||||
>
|
|
||||||
<FaAngleRight />
|
|
||||||
</TooltipButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{ptz?.features?.includes("zoom") && (
|
|
||||||
<>
|
|
||||||
<TooltipButton
|
|
||||||
label={t("ptz.zoom.in.label")}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("ZOOM_IN");
|
|
||||||
}}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("ZOOM_IN");
|
|
||||||
}}
|
|
||||||
onMouseUp={onStop}
|
|
||||||
onTouchEnd={onStop}
|
|
||||||
>
|
|
||||||
<MdZoomIn />
|
|
||||||
</TooltipButton>
|
|
||||||
<TooltipButton
|
|
||||||
label={t("ptz.zoom.out.label")}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("ZOOM_OUT");
|
|
||||||
}}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("ZOOM_OUT");
|
|
||||||
}}
|
|
||||||
onMouseUp={onStop}
|
|
||||||
onTouchEnd={onStop}
|
|
||||||
>
|
|
||||||
<MdZoomOut />
|
|
||||||
</TooltipButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{ptz?.features?.includes("focus") && (
|
|
||||||
<>
|
|
||||||
<TooltipButton
|
|
||||||
label={t("ptz.focus.in.label")}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("FOCUS_IN");
|
|
||||||
}}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("FOCUS_IN");
|
|
||||||
}}
|
|
||||||
onMouseUp={onStop}
|
|
||||||
onTouchEnd={onStop}
|
|
||||||
>
|
|
||||||
<MdCenterFocusStrong />
|
|
||||||
</TooltipButton>
|
|
||||||
<TooltipButton
|
|
||||||
label={t("ptz.focus.out.label")}
|
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("FOCUS_OUT");
|
|
||||||
}}
|
|
||||||
onTouchStart={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
sendPtz("FOCUS_OUT");
|
|
||||||
}}
|
|
||||||
onMouseUp={onStop}
|
|
||||||
onTouchEnd={onStop}
|
|
||||||
>
|
|
||||||
<MdCenterFocusWeak />
|
|
||||||
</TooltipButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ptz?.features?.includes("pt-r-fov") && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
className={`${clickOverlay ? "text-selected" : "text-primary"}`}
|
|
||||||
aria-label={t("ptz.move.clickMove.label")}
|
|
||||||
onClick={() => setClickOverlay(!clickOverlay)}
|
|
||||||
>
|
|
||||||
<TbViewfinder />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
{clickOverlay
|
|
||||||
? t("ptz.move.clickMove.disable")
|
|
||||||
: t("ptz.move.clickMove.enable")}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{(ptz?.presets?.length ?? 0) > 0 && (
|
|
||||||
<DropdownMenu modal={!isDesktop}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button aria-label={t("ptz.presets")}>
|
|
||||||
<BsThreeDotsVertical />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{t("ptz.presets")}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<DropdownMenuContent
|
|
||||||
className="scrollbar-container max-h-[40dvh] overflow-y-auto"
|
|
||||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
{ptz?.presets.map((preset) => (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={preset}
|
|
||||||
aria-label={preset}
|
|
||||||
className="cursor-pointer"
|
|
||||||
onSelect={() => sendPtz(`preset_${preset}`)}
|
|
||||||
>
|
|
||||||
{preset}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) {
|
function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) {
|
||||||
const { t } = useTranslation(["views/live", "views/events"]);
|
const { t } = useTranslation(["views/live", "views/events"]);
|
||||||
const rankMap = { all: 0, motion: 1, active_objects: 2 };
|
const rankMap = { all: 0, motion: 1, active_objects: 2 };
|
||||||
@ -1065,6 +745,8 @@ type FrigateCameraFeaturesProps = {
|
|||||||
supportsAudioOutput: boolean;
|
supportsAudioOutput: boolean;
|
||||||
supports2WayTalk: boolean;
|
supports2WayTalk: boolean;
|
||||||
cameraEnabled: boolean;
|
cameraEnabled: boolean;
|
||||||
|
debug: boolean;
|
||||||
|
setDebug: (debug: boolean) => void;
|
||||||
};
|
};
|
||||||
function FrigateCameraFeatures({
|
function FrigateCameraFeatures({
|
||||||
camera,
|
camera,
|
||||||
@ -1085,6 +767,8 @@ function FrigateCameraFeatures({
|
|||||||
supportsAudioOutput,
|
supportsAudioOutput,
|
||||||
supports2WayTalk,
|
supports2WayTalk,
|
||||||
cameraEnabled,
|
cameraEnabled,
|
||||||
|
debug,
|
||||||
|
setDebug,
|
||||||
}: FrigateCameraFeaturesProps) {
|
}: FrigateCameraFeaturesProps) {
|
||||||
const { t } = useTranslation(["views/live", "components/dialog"]);
|
const { t } = useTranslation(["views/live", "components/dialog"]);
|
||||||
const { getLocaleDocUrl } = useDocDomain();
|
const { getLocaleDocUrl } = useDocDomain();
|
||||||
@ -1196,10 +880,6 @@ function FrigateCameraFeatures({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// navigate for debug view
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
// desktop shows icons part of row
|
// desktop shows icons part of row
|
||||||
if (isDesktop || isTablet) {
|
if (isDesktop || isTablet) {
|
||||||
return (
|
return (
|
||||||
@ -1215,7 +895,7 @@ function FrigateCameraFeatures({
|
|||||||
enabledState == "ON" ? t("camera.disable") : t("camera.enable")
|
enabledState == "ON" ? t("camera.disable") : t("camera.enable")
|
||||||
}
|
}
|
||||||
onClick={() => sendEnabled(enabledState == "ON" ? "OFF" : "ON")}
|
onClick={() => sendEnabled(enabledState == "ON" ? "OFF" : "ON")}
|
||||||
disabled={false}
|
disabled={debug}
|
||||||
/>
|
/>
|
||||||
<CameraFeatureToggle
|
<CameraFeatureToggle
|
||||||
className="p-2 md:p-0"
|
className="p-2 md:p-0"
|
||||||
@ -1321,9 +1001,8 @@ function FrigateCameraFeatures({
|
|||||||
isActive={isRecording}
|
isActive={isRecording}
|
||||||
title={t("manualRecording." + (isRecording ? "stop" : "start"))}
|
title={t("manualRecording." + (isRecording ? "stop" : "start"))}
|
||||||
onClick={handleEventButtonClick}
|
onClick={handleEventButtonClick}
|
||||||
disabled={!cameraEnabled}
|
disabled={!cameraEnabled || debug}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<div
|
<div
|
||||||
@ -1387,6 +1066,7 @@ function FrigateCameraFeatures({
|
|||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={streamName}
|
value={streamName}
|
||||||
|
disabled={debug}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setStreamName?.(value);
|
setStreamName?.(value);
|
||||||
}}
|
}}
|
||||||
@ -1416,48 +1096,60 @@ function FrigateCameraFeatures({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{preferredLiveMode != "jsmpeg" && isRestreamed && (
|
{debug && (
|
||||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||||
{supportsAudioOutput ? (
|
<>
|
||||||
<>
|
<LuX className="size-8 text-danger" />
|
||||||
<LuCheck className="size-4 text-success" />
|
<div>{t("stream.debug.picker")}</div>
|
||||||
<div>{t("stream.audio.available")}</div>
|
</>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<LuX className="size-4 text-danger" />
|
|
||||||
<div>{t("stream.audio.unavailable")}</div>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<div className="cursor-pointer p-0">
|
|
||||||
<LuInfo className="size-4" />
|
|
||||||
<span className="sr-only">
|
|
||||||
{t("button.info", { ns: "common" })}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-80 text-xs">
|
|
||||||
{t("stream.audio.tips.title")}
|
|
||||||
<div className="mt-2 flex items-center text-primary">
|
|
||||||
<Link
|
|
||||||
to={getLocaleDocUrl("configuration/live")}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline"
|
|
||||||
>
|
|
||||||
{t("readTheDocumentation", {
|
|
||||||
ns: "common",
|
|
||||||
})}
|
|
||||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{preferredLiveMode != "jsmpeg" &&
|
{preferredLiveMode != "jsmpeg" &&
|
||||||
|
!debug &&
|
||||||
|
isRestreamed && (
|
||||||
|
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||||
|
{supportsAudioOutput ? (
|
||||||
|
<>
|
||||||
|
<LuCheck className="size-4 text-success" />
|
||||||
|
<div>{t("stream.audio.available")}</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<LuX className="size-4 text-danger" />
|
||||||
|
<div>{t("stream.audio.unavailable")}</div>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<div className="cursor-pointer p-0">
|
||||||
|
<LuInfo className="size-4" />
|
||||||
|
<span className="sr-only">
|
||||||
|
{t("button.info", { ns: "common" })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-80 text-xs">
|
||||||
|
{t("stream.audio.tips.title")}
|
||||||
|
<div className="mt-2 flex items-center text-primary">
|
||||||
|
<Link
|
||||||
|
to={getLocaleDocUrl("configuration/live")}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline"
|
||||||
|
>
|
||||||
|
{t("readTheDocumentation", {
|
||||||
|
ns: "common",
|
||||||
|
})}
|
||||||
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{preferredLiveMode != "jsmpeg" &&
|
||||||
|
!debug &&
|
||||||
isRestreamed &&
|
isRestreamed &&
|
||||||
supportsAudioOutput && (
|
supportsAudioOutput && (
|
||||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||||
@ -1503,29 +1195,31 @@ function FrigateCameraFeatures({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{preferredLiveMode == "jsmpeg" && isRestreamed && (
|
{preferredLiveMode == "jsmpeg" &&
|
||||||
<div className="flex flex-col items-center gap-3">
|
!debug &&
|
||||||
<div className="flex flex-row items-center gap-2">
|
isRestreamed && (
|
||||||
<IoIosWarning className="mr-1 size-8 text-danger" />
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<IoIosWarning className="mr-1 size-8 text-danger" />
|
||||||
|
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{t("stream.lowBandwidth.tips")}
|
{t("stream.lowBandwidth.tips")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className={`flex items-center gap-2.5 rounded-lg`}
|
|
||||||
aria-label={t("stream.lowBandwidth.resetStream")}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setLowBandwidth(false)}
|
|
||||||
>
|
|
||||||
<MdOutlineRestartAlt className="size-5 text-primary-variant" />
|
|
||||||
<div className="text-primary-variant">
|
|
||||||
{t("stream.lowBandwidth.resetStream")}
|
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
<Button
|
||||||
</div>
|
className={`flex items-center gap-2.5 rounded-lg`}
|
||||||
)}
|
aria-label={t("stream.lowBandwidth.resetStream")}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setLowBandwidth(false)}
|
||||||
|
>
|
||||||
|
<MdOutlineRestartAlt className="size-5 text-primary-variant" />
|
||||||
|
<div className="text-primary-variant">
|
||||||
|
{t("stream.lowBandwidth.resetStream")}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isRestreamed && (
|
{isRestreamed && (
|
||||||
@ -1540,6 +1234,7 @@ function FrigateCameraFeatures({
|
|||||||
<Switch
|
<Switch
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
id="backgroundplay"
|
id="backgroundplay"
|
||||||
|
disabled={debug}
|
||||||
checked={playInBackground}
|
checked={playInBackground}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
setPlayInBackground(checked)
|
setPlayInBackground(checked)
|
||||||
@ -1564,6 +1259,7 @@ function FrigateCameraFeatures({
|
|||||||
<Switch
|
<Switch
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
id="showstats"
|
id="showstats"
|
||||||
|
disabled={debug}
|
||||||
checked={showStats}
|
checked={showStats}
|
||||||
onCheckedChange={(checked) => setShowStats(checked)}
|
onCheckedChange={(checked) => setShowStats(checked)}
|
||||||
/>
|
/>
|
||||||
@ -1574,17 +1270,22 @@ function FrigateCameraFeatures({
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="flex flex-col gap-1">
|
||||||
className="flex cursor-pointer flex-col gap-1"
|
<div className="flex items-center justify-between">
|
||||||
onClick={() =>
|
<Label
|
||||||
navigate(`/settings?page=debug&camera=${camera.name}`)
|
className="mx-0 cursor-pointer text-primary"
|
||||||
}
|
htmlFor="debug"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between text-sm font-medium leading-none">
|
{t("streaming.debugView", {
|
||||||
{t("streaming.debugView", {
|
ns: "components/dialog",
|
||||||
ns: "components/dialog",
|
})}
|
||||||
})}
|
</Label>
|
||||||
<LuExternalLink className="ml-2 inline-flex size-5" />
|
<Switch
|
||||||
|
className="ml-1"
|
||||||
|
id="debug"
|
||||||
|
checked={debug}
|
||||||
|
onCheckedChange={(checked) => setDebug(checked)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1724,6 +1425,7 @@ function FrigateCameraFeatures({
|
|||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setStreamName?.(value);
|
setStreamName?.(value);
|
||||||
}}
|
}}
|
||||||
|
disabled={debug}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue>
|
<SelectValue>
|
||||||
@ -1749,7 +1451,17 @@ function FrigateCameraFeatures({
|
|||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{preferredLiveMode != "jsmpeg" && isRestreamed && (
|
|
||||||
|
{debug && (
|
||||||
|
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||||
|
<>
|
||||||
|
<LuX className="size-8 text-danger" />
|
||||||
|
<div>{t("stream.debug.picker")}</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{preferredLiveMode != "jsmpeg" && !debug && isRestreamed && (
|
||||||
<div className="mt-1 flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
<div className="mt-1 flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||||
{supportsAudioOutput ? (
|
{supportsAudioOutput ? (
|
||||||
<>
|
<>
|
||||||
@ -1789,6 +1501,7 @@ function FrigateCameraFeatures({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{preferredLiveMode != "jsmpeg" &&
|
{preferredLiveMode != "jsmpeg" &&
|
||||||
|
!debug &&
|
||||||
isRestreamed &&
|
isRestreamed &&
|
||||||
supportsAudioOutput && (
|
supportsAudioOutput && (
|
||||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||||
@ -1835,7 +1548,6 @@ function FrigateCameraFeatures({
|
|||||||
<div className="mt-2 flex flex-col items-center gap-3">
|
<div className="mt-2 flex flex-col items-center gap-3">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<IoIosWarning className="mr-1 size-8 text-danger" />
|
<IoIosWarning className="mr-1 size-8 text-danger" />
|
||||||
|
|
||||||
<p className="text-sm">{t("stream.lowBandwidth.tips")}</p>
|
<p className="text-sm">{t("stream.lowBandwidth.tips")}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@ -1843,6 +1555,7 @@ function FrigateCameraFeatures({
|
|||||||
aria-label={t("stream.lowBandwidth.resetStream")}
|
aria-label={t("stream.lowBandwidth.resetStream")}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
disabled={debug}
|
||||||
onClick={() => setLowBandwidth(false)}
|
onClick={() => setLowBandwidth(false)}
|
||||||
>
|
>
|
||||||
<MdOutlineRestartAlt className="size-5 text-primary-variant" />
|
<MdOutlineRestartAlt className="size-5 text-primary-variant" />
|
||||||
@ -1864,6 +1577,7 @@ function FrigateCameraFeatures({
|
|||||||
"w-full",
|
"w-full",
|
||||||
isRecording && "animate-pulse bg-red-500 hover:bg-red-600",
|
isRecording && "animate-pulse bg-red-500 hover:bg-red-600",
|
||||||
)}
|
)}
|
||||||
|
disabled={debug}
|
||||||
>
|
>
|
||||||
{t("manualRecording." + (isRecording ? "end" : "start"))}
|
{t("manualRecording." + (isRecording ? "end" : "start"))}
|
||||||
</Button>
|
</Button>
|
||||||
@ -1880,6 +1594,7 @@ function FrigateCameraFeatures({
|
|||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
setPlayInBackground(checked);
|
setPlayInBackground(checked);
|
||||||
}}
|
}}
|
||||||
|
disabled={debug}
|
||||||
/>
|
/>
|
||||||
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
|
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
|
||||||
{t("manualRecording.playInBackground.desc")}
|
{t("manualRecording.playInBackground.desc")}
|
||||||
@ -1892,6 +1607,7 @@ function FrigateCameraFeatures({
|
|||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
setShowStats(checked);
|
setShowStats(checked);
|
||||||
}}
|
}}
|
||||||
|
disabled={debug}
|
||||||
/>
|
/>
|
||||||
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
|
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
|
||||||
{t("manualRecording.showStats.desc")}
|
{t("manualRecording.showStats.desc")}
|
||||||
@ -1899,16 +1615,12 @@ function FrigateCameraFeatures({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="mb-3 flex flex-col gap-1 px-2">
|
<div className="mb-3 flex flex-col">
|
||||||
<div className="flex items-center justify-between text-sm font-medium leading-none">
|
<FilterSwitch
|
||||||
{t("manualRecording.debugView")}
|
label={t("streaming.debugView", { ns: "components/dialog" })}
|
||||||
<LuExternalLink
|
isChecked={debug}
|
||||||
onClick={() =>
|
onCheckedChange={(checked) => setDebug(checked)}
|
||||||
navigate(`/settings?page=debug&camera=${camera.name}`)
|
/>
|
||||||
}
|
|
||||||
className="ml-2 inline-flex size-5 cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user