Compare commits

..

1 Commits

Author SHA1 Message Date
GuoQing Liu
8883db7b65
Merge e4bacc2e71 into a623150811 2025-11-11 08:33:01 -06:00
19 changed files with 301 additions and 490 deletions

View File

@ -189,7 +189,7 @@ RUN /build_pysqlite3.sh
COPY docker/main/requirements-wheels.txt /requirements-wheels.txt COPY docker/main/requirements-wheels.txt /requirements-wheels.txt
RUN pip3 wheel --wheel-dir=/wheels -r /requirements-wheels.txt && \ RUN pip3 wheel --wheel-dir=/wheels -r /requirements-wheels.txt && \
if [ "$DEBUG" = "true" ]; then \ if [ "$DEBUG" = "true" ]; then \
pip3 wheel --wheel-dir=/wheels -r /requirements-dev.txt; \ pip3 wheel --wheel-dir=/wheels -r /requirements-dev.txt; \
fi fi
# Install HailoRT & Wheels # Install HailoRT & Wheels

View File

@ -255,7 +255,6 @@ 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
@ -342,13 +341,6 @@ 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

View File

@ -72,10 +72,7 @@
"formattedTimestampFilename": { "formattedTimestampFilename": {
"12hour": "MM-dd-yy-h-mm-ss-a", "12hour": "MM-dd-yy-h-mm-ss-a",
"24hour": "MM-dd-yy-HH-mm-ss" "24hour": "MM-dd-yy-HH-mm-ss"
}, }
"inProgress": "In progress",
"invalidStartTime": "Invalid start time",
"invalidEndTime": "Invalid end time"
}, },
"unit": { "unit": {
"speed": { "speed": {
@ -147,8 +144,7 @@
"unselect": "Unselect", "unselect": "Unselect",
"export": "Export", "export": "Export",
"deleteNow": "Delete Now", "deleteNow": "Delete Now",
"next": "Next", "next": "Next"
"continue": "Continue"
}, },
"menu": { "menu": {
"system": "System", "system": "System",
@ -241,7 +237,6 @@
"export": "Export", "export": "Export",
"uiPlayground": "UI Playground", "uiPlayground": "UI Playground",
"faceLibrary": "Face Library", "faceLibrary": "Face Library",
"classification": "Classification",
"user": { "user": {
"title": "User", "title": "User",
"account": "Account", "account": "Account",

View File

@ -24,8 +24,8 @@
"label": "Detail", "label": "Detail",
"noDataFound": "No detail data to review", "noDataFound": "No detail data to review",
"aria": "Toggle detail view", "aria": "Toggle detail view",
"trackedObject_one": "{{count}} object", "trackedObject_one": "object",
"trackedObject_other": "{{count}} objects", "trackedObject_other": "objects",
"noObjectDetailData": "No object detail data available.", "noObjectDetailData": "No object detail data available.",
"settings": "Detail View Settings", "settings": "Detail View Settings",
"alwaysExpandActive": { "alwaysExpandActive": {

View File

@ -35,7 +35,7 @@
"snapshot": "snapshot", "snapshot": "snapshot",
"thumbnail": "thumbnail", "thumbnail": "thumbnail",
"video": "video", "video": "video",
"tracking_details": "tracking details" "object_lifecycle": "object lifecycle"
}, },
"trackingDetails": { "trackingDetails": {
"title": "Tracking Details", "title": "Tracking Details",

View File

@ -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": "UI Settings - Frigate", "general": "General 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": "UI Settings", "title": "General Settings",
"liveDashboard": { "liveDashboard": {
"title": "Live Dashboard", "title": "Live Dashboard",
"automaticLiveView": { "automaticLiveView": {
@ -51,10 +51,6 @@
"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": {

View File

@ -454,24 +454,6 @@ export function GeneralFilterContent({
onClose, onClose,
}: GeneralFilterContentProps) { }: GeneralFilterContentProps) {
const { t } = useTranslation(["components/filter", "views/events"]); const { t } = useTranslation(["components/filter", "views/events"]);
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
const allAudioListenLabels = useMemo<string[]>(() => {
if (!config) {
return [];
}
const labels = new Set<string>();
Object.values(config.cameras).forEach((camera) => {
if (camera?.audio?.enabled) {
camera.audio.listen.forEach((label) => {
labels.add(label);
});
}
});
return [...labels].sort();
}, [config]);
return ( return (
<> <>
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden"> <div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
@ -522,10 +504,7 @@ export function GeneralFilterContent({
{allLabels.map((item) => ( {allLabels.map((item) => (
<FilterSwitch <FilterSwitch
key={item} key={item}
label={getTranslatedLabel( label={getTranslatedLabel(item)}
item,
allAudioListenLabels.includes(item) ? "audio" : "object",
)}
isChecked={filter.labels?.includes(item) ?? false} isChecked={filter.labels?.includes(item) ?? false}
onCheckedChange={(isChecked) => { onCheckedChange={(isChecked) => {
if (isChecked) { if (isChecked) {

View File

@ -81,43 +81,6 @@ export default function InputWithTags({
revalidateOnFocus: false, revalidateOnFocus: false,
}); });
const allAudioListenLabels = useMemo<Set<string>>(() => {
if (!config) {
return new Set<string>();
}
const labels = new Set<string>();
Object.values(config.cameras).forEach((camera) => {
if (camera?.audio?.enabled) {
camera.audio.listen.forEach((label) => {
labels.add(label);
});
}
});
return labels;
}, [config]);
const translatedAudioLabelMap = useMemo<Map<string, string>>(() => {
const map = new Map<string, string>();
if (!config) return map;
allAudioListenLabels.forEach((label) => {
// getTranslatedLabel likely depends on i18n internally; including `lang`
// in deps ensures this map is rebuilt when language changes
map.set(label, getTranslatedLabel(label, "audio"));
});
return map;
}, [allAudioListenLabels, config]);
function resolveLabel(value: string) {
const mapped = translatedAudioLabelMap.get(value);
if (mapped) return mapped;
return getTranslatedLabel(
value,
allAudioListenLabels.has(value) ? "audio" : "object",
);
}
const [inputValue, setInputValue] = useState(search || ""); const [inputValue, setInputValue] = useState(search || "");
const [currentFilterType, setCurrentFilterType] = useState<FilterType | null>( const [currentFilterType, setCurrentFilterType] = useState<FilterType | null>(
null, null,
@ -458,8 +421,7 @@ export default function InputWithTags({
? t("button.yes", { ns: "common" }) ? t("button.yes", { ns: "common" })
: t("button.no", { ns: "common" }); : t("button.no", { ns: "common" });
} else if (filterType === "labels") { } else if (filterType === "labels") {
const value = String(filterValues); return getTranslatedLabel(String(filterValues));
return resolveLabel(value);
} else if (filterType === "search_type") { } else if (filterType === "search_type") {
return t("filter.searchType." + String(filterValues)); return t("filter.searchType." + String(filterValues));
} else { } else {
@ -866,7 +828,7 @@ export default function InputWithTags({
> >
{t("filter.label." + filterType)}:{" "} {t("filter.label." + filterType)}:{" "}
{filterType === "labels" ? ( {filterType === "labels" ? (
resolveLabel(value) getTranslatedLabel(value)
) : filterType === "cameras" ? ( ) : filterType === "cameras" ? (
<CameraNameLabel camera={value} /> <CameraNameLabel camera={value} />
) : filterType === "zones" ? ( ) : filterType === "zones" ? (

View File

@ -1155,7 +1155,7 @@ function ObjectDetailsTab({
</div> </div>
<div className="flex flex-row items-center gap-2 text-sm smart-capitalize"> <div className="flex flex-row items-center gap-2 text-sm smart-capitalize">
{getIconForLabel(search.label, "size-4 text-primary")} {getIconForLabel(search.label, "size-4 text-primary")}
{getTranslatedLabel(search.label, search.data.type)} {getTranslatedLabel(search.label)}
{search.sub_label && ` (${search.sub_label})`} {search.sub_label && ` (${search.sub_label})`}
{isAdmin && search.end_time && ( {isAdmin && search.end_time && (
<Tooltip> <Tooltip>
@ -1394,9 +1394,7 @@ function ObjectDetailsTab({
{state == "submitted" && ( {state == "submitted" && (
<div className="flex flex-row items-center justify-center gap-2"> <div className="flex flex-row items-center justify-center gap-2">
<FaCheckCircle className="size-4 text-success" /> <FaCheckCircle className="size-4 text-success" />
{t("explore.plus.review.state.submitted", { {t("explore.plus.review.state.submitted")}
ns: "components/dialog",
})}
</div> </div>
)} )}
</div> </div>

View File

@ -343,10 +343,6 @@ 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 />;
} }
@ -392,7 +388,6 @@ 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}

View File

@ -1,5 +1,4 @@
import { baseUrl } from "@/api/baseUrl"; import { baseUrl } from "@/api/baseUrl";
import { usePersistence } from "@/hooks/use-persistence";
import { import {
LivePlayerError, LivePlayerError,
PlayerStatsType, PlayerStatsType,
@ -72,8 +71,6 @@ 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);
@ -478,10 +475,7 @@ function MSEPlayer({
setBufferTimeout(undefined); setBufferTimeout(undefined);
} }
const timeoutDuration = const timeoutDuration = bufferTime == 0 ? 5000 : 3000;
bufferTime == 0
? (fallbackTimeout ?? 3) * 2 * 1000
: (fallbackTimeout ?? 3) * 1000;
setBufferTimeout( setBufferTimeout(
setTimeout(() => { setTimeout(() => {
if ( if (
@ -506,7 +500,6 @@ function MSEPlayer({
onError, onError,
onPlaying, onPlaying,
playbackEnabled, playbackEnabled,
fallbackTimeout,
]); ]);
useEffect(() => { useEffect(() => {

View File

@ -349,7 +349,7 @@ function ReviewGroup({
? fetchedEvents.length ? fetchedEvents.length
: (review.data.objects ?? []).length; : (review.data.objects ?? []).length;
return `${t("detail.trackedObject", { count: objectCount })}`; return `${objectCount} ${t("detail.trackedObject", { count: objectCount })}`;
}, [review, t, fetchedEvents]); }, [review, t, fetchedEvents]);
const reviewDuration = useMemo( const reviewDuration = useMemo(
@ -478,7 +478,7 @@ function ReviewGroup({
<div className="rounded-full bg-muted-foreground p-1"> <div className="rounded-full bg-muted-foreground p-1">
{getIconForLabel(audioLabel, "size-3 text-white")} {getIconForLabel(audioLabel, "size-3 text-white")}
</div> </div>
<span>{getTranslatedLabel(audioLabel, "audio")}</span> <span>{getTranslatedLabel(audioLabel)}</span>
</div> </div>
</div> </div>
))} ))}
@ -513,8 +513,7 @@ function EventList({
const isSelected = selectedObjectIds.includes(event.id); const isSelected = selectedObjectIds.includes(event.id);
const label = const label = event.sub_label || getTranslatedLabel(event.label);
event.sub_label || getTranslatedLabel(event.label, event.data.type);
const handleObjectSelect = (event: Event | undefined) => { const handleObjectSelect = (event: Event | undefined) => {
if (event) { if (event) {

View File

@ -6,7 +6,6 @@ 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");
@ -21,20 +20,16 @@ export default function useCameraLiveMode(
); );
if (isRestreamed) { if (isRestreamed) {
if (activeStreams && activeStreams[camera.name]) { Object.values(camera.live.streams).forEach((streamName) => {
streamNames.add(activeStreams[camera.name]); streamNames.add(streamName);
} 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, activeStreams]); }, [cameras, config]);
const streamsFetcher = useCallback(async (key: string) => { const streamsFetcher = useCallback(async (key: string) => {
const streamNames = key.split(","); const streamNames = key.split(",");
@ -73,9 +68,7 @@ export default function useCameraLiveMode(
[key: string]: LiveStreamMetadata; [key: string]: LiveStreamMetadata;
}>(restreamedStreamsKey, streamsFetcher, { }>(restreamedStreamsKey, streamsFetcher, {
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateOnReconnect: false, dedupingInterval: 10000,
revalidateIfStale: false,
dedupingInterval: 60000,
}); });
const [preferredLiveModes, setPreferredLiveModes] = useState<{ const [preferredLiveModes, setPreferredLiveModes] = useState<{

View File

@ -244,12 +244,12 @@ export const getDurationFromTimestamps = (
abbreviated: boolean = false, abbreviated: boolean = false,
): string => { ): string => {
if (isNaN(start_time)) { if (isNaN(start_time)) {
return i18n.t("time.invalidStartTime", { ns: "common" }); return "Invalid start time";
} }
let duration = i18n.t("time.inProgress", { ns: "common" }); let duration = "In Progress";
if (end_time !== null) { if (end_time !== null) {
if (isNaN(end_time)) { if (isNaN(end_time)) {
return i18n.t("time.invalidEndTime", { ns: "common" }); return "Invalid end time";
} }
const start = fromUnixTime(start_time); const start = fromUnixTime(start_time);
const end = fromUnixTime(end_time); const end = fromUnixTime(end_time);

View File

@ -86,6 +86,14 @@ 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);
@ -98,33 +106,6 @@ 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), []);

View File

@ -162,9 +162,6 @@ export default function LiveCameraView({
isRestreamed ? `go2rtc/streams/${streamName}` : null, isRestreamed ? `go2rtc/streams/${streamName}` : null,
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
dedupingInterval: 60000,
}, },
); );
@ -1030,298 +1027,294 @@ function FrigateCameraFeatures({
disabled={!cameraEnabled || debug || isSnapshotLoading} disabled={!cameraEnabled || debug || isSnapshotLoading}
loading={isSnapshotLoading} loading={isSnapshotLoading}
/> />
{!fullscreen && ( <DropdownMenu modal={false}>
<DropdownMenu modal={false}> <DropdownMenuTrigger>
<DropdownMenuTrigger> <div
<div className={cn(
className={cn( "flex flex-col items-center justify-center rounded-lg bg-secondary p-2 text-secondary-foreground md:p-0",
"flex flex-col items-center justify-center rounded-lg bg-secondary p-2 text-secondary-foreground md:p-0", )}
)} >
> <FaCog
<FaCog className={`text-secondary-foreground" size-5 md:m-[6px]`}
className={`text-secondary-foreground" size-5 md:m-[6px]`} />
/> </div>
</div> </DropdownMenuTrigger>
</DropdownMenuTrigger> <DropdownMenuContent className="max-w-96">
<DropdownMenuContent className="max-w-96"> <div className="flex flex-col gap-5 p-4">
<div className="flex flex-col gap-5 p-4"> {!isRestreamed && (
{!isRestreamed && ( <div className="flex flex-col gap-2">
<div className="flex flex-col gap-2"> <Label>
<Label> {t("streaming.label", { ns: "components/dialog" })}
{t("streaming.label", { ns: "components/dialog" })} </Label>
</Label> <div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground"> <LuX className="size-4 text-danger" />
<LuX className="size-4 text-danger" /> <div>
<div> {t("streaming.restreaming.disabled", {
{t("streaming.restreaming.disabled", { ns: "components/dialog",
})}
</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> <div className="mt-2 flex items-center text-primary">
<Popover> <Link
<PopoverTrigger asChild> to={getLocaleDocUrl("configuration/live")}
<div className="cursor-pointer p-0"> target="_blank"
<LuInfo className="size-4" /> rel="noopener noreferrer"
<span className="sr-only"> className="inline"
{t("button.info", { ns: "common" })} >
</span> {t("readTheDocumentation", { ns: "common" })}
</div> <LuExternalLink className="ml-2 inline-flex size-3" />
</PopoverTrigger> </Link>
<PopoverContent className="w-80 text-xs"> </div>
{t("streaming.restreaming.desc.title", { </PopoverContent>
ns: "components/dialog", </Popover>
})}
<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> </div>
)} </div>
{isRestreamed && )}
Object.values(camera.live.streams).length > 0 && ( {isRestreamed &&
<div className="flex flex-col gap-1"> Object.values(camera.live.streams).length > 0 && (
<Label htmlFor="streaming-method"> <div className="flex flex-col gap-1">
{t("stream.title")} <Label htmlFor="streaming-method">
</Label> {t("stream.title")}
<Select </Label>
value={streamName} <Select
disabled={debug} value={streamName}
onValueChange={(value) => { disabled={debug}
setStreamName?.(value); onValueChange={(value) => {
}} setStreamName?.(value);
> }}
<SelectTrigger className="w-full"> >
<SelectValue> <SelectTrigger className="w-full">
{Object.keys(camera.live.streams).find( <SelectValue>
(key) => camera.live.streams[key] === streamName, {Object.keys(camera.live.streams).find(
)} (key) => camera.live.streams[key] === streamName,
</SelectValue> )}
</SelectTrigger> </SelectValue>
</SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
{Object.entries(camera.live.streams).map( {Object.entries(camera.live.streams).map(
([stream, name]) => ( ([stream, name]) => (
<SelectItem <SelectItem
key={stream} key={stream}
className="cursor-pointer" className="cursor-pointer"
value={name} value={name}
> >
{stream} {stream}
</SelectItem> </SelectItem>
), ),
)} )}
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
{debug && ( {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"> <div className="flex flex-row items-center gap-1 text-sm text-muted-foreground">
<> {supportsAudioOutput ? (
<LuX className="size-8 text-danger" /> <>
<div>{t("stream.debug.picker")}</div> <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> </div>
)} )}
{preferredLiveMode != "jsmpeg" && {preferredLiveMode == "jsmpeg" &&
!debug && !debug &&
isRestreamed && ( isRestreamed && (
<div className="flex flex-row items-center gap-1 text-sm text-muted-foreground"> <div className="flex flex-col items-center gap-3">
{supportsAudioOutput ? ( <div className="flex flex-row items-center gap-2">
<> <IoIosWarning className="mr-1 size-8 text-danger" />
<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" && <p className="text-sm">
!debug && {t("stream.lowBandwidth.tips")}
isRestreamed && ( </p>
<div className="flex flex-col items-center gap-3"> </div>
<div className="flex flex-row items-center gap-2"> <Button
<IoIosWarning className="mr-1 size-8 text-danger" /> className={`flex items-center gap-2.5 rounded-lg`}
aria-label={t("stream.lowBandwidth.resetStream")}
<p className="text-sm"> variant="outline"
{t("stream.lowBandwidth.tips")} size="sm"
</p> onClick={() => setLowBandwidth(false)}
>
<MdOutlineRestartAlt className="size-5 text-primary-variant" />
<div className="text-primary-variant">
{t("stream.lowBandwidth.resetStream")}
</div> </div>
<Button </Button>
className={`flex items-center gap-2.5 rounded-lg`} </div>
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="showstats" htmlFor="backgroundplay"
> >
{t("streaming.showStats.label", { {t("stream.playInBackground.label")}
ns: "components/dialog",
})}
</Label> </Label>
<Switch <Switch
className="ml-1" className="ml-1"
id="showstats" id="backgroundplay"
disabled={debug} disabled={debug}
checked={showStats} checked={playInBackground}
onCheckedChange={(checked) => setShowStats(checked)} onCheckedChange={(checked) =>
setPlayInBackground(checked)
}
/> />
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t("streaming.showStats.desc", { {t("stream.playInBackground.tips")}
ns: "components/dialog",
})}
</p> </p>
</div> </div>
<div className="flex flex-col gap-1"> )}
<div className="flex items-center justify-between"> <div className="flex flex-col gap-1">
<Label <div className="flex items-center justify-between">
className="mx-0 cursor-pointer text-primary" <Label
htmlFor="debug" className="mx-0 cursor-pointer text-primary"
> htmlFor="showstats"
{t("streaming.debugView", { >
ns: "components/dialog", {t("streaming.showStats.label", {
})} ns: "components/dialog",
</Label> })}
<Switch </Label>
className="ml-1" <Switch
id="debug" className="ml-1"
checked={debug} id="showstats"
onCheckedChange={(checked) => setDebug(checked)} disabled={debug}
/> checked={showStats}
</div> onCheckedChange={(checked) => setShowStats(checked)}
/>
</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>
</DropdownMenuContent> </div>
</DropdownMenu> </DropdownMenuContent>
)} </DropdownMenu>
</> </>
); );
} }

View File

@ -202,6 +202,14 @@ 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);
@ -231,33 +239,6 @@ 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(

View File

@ -649,7 +649,7 @@ export function RecordingView({
value="detail" value="detail"
aria-label="Detail Stream" aria-label="Detail Stream"
> >
<div className="">{t("detail.label")}</div> <div className="">Detail</div>
</ToggleGroupItem> </ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
) : ( ) : (

View File

@ -99,10 +99,6 @@ 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 (
<> <>
@ -165,48 +161,6 @@ 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">