Compare commits

..

No commits in common. "8b85cd816eb16d83cd572dda299f802c847b809a" and "9fdce80729743b106b5616199f02bbc8b46ab064" have entirely different histories.

6 changed files with 488 additions and 615 deletions

View File

@ -50,38 +50,6 @@ 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
@ -102,10 +70,6 @@ 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
@ -118,7 +82,5 @@ 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="${homekit_config_path}" -config=/dev/shm/go2rtc.yaml
exec "${binary_path}" -config=/dev/shm/go2rtc.yaml

View File

@ -3,13 +3,15 @@ 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.
@ -109,11 +111,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.
## Homekit Configuration
## Important considerations
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`.
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.

View File

@ -124,9 +124,6 @@
"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",

View File

@ -1,324 +0,0 @@
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>
);
}

View File

@ -1,52 +0,0 @@
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>
);
}

View File

@ -17,6 +17,7 @@ import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
@ -24,6 +25,11 @@ 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";
@ -32,8 +38,10 @@ import {
LiveStreamMetadata,
VideoResolutionType,
} from "@/types/live";
import { CameraPtzInfo } from "@/types/ptz";
import { RecordingStartingPoint } from "@/types/record";
import React, {
ReactNode,
useCallback,
useEffect,
useMemo,
@ -48,7 +56,12 @@ import {
isTablet,
useMobileOrientation,
} from "react-device-detect";
import { BsThreeDotsVertical } from "react-icons/bs";
import {
FaAngleDown,
FaAngleLeft,
FaAngleRight,
FaAngleUp,
FaCog,
FaCompress,
FaExpand,
@ -78,6 +91,8 @@ import {
LuX,
} from "react-icons/lu";
import {
MdCenterFocusStrong,
MdCenterFocusWeak,
MdClosedCaption,
MdClosedCaptionDisabled,
MdNoPhotography,
@ -85,6 +100,8 @@ 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";
@ -109,8 +126,6 @@ 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;
@ -272,7 +287,6 @@ export default function LiveCameraView({
);
const [showStats, setShowStats] = useState(false);
const [debug, setDebug] = useState(false);
const [fullResolution, setFullResolution] = useState<VideoResolutionType>({
width: 0,
@ -423,11 +437,7 @@ export default function LiveCameraView({
);
return (
<TransformWrapper
minScale={1.0}
wheel={{ smoothStep: 0.005 }}
disabled={debug}
>
<TransformWrapper minScale={1.0} wheel={{ smoothStep: 0.005 }}>
<Toaster position="top-center" closeButton={true} />
<div
ref={mainRef}
@ -513,7 +523,6 @@ export default function LiveCameraView({
variant={fullscreen ? "overlay" : "primary"}
Icon={fullscreen ? FaCompress : FaExpand}
isActive={fullscreen}
disabled={debug}
title={
fullscreen
? t("button.close", { ns: "common" })
@ -541,7 +550,7 @@ export default function LiveCameraView({
setPip(false);
}
}}
disabled={!cameraEnabled || debug}
disabled={!cameraEnabled}
/>
)}
{supports2WayTalk && (
@ -561,7 +570,7 @@ export default function LiveCameraView({
setAudio(true);
}
}}
disabled={!cameraEnabled || debug}
disabled={!cameraEnabled}
/>
)}
{supportsAudioOutput && preferredLiveMode != "jsmpeg" && (
@ -576,7 +585,7 @@ export default function LiveCameraView({
: t("cameraAudio.enable", { ns: "views/live" })
}
onClick={() => setAudio(!audio)}
disabled={!cameraEnabled || debug}
disabled={!cameraEnabled}
/>
)}
<FrigateCameraFeatures
@ -600,66 +609,10 @@ export default function LiveCameraView({
supportsAudioOutput={supportsAudioOutput}
supports2WayTalk={supports2WayTalk}
cameraEnabled={cameraEnabled}
debug={debug}
setDebug={setDebug}
/>
</div>
</div>
{!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>
) : (
<div id="player-container" className="size-full" ref={containerRef}>
<TransformComponent
wrapperStyle={{
width: "100%",
@ -669,16 +622,53 @@ export default function LiveCameraView({
position: "relative",
width: "100%",
height: "100%",
padding: "8px",
}}
>
<ObjectSettingsView selectedCamera={camera.name} />
<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>
</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}
@ -690,6 +680,336 @@ 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 };
@ -745,8 +1065,6 @@ type FrigateCameraFeaturesProps = {
supportsAudioOutput: boolean;
supports2WayTalk: boolean;
cameraEnabled: boolean;
debug: boolean;
setDebug: (debug: boolean) => void;
};
function FrigateCameraFeatures({
camera,
@ -767,8 +1085,6 @@ function FrigateCameraFeatures({
supportsAudioOutput,
supports2WayTalk,
cameraEnabled,
debug,
setDebug,
}: FrigateCameraFeaturesProps) {
const { t } = useTranslation(["views/live", "components/dialog"]);
const { getLocaleDocUrl } = useDocDomain();
@ -880,6 +1196,10 @@ 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 (
@ -895,7 +1215,7 @@ function FrigateCameraFeatures({
enabledState == "ON" ? t("camera.disable") : t("camera.enable")
}
onClick={() => sendEnabled(enabledState == "ON" ? "OFF" : "ON")}
disabled={debug}
disabled={false}
/>
<CameraFeatureToggle
className="p-2 md:p-0"
@ -1001,8 +1321,9 @@ function FrigateCameraFeatures({
isActive={isRecording}
title={t("manualRecording." + (isRecording ? "stop" : "start"))}
onClick={handleEventButtonClick}
disabled={!cameraEnabled || debug}
disabled={!cameraEnabled}
/>
<DropdownMenu modal={false}>
<DropdownMenuTrigger>
<div
@ -1066,7 +1387,6 @@ function FrigateCameraFeatures({
</Label>
<Select
value={streamName}
disabled={debug}
onValueChange={(value) => {
setStreamName?.(value);
}}
@ -1096,60 +1416,48 @@ function FrigateCameraFeatures({
</SelectContent>
</Select>
{debug && (
{preferredLiveMode != "jsmpeg" && isRestreamed && (
<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>
</>
{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 && (
<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">
@ -1195,31 +1503,29 @@ function FrigateCameraFeatures({
</div>
)}
{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" />
{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" />
<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")}
</div>
</Button>
<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")}
</div>
</Button>
</div>
)}
</div>
)}
{isRestreamed && (
@ -1234,7 +1540,6 @@ function FrigateCameraFeatures({
<Switch
className="ml-1"
id="backgroundplay"
disabled={debug}
checked={playInBackground}
onCheckedChange={(checked) =>
setPlayInBackground(checked)
@ -1259,7 +1564,6 @@ function FrigateCameraFeatures({
<Switch
className="ml-1"
id="showstats"
disabled={debug}
checked={showStats}
onCheckedChange={(checked) => setShowStats(checked)}
/>
@ -1270,22 +1574,17 @@ function FrigateCameraFeatures({
})}
</p>
</div>
<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
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>
</div>
</div>
@ -1425,7 +1724,6 @@ function FrigateCameraFeatures({
onValueChange={(value) => {
setStreamName?.(value);
}}
disabled={debug}
>
<SelectTrigger className="w-full">
<SelectValue>
@ -1451,17 +1749,7 @@ function FrigateCameraFeatures({
</SelectGroup>
</SelectContent>
</Select>
{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 && (
{preferredLiveMode != "jsmpeg" && isRestreamed && (
<div className="mt-1 flex flex-row items-center gap-1 text-sm text-muted-foreground">
{supportsAudioOutput ? (
<>
@ -1501,7 +1789,6 @@ function FrigateCameraFeatures({
</div>
)}
{preferredLiveMode != "jsmpeg" &&
!debug &&
isRestreamed &&
supportsAudioOutput && (
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
@ -1548,6 +1835,7 @@ 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
@ -1555,7 +1843,6 @@ function FrigateCameraFeatures({
aria-label={t("stream.lowBandwidth.resetStream")}
variant="outline"
size="sm"
disabled={debug}
onClick={() => setLowBandwidth(false)}
>
<MdOutlineRestartAlt className="size-5 text-primary-variant" />
@ -1577,7 +1864,6 @@ function FrigateCameraFeatures({
"w-full",
isRecording && "animate-pulse bg-red-500 hover:bg-red-600",
)}
disabled={debug}
>
{t("manualRecording." + (isRecording ? "end" : "start"))}
</Button>
@ -1594,7 +1880,6 @@ function FrigateCameraFeatures({
onCheckedChange={(checked) => {
setPlayInBackground(checked);
}}
disabled={debug}
/>
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
{t("manualRecording.playInBackground.desc")}
@ -1607,7 +1892,6 @@ function FrigateCameraFeatures({
onCheckedChange={(checked) => {
setShowStats(checked);
}}
disabled={debug}
/>
<p className="mx-2 -mt-2 text-sm text-muted-foreground">
{t("manualRecording.showStats.desc")}
@ -1615,12 +1899,16 @@ function FrigateCameraFeatures({
</div>
</>
)}
<div className="mb-3 flex flex-col">
<FilterSwitch
label={t("streaming.debugView", { ns: "components/dialog" })}
isChecked={debug}
onCheckedChange={(checked) => setDebug(checked)}
/>
<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>
</div>
</DrawerContent>