mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-01-25 13:38:29 +03:00
Compare commits
3 Commits
9fdce80729
...
8b85cd816e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b85cd816e | ||
|
|
bebe99d9b8 | ||
|
|
a08fda62f8 |
@ -50,6 +50,38 @@ function set_libva_version() {
|
||||
export LIBAVFORMAT_VERSION_MAJOR
|
||||
}
|
||||
|
||||
function setup_homekit_config() {
|
||||
local config_path="$1"
|
||||
|
||||
if [[ ! -f "${config_path}" ]]; then
|
||||
echo "[INFO] Creating empty HomeKit config file..."
|
||||
echo '{}' > "${config_path}"
|
||||
fi
|
||||
|
||||
# Convert YAML to JSON for jq processing
|
||||
local temp_json="/tmp/cache/homekit_config.json"
|
||||
yq eval -o=json "${config_path}" > "${temp_json}" 2>/dev/null || {
|
||||
echo "[WARNING] Failed to convert HomeKit config to JSON, skipping cleanup"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Use jq to filter and keep only the homekit section
|
||||
local cleaned_json="/tmp/cache/homekit_cleaned.json"
|
||||
jq '
|
||||
# Keep only the homekit section if it exists, otherwise empty object
|
||||
if has("homekit") then {homekit: .homekit} else {homekit: {}} end
|
||||
' "${temp_json}" > "${cleaned_json}" 2>/dev/null || echo '{"homekit": {}}' > "${cleaned_json}"
|
||||
|
||||
# Convert back to YAML and write to the config file
|
||||
yq eval -P "${cleaned_json}" > "${config_path}" 2>/dev/null || {
|
||||
echo "[WARNING] Failed to convert cleaned config to YAML, creating minimal config"
|
||||
echo '{"homekit": {}}' > "${config_path}"
|
||||
}
|
||||
|
||||
# Clean up temp files
|
||||
rm -f "${temp_json}" "${cleaned_json}"
|
||||
}
|
||||
|
||||
set_libva_version
|
||||
|
||||
if [[ -f "/dev/shm/go2rtc.yaml" ]]; then
|
||||
@ -70,6 +102,10 @@ else
|
||||
echo "[WARNING] Unable to remove existing go2rtc config. Changes made to your frigate config file may not be recognized. Please remove the /dev/shm/go2rtc.yaml from your docker host manually."
|
||||
fi
|
||||
|
||||
# HomeKit configuration persistence setup
|
||||
readonly homekit_config_path="/config/go2rtc_homekit.yml"
|
||||
setup_homekit_config "${homekit_config_path}"
|
||||
|
||||
readonly config_path="/config"
|
||||
|
||||
if [[ -x "${config_path}/go2rtc" ]]; then
|
||||
@ -82,5 +118,7 @@ fi
|
||||
echo "[INFO] Starting go2rtc..."
|
||||
|
||||
# Replace the bash process with the go2rtc process, redirecting stderr to stdout
|
||||
# Use HomeKit config as the primary config so writebacks go there
|
||||
# The main config from Frigate will be loaded as a secondary config
|
||||
exec 2>&1
|
||||
exec "${binary_path}" -config=/dev/shm/go2rtc.yaml
|
||||
exec "${binary_path}" -config="${homekit_config_path}" -config=/dev/shm/go2rtc.yaml
|
||||
|
||||
@ -3,15 +3,13 @@ id: configuring_go2rtc
|
||||
title: Configuring go2rtc
|
||||
---
|
||||
|
||||
# Configuring go2rtc
|
||||
|
||||
Use of the bundled go2rtc is optional. You can still configure FFmpeg to connect directly to your cameras. However, adding go2rtc to your configuration is required for the following features:
|
||||
|
||||
- WebRTC or MSE for live viewing with audio, higher resolutions and frame rates than the jsmpeg stream which is limited to the detect stream and does not support audio
|
||||
- Live stream support for cameras in Home Assistant Integration
|
||||
- RTSP relay for use with other consumers to reduce the number of connections to your camera streams
|
||||
|
||||
# Setup a go2rtc stream
|
||||
## Setup a go2rtc stream
|
||||
|
||||
First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#module-streams), not just rtsp.
|
||||
|
||||
@ -111,11 +109,11 @@ section.
|
||||
|
||||
:::
|
||||
|
||||
## Next steps
|
||||
### Next steps
|
||||
|
||||
1. If the stream you added to go2rtc is also used by Frigate for the `record` or `detect` role, you can migrate your config to pull from the RTSP restream to reduce the number of connections to your camera as shown [here](/configuration/restream#reduce-connections-to-camera).
|
||||
2. You can [set up WebRTC](/configuration/live#webrtc-extra-configuration) if your camera supports two-way talk. Note that WebRTC only supports specific audio formats and may require opening ports on your router.
|
||||
|
||||
## Important considerations
|
||||
## Homekit Configuration
|
||||
|
||||
If you are configuring go2rtc to publish HomeKit camera streams, on pairing the configuration is written to the `/dev/shm/go2rtc.yaml` file inside the container. These changes must be manually copied across to the `go2rtc` section of your Frigate configuration in order to persist through restarts.
|
||||
To add camera streams to Homekit Frigate must be configured in docker to use `host` networking mode. Once that is done, you can use the go2rtc WebUI (accessed via port 1984, which is disabled by default) to share export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`.
|
||||
@ -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",
|
||||
|
||||
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 {
|
||||
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<VideoResolutionType>({
|
||||
width: 0,
|
||||
@ -437,7 +423,11 @@ export default function LiveCameraView({
|
||||
);
|
||||
|
||||
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} />
|
||||
<div
|
||||
ref={mainRef}
|
||||
@ -523,6 +513,7 @@ export default function LiveCameraView({
|
||||
variant={fullscreen ? "overlay" : "primary"}
|
||||
Icon={fullscreen ? FaCompress : FaExpand}
|
||||
isActive={fullscreen}
|
||||
disabled={debug}
|
||||
title={
|
||||
fullscreen
|
||||
? t("button.close", { ns: "common" })
|
||||
@ -550,7 +541,7 @@ export default function LiveCameraView({
|
||||
setPip(false);
|
||||
}
|
||||
}}
|
||||
disabled={!cameraEnabled}
|
||||
disabled={!cameraEnabled || debug}
|
||||
/>
|
||||
)}
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
<FrigateCameraFeatures
|
||||
@ -609,10 +600,66 @@ export default function LiveCameraView({
|
||||
supportsAudioOutput={supportsAudioOutput}
|
||||
supports2WayTalk={supports2WayTalk}
|
||||
cameraEnabled={cameraEnabled}
|
||||
debug={debug}
|
||||
setDebug={setDebug}
|
||||
/>
|
||||
</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
|
||||
wrapperStyle={{
|
||||
width: "100%",
|
||||
@ -622,53 +669,16 @@ export default function LiveCameraView({
|
||||
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>
|
||||
<ObjectSettingsView selectedCamera={camera.name} />
|
||||
</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>
|
||||
{camera.onvif.host != "" && (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<PtzControlPanel
|
||||
className={debug && isMobile ? "bottom-auto top-[25%]" : ""}
|
||||
camera={camera.name}
|
||||
enabled={cameraEnabled}
|
||||
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 }) {
|
||||
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}
|
||||
/>
|
||||
<CameraFeatureToggle
|
||||
className="p-2 md:p-0"
|
||||
@ -1321,9 +1001,8 @@ function FrigateCameraFeatures({
|
||||
isActive={isRecording}
|
||||
title={t("manualRecording." + (isRecording ? "stop" : "start"))}
|
||||
onClick={handleEventButtonClick}
|
||||
disabled={!cameraEnabled}
|
||||
disabled={!cameraEnabled || debug}
|
||||
/>
|
||||
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger>
|
||||
<div
|
||||
@ -1387,6 +1066,7 @@ function FrigateCameraFeatures({
|
||||
</Label>
|
||||
<Select
|
||||
value={streamName}
|
||||
disabled={debug}
|
||||
onValueChange={(value) => {
|
||||
setStreamName?.(value);
|
||||
}}
|
||||
@ -1416,48 +1096,60 @@ function FrigateCameraFeatures({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{preferredLiveMode != "jsmpeg" && isRestreamed && (
|
||||
{debug && (
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<LuX className="size-8 text-danger" />
|
||||
<div>{t("stream.debug.picker")}</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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 &&
|
||||
supportsAudioOutput && (
|
||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||
@ -1503,29 +1195,31 @@ function FrigateCameraFeatures({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{preferredLiveMode == "jsmpeg" && isRestreamed && (
|
||||
<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" />
|
||||
{preferredLiveMode == "jsmpeg" &&
|
||||
!debug &&
|
||||
isRestreamed && (
|
||||
<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">
|
||||
{t("stream.lowBandwidth.tips")}
|
||||
</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")}
|
||||
<p className="text-sm">
|
||||
{t("stream.lowBandwidth.tips")}
|
||||
</p>
|
||||
</div>
|
||||
</Button>
|
||||
</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>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isRestreamed && (
|
||||
@ -1540,6 +1234,7 @@ function FrigateCameraFeatures({
|
||||
<Switch
|
||||
className="ml-1"
|
||||
id="backgroundplay"
|
||||
disabled={debug}
|
||||
checked={playInBackground}
|
||||
onCheckedChange={(checked) =>
|
||||
setPlayInBackground(checked)
|
||||
@ -1564,6 +1259,7 @@ function FrigateCameraFeatures({
|
||||
<Switch
|
||||
className="ml-1"
|
||||
id="showstats"
|
||||
disabled={debug}
|
||||
checked={showStats}
|
||||
onCheckedChange={(checked) => setShowStats(checked)}
|
||||
/>
|
||||
@ -1574,17 +1270,22 @@ function FrigateCameraFeatures({
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="flex cursor-pointer flex-col gap-1"
|
||||
onClick={() =>
|
||||
navigate(`/settings?page=debug&camera=${camera.name}`)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center justify-between text-sm font-medium leading-none">
|
||||
{t("streaming.debugView", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
<LuExternalLink className="ml-2 inline-flex size-5" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label
|
||||
className="mx-0 cursor-pointer text-primary"
|
||||
htmlFor="debug"
|
||||
>
|
||||
{t("streaming.debugView", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
</Label>
|
||||
<Switch
|
||||
className="ml-1"
|
||||
id="debug"
|
||||
checked={debug}
|
||||
onCheckedChange={(checked) => setDebug(checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1724,6 +1425,7 @@ function FrigateCameraFeatures({
|
||||
onValueChange={(value) => {
|
||||
setStreamName?.(value);
|
||||
}}
|
||||
disabled={debug}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue>
|
||||
@ -1749,7 +1451,17 @@ function FrigateCameraFeatures({
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</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">
|
||||
{supportsAudioOutput ? (
|
||||
<>
|
||||
@ -1789,6 +1501,7 @@ function FrigateCameraFeatures({
|
||||
</div>
|
||||
)}
|
||||
{preferredLiveMode != "jsmpeg" &&
|
||||
!debug &&
|
||||
isRestreamed &&
|
||||
supportsAudioOutput && (
|
||||
<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="flex flex-row items-center gap-2">
|
||||
<IoIosWarning className="mr-1 size-8 text-danger" />
|
||||
|
||||
<p className="text-sm">{t("stream.lowBandwidth.tips")}</p>
|
||||
</div>
|
||||
<Button
|
||||
@ -1843,6 +1555,7 @@ function FrigateCameraFeatures({
|
||||
aria-label={t("stream.lowBandwidth.resetStream")}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={debug}
|
||||
onClick={() => setLowBandwidth(false)}
|
||||
>
|
||||
<MdOutlineRestartAlt className="size-5 text-primary-variant" />
|
||||
@ -1864,6 +1577,7 @@ function FrigateCameraFeatures({
|
||||
"w-full",
|
||||
isRecording && "animate-pulse bg-red-500 hover:bg-red-600",
|
||||
)}
|
||||
disabled={debug}
|
||||
>
|
||||
{t("manualRecording." + (isRecording ? "end" : "start"))}
|
||||
</Button>
|
||||
@ -1880,6 +1594,7 @@ function FrigateCameraFeatures({
|
||||
onCheckedChange={(checked) => {
|
||||
setPlayInBackground(checked);
|
||||
}}
|
||||
disabled={debug}
|
||||
/>
|
||||
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
|
||||
{t("manualRecording.playInBackground.desc")}
|
||||
@ -1892,6 +1607,7 @@ function FrigateCameraFeatures({
|
||||
onCheckedChange={(checked) => {
|
||||
setShowStats(checked);
|
||||
}}
|
||||
disabled={debug}
|
||||
/>
|
||||
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
|
||||
{t("manualRecording.showStats.desc")}
|
||||
@ -1899,16 +1615,12 @@ function FrigateCameraFeatures({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="mb-3 flex flex-col gap-1 px-2">
|
||||
<div className="flex items-center justify-between text-sm font-medium leading-none">
|
||||
{t("manualRecording.debugView")}
|
||||
<LuExternalLink
|
||||
onClick={() =>
|
||||
navigate(`/settings?page=debug&camera=${camera.name}`)
|
||||
}
|
||||
className="ml-2 inline-flex size-5 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex flex-col">
|
||||
<FilterSwitch
|
||||
label={t("streaming.debugView", { ns: "components/dialog" })}
|
||||
isChecked={debug}
|
||||
onCheckedChange={(checked) => setDebug(checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user