mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 05:24:11 +03:00
Miscellaneous fixes (#20875)
* Improve stream fetching logic * Reduce need to revalidate stream info * fix frigate+ frame submission * add UI setting to configure jsmpeg fallback timeout * hide settings dropdown when fullscreen * Fix arcface running on OpenVINO --------- Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
parent
a623150811
commit
f1a05d0f9b
@ -255,6 +255,7 @@ class OpenVINOModelRunner(BaseModelRunner):
|
|||||||
def __init__(self, model_path: str, device: str, model_type: str, **kwargs):
|
def __init__(self, model_path: str, device: str, model_type: str, **kwargs):
|
||||||
self.model_path = model_path
|
self.model_path = model_path
|
||||||
self.device = device
|
self.device = device
|
||||||
|
self.model_type = model_type
|
||||||
|
|
||||||
if device == "NPU" and not OpenVINOModelRunner.is_model_npu_supported(
|
if device == "NPU" and not OpenVINOModelRunner.is_model_npu_supported(
|
||||||
model_type
|
model_type
|
||||||
@ -341,6 +342,13 @@ class OpenVINOModelRunner(BaseModelRunner):
|
|||||||
# Lock prevents concurrent access to infer_request
|
# Lock prevents concurrent access to infer_request
|
||||||
# Needed for JinaV2: genai thread (text) + embeddings thread (vision)
|
# Needed for JinaV2: genai thread (text) + embeddings thread (vision)
|
||||||
with self._inference_lock:
|
with self._inference_lock:
|
||||||
|
from frigate.embeddings.types import EnrichmentModelTypeEnum
|
||||||
|
|
||||||
|
if self.model_type in [EnrichmentModelTypeEnum.arcface.value]:
|
||||||
|
# For face recognition models, create a fresh infer_request
|
||||||
|
# for each inference to avoid state pollution that causes incorrect results.
|
||||||
|
self.infer_request = self.compiled_model.create_infer_request()
|
||||||
|
|
||||||
# Handle single input case for backward compatibility
|
# Handle single input case for backward compatibility
|
||||||
if (
|
if (
|
||||||
len(inputs) == 1
|
len(inputs) == 1
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
"masksAndZones": "Mask and Zone Editor - Frigate",
|
"masksAndZones": "Mask and Zone Editor - Frigate",
|
||||||
"motionTuner": "Motion Tuner - Frigate",
|
"motionTuner": "Motion Tuner - Frigate",
|
||||||
"object": "Debug - Frigate",
|
"object": "Debug - Frigate",
|
||||||
"general": "General Settings - Frigate",
|
"general": "UI Settings - Frigate",
|
||||||
"frigatePlus": "Frigate+ Settings - Frigate",
|
"frigatePlus": "Frigate+ Settings - Frigate",
|
||||||
"notifications": "Notification Settings - Frigate"
|
"notifications": "Notification Settings - Frigate"
|
||||||
},
|
},
|
||||||
@ -37,7 +37,7 @@
|
|||||||
"noCamera": "No Camera"
|
"noCamera": "No Camera"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"title": "General Settings",
|
"title": "UI Settings",
|
||||||
"liveDashboard": {
|
"liveDashboard": {
|
||||||
"title": "Live Dashboard",
|
"title": "Live Dashboard",
|
||||||
"automaticLiveView": {
|
"automaticLiveView": {
|
||||||
@ -51,6 +51,10 @@
|
|||||||
"displayCameraNames": {
|
"displayCameraNames": {
|
||||||
"label": "Always Show Camera Names",
|
"label": "Always Show Camera Names",
|
||||||
"desc": "Always show the camera names in a chip in the multi-camera live view dashboard."
|
"desc": "Always show the camera names in a chip in the multi-camera live view dashboard."
|
||||||
|
},
|
||||||
|
"liveFallbackTimeout": {
|
||||||
|
"label": "Live Player Fallback Timeout",
|
||||||
|
"desc": "When a camera's high quality live stream is unavailable, fall back to low bandwidth mode after this many seconds. Default: 3."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"storedLayouts": {
|
"storedLayouts": {
|
||||||
|
|||||||
@ -343,6 +343,10 @@ export function TrackingDetails({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [displayedRecordTime]);
|
}, [displayedRecordTime]);
|
||||||
|
|
||||||
|
const onUploadFrameToPlus = useCallback(() => {
|
||||||
|
return axios.post(`/${event.camera}/plus/${currentTime}`);
|
||||||
|
}, [event.camera, currentTime]);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
@ -388,6 +392,7 @@ export function TrackingDetails({
|
|||||||
frigateControls={true}
|
frigateControls={true}
|
||||||
onTimeUpdate={handleTimeUpdate}
|
onTimeUpdate={handleTimeUpdate}
|
||||||
onSeekToTime={handleSeekToTime}
|
onSeekToTime={handleSeekToTime}
|
||||||
|
onUploadFrame={onUploadFrameToPlus}
|
||||||
isDetailMode={true}
|
isDetailMode={true}
|
||||||
camera={event.camera}
|
camera={event.camera}
|
||||||
currentTimeOverride={currentTime}
|
currentTimeOverride={currentTime}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
|
import { usePersistence } from "@/hooks/use-persistence";
|
||||||
import {
|
import {
|
||||||
LivePlayerError,
|
LivePlayerError,
|
||||||
PlayerStatsType,
|
PlayerStatsType,
|
||||||
@ -71,6 +72,8 @@ function MSEPlayer({
|
|||||||
const [errorCount, setErrorCount] = useState<number>(0);
|
const [errorCount, setErrorCount] = useState<number>(0);
|
||||||
const totalBytesLoaded = useRef(0);
|
const totalBytesLoaded = useRef(0);
|
||||||
|
|
||||||
|
const [fallbackTimeout] = usePersistence<number>("liveFallbackTimeout", 3);
|
||||||
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
const reconnectTIDRef = useRef<number | null>(null);
|
const reconnectTIDRef = useRef<number | null>(null);
|
||||||
@ -475,7 +478,10 @@ function MSEPlayer({
|
|||||||
setBufferTimeout(undefined);
|
setBufferTimeout(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeoutDuration = bufferTime == 0 ? 5000 : 3000;
|
const timeoutDuration =
|
||||||
|
bufferTime == 0
|
||||||
|
? (fallbackTimeout ?? 3) * 2 * 1000
|
||||||
|
: (fallbackTimeout ?? 3) * 1000;
|
||||||
setBufferTimeout(
|
setBufferTimeout(
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (
|
if (
|
||||||
@ -500,6 +506,7 @@ function MSEPlayer({
|
|||||||
onError,
|
onError,
|
||||||
onPlaying,
|
onPlaying,
|
||||||
playbackEnabled,
|
playbackEnabled,
|
||||||
|
fallbackTimeout,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { LivePlayerMode, LiveStreamMetadata } from "@/types/live";
|
|||||||
export default function useCameraLiveMode(
|
export default function useCameraLiveMode(
|
||||||
cameras: CameraConfig[],
|
cameras: CameraConfig[],
|
||||||
windowVisible: boolean,
|
windowVisible: boolean,
|
||||||
|
activeStreams?: { [cameraName: string]: string },
|
||||||
) {
|
) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
@ -20,16 +21,20 @@ export default function useCameraLiveMode(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isRestreamed) {
|
if (isRestreamed) {
|
||||||
Object.values(camera.live.streams).forEach((streamName) => {
|
if (activeStreams && activeStreams[camera.name]) {
|
||||||
streamNames.add(streamName);
|
streamNames.add(activeStreams[camera.name]);
|
||||||
});
|
} else {
|
||||||
|
Object.values(camera.live.streams).forEach((streamName) => {
|
||||||
|
streamNames.add(streamName);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return streamNames.size > 0
|
return streamNames.size > 0
|
||||||
? Array.from(streamNames).sort().join(",")
|
? Array.from(streamNames).sort().join(",")
|
||||||
: null;
|
: null;
|
||||||
}, [cameras, config]);
|
}, [cameras, config, activeStreams]);
|
||||||
|
|
||||||
const streamsFetcher = useCallback(async (key: string) => {
|
const streamsFetcher = useCallback(async (key: string) => {
|
||||||
const streamNames = key.split(",");
|
const streamNames = key.split(",");
|
||||||
@ -68,7 +73,9 @@ export default function useCameraLiveMode(
|
|||||||
[key: string]: LiveStreamMetadata;
|
[key: string]: LiveStreamMetadata;
|
||||||
}>(restreamedStreamsKey, streamsFetcher, {
|
}>(restreamedStreamsKey, streamsFetcher, {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
dedupingInterval: 10000,
|
revalidateOnReconnect: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
|
dedupingInterval: 60000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [preferredLiveModes, setPreferredLiveModes] = useState<{
|
const [preferredLiveModes, setPreferredLiveModes] = useState<{
|
||||||
|
|||||||
@ -86,14 +86,6 @@ export default function DraggableGridLayout({
|
|||||||
|
|
||||||
// preferred live modes per camera
|
// preferred live modes per camera
|
||||||
|
|
||||||
const {
|
|
||||||
preferredLiveModes,
|
|
||||||
setPreferredLiveModes,
|
|
||||||
resetPreferredLiveMode,
|
|
||||||
isRestreamedStates,
|
|
||||||
supportsAudioOutputStates,
|
|
||||||
} = useCameraLiveMode(cameras, windowVisible);
|
|
||||||
|
|
||||||
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
||||||
const [displayCameraNames] = usePersistence("displayCameraNames", false);
|
const [displayCameraNames] = usePersistence("displayCameraNames", false);
|
||||||
|
|
||||||
@ -106,6 +98,33 @@ export default function DraggableGridLayout({
|
|||||||
}
|
}
|
||||||
}, [allGroupsStreamingSettings, cameraGroup]);
|
}, [allGroupsStreamingSettings, cameraGroup]);
|
||||||
|
|
||||||
|
const activeStreams = useMemo(() => {
|
||||||
|
const streams: { [cameraName: string]: string } = {};
|
||||||
|
cameras.forEach((camera) => {
|
||||||
|
const availableStreams = camera.live.streams || {};
|
||||||
|
const streamNameFromSettings =
|
||||||
|
currentGroupStreamingSettings?.[camera.name]?.streamName || "";
|
||||||
|
const streamExists =
|
||||||
|
streamNameFromSettings &&
|
||||||
|
Object.values(availableStreams).includes(streamNameFromSettings);
|
||||||
|
|
||||||
|
const streamName = streamExists
|
||||||
|
? streamNameFromSettings
|
||||||
|
: Object.values(availableStreams)[0] || "";
|
||||||
|
|
||||||
|
streams[camera.name] = streamName;
|
||||||
|
});
|
||||||
|
return streams;
|
||||||
|
}, [cameras, currentGroupStreamingSettings]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
preferredLiveModes,
|
||||||
|
setPreferredLiveModes,
|
||||||
|
resetPreferredLiveMode,
|
||||||
|
isRestreamedStates,
|
||||||
|
supportsAudioOutputStates,
|
||||||
|
} = useCameraLiveMode(cameras, windowVisible, activeStreams);
|
||||||
|
|
||||||
// grid layout
|
// grid layout
|
||||||
|
|
||||||
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
|
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []);
|
||||||
|
|||||||
@ -162,6 +162,9 @@ export default function LiveCameraView({
|
|||||||
isRestreamed ? `go2rtc/streams/${streamName}` : null,
|
isRestreamed ? `go2rtc/streams/${streamName}` : null,
|
||||||
{
|
{
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
|
dedupingInterval: 60000,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1027,294 +1030,298 @@ function FrigateCameraFeatures({
|
|||||||
disabled={!cameraEnabled || debug || isSnapshotLoading}
|
disabled={!cameraEnabled || debug || isSnapshotLoading}
|
||||||
loading={isSnapshotLoading}
|
loading={isSnapshotLoading}
|
||||||
/>
|
/>
|
||||||
<DropdownMenu modal={false}>
|
{!fullscreen && (
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenu modal={false}>
|
||||||
<div
|
<DropdownMenuTrigger>
|
||||||
className={cn(
|
<div
|
||||||
"flex flex-col items-center justify-center rounded-lg bg-secondary p-2 text-secondary-foreground md:p-0",
|
className={cn(
|
||||||
)}
|
"flex flex-col items-center justify-center rounded-lg bg-secondary p-2 text-secondary-foreground md:p-0",
|
||||||
>
|
)}
|
||||||
<FaCog
|
>
|
||||||
className={`text-secondary-foreground" size-5 md:m-[6px]`}
|
<FaCog
|
||||||
/>
|
className={`text-secondary-foreground" size-5 md:m-[6px]`}
|
||||||
</div>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</div>
|
||||||
<DropdownMenuContent className="max-w-96">
|
</DropdownMenuTrigger>
|
||||||
<div className="flex flex-col gap-5 p-4">
|
<DropdownMenuContent className="max-w-96">
|
||||||
{!isRestreamed && (
|
<div className="flex flex-col gap-5 p-4">
|
||||||
<div className="flex flex-col gap-2">
|
{!isRestreamed && (
|
||||||
<Label>
|
<div className="flex flex-col gap-2">
|
||||||
{t("streaming.label", { ns: "components/dialog" })}
|
<Label>
|
||||||
</Label>
|
{t("streaming.label", { ns: "components/dialog" })}
|
||||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
</Label>
|
||||||
<LuX className="size-4 text-danger" />
|
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||||
<div>
|
<LuX className="size-4 text-danger" />
|
||||||
{t("streaming.restreaming.disabled", {
|
<div>
|
||||||
ns: "components/dialog",
|
{t("streaming.restreaming.disabled", {
|
||||||
})}
|
|
||||||
</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("streaming.restreaming.desc.title", {
|
|
||||||
ns: "components/dialog",
|
ns: "components/dialog",
|
||||||
})}
|
})}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
{isRestreamed &&
|
|
||||||
Object.values(camera.live.streams).length > 0 && (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<Label htmlFor="streaming-method">
|
|
||||||
{t("stream.title")}
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={streamName}
|
|
||||||
disabled={debug}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setStreamName?.(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue>
|
|
||||||
{Object.keys(camera.live.streams).find(
|
|
||||||
(key) => camera.live.streams[key] === streamName,
|
|
||||||
)}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
|
||||||
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
{Object.entries(camera.live.streams).map(
|
|
||||||
([stream, name]) => (
|
|
||||||
<SelectItem
|
|
||||||
key={stream}
|
|
||||||
className="cursor-pointer"
|
|
||||||
value={name}
|
|
||||||
>
|
|
||||||
{stream}
|
|
||||||
</SelectItem>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</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>
|
</div>
|
||||||
)}
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
{preferredLiveMode != "jsmpeg" &&
|
<div className="cursor-pointer p-0">
|
||||||
!debug &&
|
<LuInfo className="size-4" />
|
||||||
isRestreamed && (
|
<span className="sr-only">
|
||||||
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
{t("button.info", { ns: "common" })}
|
||||||
{supportsAudioOutput ? (
|
</span>
|
||||||
<>
|
|
||||||
<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">
|
|
||||||
{supports2WayTalk ? (
|
|
||||||
<>
|
|
||||||
<LuCheck className="size-4 text-success" />
|
|
||||||
<div>{t("stream.twoWayTalk.available")}</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<LuX className="size-4 text-danger" />
|
|
||||||
<div>{t("stream.twoWayTalk.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.twoWayTalk.tips")}
|
|
||||||
<div className="mt-2 flex items-center text-primary">
|
|
||||||
<Link
|
|
||||||
to={getLocaleDocUrl(
|
|
||||||
"configuration/live/#webrtc-extra-configuration",
|
|
||||||
)}
|
|
||||||
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-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>
|
</div>
|
||||||
<Button
|
</PopoverTrigger>
|
||||||
className={`flex items-center gap-2.5 rounded-lg`}
|
<PopoverContent className="w-80 text-xs">
|
||||||
aria-label={t("stream.lowBandwidth.resetStream")}
|
{t("streaming.restreaming.desc.title", {
|
||||||
variant="outline"
|
ns: "components/dialog",
|
||||||
size="sm"
|
})}
|
||||||
onClick={() => setLowBandwidth(false)}
|
<div className="mt-2 flex items-center text-primary">
|
||||||
>
|
<Link
|
||||||
<MdOutlineRestartAlt className="size-5 text-primary-variant" />
|
to={getLocaleDocUrl("configuration/live")}
|
||||||
<div className="text-primary-variant">
|
target="_blank"
|
||||||
{t("stream.lowBandwidth.resetStream")}
|
rel="noopener noreferrer"
|
||||||
</div>
|
className="inline"
|
||||||
</Button>
|
>
|
||||||
</div>
|
{t("readTheDocumentation", { ns: "common" })}
|
||||||
)}
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isRestreamed &&
|
||||||
|
Object.values(camera.live.streams).length > 0 && (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Label htmlFor="streaming-method">
|
||||||
|
{t("stream.title")}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={streamName}
|
||||||
|
disabled={debug}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setStreamName?.(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue>
|
||||||
|
{Object.keys(camera.live.streams).find(
|
||||||
|
(key) => camera.live.streams[key] === streamName,
|
||||||
|
)}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{Object.entries(camera.live.streams).map(
|
||||||
|
([stream, name]) => (
|
||||||
|
<SelectItem
|
||||||
|
key={stream}
|
||||||
|
className="cursor-pointer"
|
||||||
|
value={name}
|
||||||
|
>
|
||||||
|
{stream}
|
||||||
|
</SelectItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</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 && (
|
||||||
|
<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">
|
||||||
|
{supports2WayTalk ? (
|
||||||
|
<>
|
||||||
|
<LuCheck className="size-4 text-success" />
|
||||||
|
<div>{t("stream.twoWayTalk.available")}</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<LuX className="size-4 text-danger" />
|
||||||
|
<div>{t("stream.twoWayTalk.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.twoWayTalk.tips")}
|
||||||
|
<div className="mt-2 flex items-center text-primary">
|
||||||
|
<Link
|
||||||
|
to={getLocaleDocUrl(
|
||||||
|
"configuration/live/#webrtc-extra-configuration",
|
||||||
|
)}
|
||||||
|
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-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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isRestreamed && (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label
|
||||||
|
className="mx-0 cursor-pointer text-primary"
|
||||||
|
htmlFor="backgroundplay"
|
||||||
|
>
|
||||||
|
{t("stream.playInBackground.label")}
|
||||||
|
</Label>
|
||||||
|
<Switch
|
||||||
|
className="ml-1"
|
||||||
|
id="backgroundplay"
|
||||||
|
disabled={debug}
|
||||||
|
checked={playInBackground}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setPlayInBackground(checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("stream.playInBackground.tips")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isRestreamed && (
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label
|
<Label
|
||||||
className="mx-0 cursor-pointer text-primary"
|
className="mx-0 cursor-pointer text-primary"
|
||||||
htmlFor="backgroundplay"
|
htmlFor="showstats"
|
||||||
>
|
>
|
||||||
{t("stream.playInBackground.label")}
|
{t("streaming.showStats.label", {
|
||||||
|
ns: "components/dialog",
|
||||||
|
})}
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
id="backgroundplay"
|
id="showstats"
|
||||||
disabled={debug}
|
disabled={debug}
|
||||||
checked={playInBackground}
|
checked={showStats}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) => setShowStats(checked)}
|
||||||
setPlayInBackground(checked)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("stream.playInBackground.tips")}
|
{t("streaming.showStats.desc", {
|
||||||
|
ns: "components/dialog",
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center justify-between">
|
<Label
|
||||||
<Label
|
className="mx-0 cursor-pointer text-primary"
|
||||||
className="mx-0 cursor-pointer text-primary"
|
htmlFor="debug"
|
||||||
htmlFor="showstats"
|
>
|
||||||
>
|
{t("streaming.debugView", {
|
||||||
{t("streaming.showStats.label", {
|
ns: "components/dialog",
|
||||||
ns: "components/dialog",
|
})}
|
||||||
})}
|
</Label>
|
||||||
</Label>
|
<Switch
|
||||||
<Switch
|
className="ml-1"
|
||||||
className="ml-1"
|
id="debug"
|
||||||
id="showstats"
|
checked={debug}
|
||||||
disabled={debug}
|
onCheckedChange={(checked) => setDebug(checked)}
|
||||||
checked={showStats}
|
/>
|
||||||
onCheckedChange={(checked) => setShowStats(checked)}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t("streaming.showStats.desc", {
|
|
||||||
ns: "components/dialog",
|
|
||||||
})}
|
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DropdownMenuContent>
|
||||||
</DropdownMenuContent>
|
</DropdownMenu>
|
||||||
</DropdownMenu>
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -202,14 +202,6 @@ export default function LiveDashboardView({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const {
|
|
||||||
preferredLiveModes,
|
|
||||||
setPreferredLiveModes,
|
|
||||||
resetPreferredLiveMode,
|
|
||||||
isRestreamedStates,
|
|
||||||
supportsAudioOutputStates,
|
|
||||||
} = useCameraLiveMode(cameras, windowVisible);
|
|
||||||
|
|
||||||
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
const [globalAutoLive] = usePersistence("autoLiveView", true);
|
||||||
const [displayCameraNames] = usePersistence("displayCameraNames", false);
|
const [displayCameraNames] = usePersistence("displayCameraNames", false);
|
||||||
|
|
||||||
@ -239,6 +231,33 @@ export default function LiveDashboardView({
|
|||||||
[visibleCameraObserver.current],
|
[visibleCameraObserver.current],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const activeStreams = useMemo(() => {
|
||||||
|
const streams: { [cameraName: string]: string } = {};
|
||||||
|
cameras.forEach((camera) => {
|
||||||
|
const availableStreams = camera.live.streams || {};
|
||||||
|
const streamNameFromSettings =
|
||||||
|
currentGroupStreamingSettings?.[camera.name]?.streamName || "";
|
||||||
|
const streamExists =
|
||||||
|
streamNameFromSettings &&
|
||||||
|
Object.values(availableStreams).includes(streamNameFromSettings);
|
||||||
|
|
||||||
|
const streamName = streamExists
|
||||||
|
? streamNameFromSettings
|
||||||
|
: Object.values(availableStreams)[0] || "";
|
||||||
|
|
||||||
|
streams[camera.name] = streamName;
|
||||||
|
});
|
||||||
|
return streams;
|
||||||
|
}, [cameras, currentGroupStreamingSettings]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
preferredLiveModes,
|
||||||
|
setPreferredLiveModes,
|
||||||
|
resetPreferredLiveMode,
|
||||||
|
isRestreamedStates,
|
||||||
|
supportsAudioOutputStates,
|
||||||
|
} = useCameraLiveMode(cameras, windowVisible, activeStreams);
|
||||||
|
|
||||||
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
const birdseyeConfig = useMemo(() => config?.birdseye, [config]);
|
||||||
|
|
||||||
const handleError = useCallback(
|
const handleError = useCallback(
|
||||||
|
|||||||
@ -99,6 +99,10 @@ export default function UiSettingsView() {
|
|||||||
const [playbackRate, setPlaybackRate] = usePersistence("playbackRate", 1);
|
const [playbackRate, setPlaybackRate] = usePersistence("playbackRate", 1);
|
||||||
const [weekStartsOn, setWeekStartsOn] = usePersistence("weekStartsOn", 0);
|
const [weekStartsOn, setWeekStartsOn] = usePersistence("weekStartsOn", 0);
|
||||||
const [alertVideos, setAlertVideos] = usePersistence("alertVideos", true);
|
const [alertVideos, setAlertVideos] = usePersistence("alertVideos", true);
|
||||||
|
const [fallbackTimeout, setFallbackTimeout] = usePersistence(
|
||||||
|
"liveFallbackTimeout",
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -161,6 +165,48 @@ export default function UiSettingsView() {
|
|||||||
<p>{t("general.liveDashboard.displayCameraNames.desc")}</p>
|
<p>{t("general.liveDashboard.displayCameraNames.desc")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex flex-row items-center justify-start gap-2">
|
||||||
|
<Label
|
||||||
|
className="cursor-pointer"
|
||||||
|
htmlFor="live-fallback-timeout"
|
||||||
|
>
|
||||||
|
{t("general.liveDashboard.liveFallbackTimeout.label")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
|
||||||
|
<p>{t("general.liveDashboard.liveFallbackTimeout.desc")}</p>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={fallbackTimeout?.toString()}
|
||||||
|
onValueChange={(value) => setFallbackTimeout(parseInt(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-36">
|
||||||
|
{t("time.second", {
|
||||||
|
ns: "common",
|
||||||
|
time: fallbackTimeout,
|
||||||
|
count: fallbackTimeout,
|
||||||
|
})}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((timeout) => (
|
||||||
|
<SelectItem
|
||||||
|
key={timeout}
|
||||||
|
className="cursor-pointer"
|
||||||
|
value={timeout.toString()}
|
||||||
|
>
|
||||||
|
{t("time.second", {
|
||||||
|
ns: "common",
|
||||||
|
time: timeout,
|
||||||
|
count: timeout,
|
||||||
|
})}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="my-3 flex w-full flex-col space-y-6">
|
<div className="my-3 flex w-full flex-col space-y-6">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user