diff --git a/frigate/detectors/detection_runners.py b/frigate/detectors/detection_runners.py index 80d4e0487..eb3a0ecb9 100644 --- a/frigate/detectors/detection_runners.py +++ b/frigate/detectors/detection_runners.py @@ -255,6 +255,7 @@ class OpenVINOModelRunner(BaseModelRunner): def __init__(self, model_path: str, device: str, model_type: str, **kwargs): self.model_path = model_path self.device = device + self.model_type = model_type if device == "NPU" and not OpenVINOModelRunner.is_model_npu_supported( model_type @@ -341,6 +342,13 @@ class OpenVINOModelRunner(BaseModelRunner): # Lock prevents concurrent access to infer_request # Needed for JinaV2: genai thread (text) + embeddings thread (vision) 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 if ( len(inputs) == 1 diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 6a44b1116..b08aa10c0 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -8,7 +8,7 @@ "masksAndZones": "Mask and Zone Editor - Frigate", "motionTuner": "Motion Tuner - Frigate", "object": "Debug - Frigate", - "general": "General Settings - Frigate", + "general": "UI Settings - Frigate", "frigatePlus": "Frigate+ Settings - Frigate", "notifications": "Notification Settings - Frigate" }, @@ -37,7 +37,7 @@ "noCamera": "No Camera" }, "general": { - "title": "General Settings", + "title": "UI Settings", "liveDashboard": { "title": "Live Dashboard", "automaticLiveView": { @@ -51,6 +51,10 @@ "displayCameraNames": { "label": "Always Show Camera Names", "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": { diff --git a/web/src/components/overlay/detail/TrackingDetails.tsx b/web/src/components/overlay/detail/TrackingDetails.tsx index 279b85e4c..33ddd0f00 100644 --- a/web/src/components/overlay/detail/TrackingDetails.tsx +++ b/web/src/components/overlay/detail/TrackingDetails.tsx @@ -343,6 +343,10 @@ export function TrackingDetails({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [displayedRecordTime]); + const onUploadFrameToPlus = useCallback(() => { + return axios.post(`/${event.camera}/plus/${currentTime}`); + }, [event.camera, currentTime]); + if (!config) { return ; } @@ -388,6 +392,7 @@ export function TrackingDetails({ frigateControls={true} onTimeUpdate={handleTimeUpdate} onSeekToTime={handleSeekToTime} + onUploadFrame={onUploadFrameToPlus} isDetailMode={true} camera={event.camera} currentTimeOverride={currentTime} diff --git a/web/src/components/player/MsePlayer.tsx b/web/src/components/player/MsePlayer.tsx index c4a30c7e6..4d2b8b365 100644 --- a/web/src/components/player/MsePlayer.tsx +++ b/web/src/components/player/MsePlayer.tsx @@ -1,4 +1,5 @@ import { baseUrl } from "@/api/baseUrl"; +import { usePersistence } from "@/hooks/use-persistence"; import { LivePlayerError, PlayerStatsType, @@ -71,6 +72,8 @@ function MSEPlayer({ const [errorCount, setErrorCount] = useState(0); const totalBytesLoaded = useRef(0); + const [fallbackTimeout] = usePersistence("liveFallbackTimeout", 3); + const videoRef = useRef(null); const wsRef = useRef(null); const reconnectTIDRef = useRef(null); @@ -475,7 +478,10 @@ function MSEPlayer({ setBufferTimeout(undefined); } - const timeoutDuration = bufferTime == 0 ? 5000 : 3000; + const timeoutDuration = + bufferTime == 0 + ? (fallbackTimeout ?? 3) * 2 * 1000 + : (fallbackTimeout ?? 3) * 1000; setBufferTimeout( setTimeout(() => { if ( @@ -500,6 +506,7 @@ function MSEPlayer({ onError, onPlaying, playbackEnabled, + fallbackTimeout, ]); useEffect(() => { diff --git a/web/src/hooks/use-camera-live-mode.ts b/web/src/hooks/use-camera-live-mode.ts index 17dfaee91..2bdb1f866 100644 --- a/web/src/hooks/use-camera-live-mode.ts +++ b/web/src/hooks/use-camera-live-mode.ts @@ -6,6 +6,7 @@ import { LivePlayerMode, LiveStreamMetadata } from "@/types/live"; export default function useCameraLiveMode( cameras: CameraConfig[], windowVisible: boolean, + activeStreams?: { [cameraName: string]: string }, ) { const { data: config } = useSWR("config"); @@ -20,16 +21,20 @@ export default function useCameraLiveMode( ); if (isRestreamed) { - Object.values(camera.live.streams).forEach((streamName) => { - streamNames.add(streamName); - }); + if (activeStreams && activeStreams[camera.name]) { + streamNames.add(activeStreams[camera.name]); + } else { + Object.values(camera.live.streams).forEach((streamName) => { + streamNames.add(streamName); + }); + } } }); return streamNames.size > 0 ? Array.from(streamNames).sort().join(",") : null; - }, [cameras, config]); + }, [cameras, config, activeStreams]); const streamsFetcher = useCallback(async (key: string) => { const streamNames = key.split(","); @@ -68,7 +73,9 @@ export default function useCameraLiveMode( [key: string]: LiveStreamMetadata; }>(restreamedStreamsKey, streamsFetcher, { revalidateOnFocus: false, - dedupingInterval: 10000, + revalidateOnReconnect: false, + revalidateIfStale: false, + dedupingInterval: 60000, }); const [preferredLiveModes, setPreferredLiveModes] = useState<{ diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index 5239fe388..259e4093f 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -86,14 +86,6 @@ export default function DraggableGridLayout({ // preferred live modes per camera - const { - preferredLiveModes, - setPreferredLiveModes, - resetPreferredLiveMode, - isRestreamedStates, - supportsAudioOutputStates, - } = useCameraLiveMode(cameras, windowVisible); - const [globalAutoLive] = usePersistence("autoLiveView", true); const [displayCameraNames] = usePersistence("displayCameraNames", false); @@ -106,6 +98,33 @@ export default function DraggableGridLayout({ } }, [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 const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive), []); diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index 2d61d40f5..9e639ab7b 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -162,6 +162,9 @@ export default function LiveCameraView({ isRestreamed ? `go2rtc/streams/${streamName}` : null, { revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + dedupingInterval: 60000, }, ); @@ -1027,294 +1030,298 @@ function FrigateCameraFeatures({ disabled={!cameraEnabled || debug || isSnapshotLoading} loading={isSnapshotLoading} /> - - -
- -
-
- -
- {!isRestreamed && ( -
- -
- -
- {t("streaming.restreaming.disabled", { - ns: "components/dialog", - })} -
- - -
- - - {t("button.info", { ns: "common" })} - -
-
- - {t("streaming.restreaming.desc.title", { + {!fullscreen && ( + + +
+ +
+
+ +
+ {!isRestreamed && ( +
+ +
+ +
+ {t("streaming.restreaming.disabled", { ns: "components/dialog", })} -
- - {t("readTheDocumentation", { ns: "common" })} - - -
- - -
-
- )} - {isRestreamed && - Object.values(camera.live.streams).length > 0 && ( -
- - - - {debug && ( -
- <> - -
{t("stream.debug.picker")}
-
- )} - - {preferredLiveMode != "jsmpeg" && - !debug && - isRestreamed && ( -
- {supportsAudioOutput ? ( - <> - -
{t("stream.audio.available")}
- - ) : ( - <> - -
{t("stream.audio.unavailable")}
- - -
- - - {t("button.info", { ns: "common" })} - -
-
- - {t("stream.audio.tips.title")} -
- - {t("readTheDocumentation", { - ns: "common", - })} - - -
-
-
- - )} -
- )} - {preferredLiveMode != "jsmpeg" && - !debug && - isRestreamed && - supportsAudioOutput && ( -
- {supports2WayTalk ? ( - <> - -
{t("stream.twoWayTalk.available")}
- - ) : ( - <> - -
{t("stream.twoWayTalk.unavailable")}
- - -
- - - {t("button.info", { ns: "common" })} - -
-
- - {t("stream.twoWayTalk.tips")} -
- - {t("readTheDocumentation", { - ns: "common", - })} - - -
-
-
- - )} -
- )} - - {preferredLiveMode == "jsmpeg" && - !debug && - isRestreamed && ( -
-
- - -

- {t("stream.lowBandwidth.tips")} -

+ + +
+ + + {t("button.info", { ns: "common" })} +
- -
- )} + + + {t("streaming.restreaming.desc.title", { + ns: "components/dialog", + })} +
+ + {t("readTheDocumentation", { ns: "common" })} + + +
+
+ +
+
+ )} + {isRestreamed && + Object.values(camera.live.streams).length > 0 && ( +
+ + + + {debug && ( +
+ <> + +
{t("stream.debug.picker")}
+ +
+ )} + + {preferredLiveMode != "jsmpeg" && + !debug && + isRestreamed && ( +
+ {supportsAudioOutput ? ( + <> + +
{t("stream.audio.available")}
+ + ) : ( + <> + +
{t("stream.audio.unavailable")}
+ + +
+ + + {t("button.info", { ns: "common" })} + +
+
+ + {t("stream.audio.tips.title")} +
+ + {t("readTheDocumentation", { + ns: "common", + })} + + +
+
+
+ + )} +
+ )} + {preferredLiveMode != "jsmpeg" && + !debug && + isRestreamed && + supportsAudioOutput && ( +
+ {supports2WayTalk ? ( + <> + +
{t("stream.twoWayTalk.available")}
+ + ) : ( + <> + +
{t("stream.twoWayTalk.unavailable")}
+ + +
+ + + {t("button.info", { ns: "common" })} + +
+
+ + {t("stream.twoWayTalk.tips")} +
+ + {t("readTheDocumentation", { + ns: "common", + })} + + +
+
+
+ + )} +
+ )} + + {preferredLiveMode == "jsmpeg" && + !debug && + isRestreamed && ( +
+
+ + +

+ {t("stream.lowBandwidth.tips")} +

+
+ +
+ )} +
+ )} + {isRestreamed && ( +
+
+ + + setPlayInBackground(checked) + } + /> +
+

+ {t("stream.playInBackground.tips")} +

)} - {isRestreamed && (
- setPlayInBackground(checked) - } + checked={showStats} + onCheckedChange={(checked) => setShowStats(checked)} />

- {t("stream.playInBackground.tips")} + {t("streaming.showStats.desc", { + ns: "components/dialog", + })}

- )} -
-
- - setShowStats(checked)} - /> -
-

- {t("streaming.showStats.desc", { - ns: "components/dialog", - })} -

-
-
-
- - setDebug(checked)} - /> +
+
+ + setDebug(checked)} + /> +
-
- - + + + )} ); } diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index 8b7dd1336..c4104576c 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -202,14 +202,6 @@ export default function LiveDashboardView({ }; }, []); - const { - preferredLiveModes, - setPreferredLiveModes, - resetPreferredLiveMode, - isRestreamedStates, - supportsAudioOutputStates, - } = useCameraLiveMode(cameras, windowVisible); - const [globalAutoLive] = usePersistence("autoLiveView", true); const [displayCameraNames] = usePersistence("displayCameraNames", false); @@ -239,6 +231,33 @@ export default function LiveDashboardView({ [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 handleError = useCallback( diff --git a/web/src/views/settings/UiSettingsView.tsx b/web/src/views/settings/UiSettingsView.tsx index 092e3713d..d09ab7c75 100644 --- a/web/src/views/settings/UiSettingsView.tsx +++ b/web/src/views/settings/UiSettingsView.tsx @@ -99,6 +99,10 @@ export default function UiSettingsView() { const [playbackRate, setPlaybackRate] = usePersistence("playbackRate", 1); const [weekStartsOn, setWeekStartsOn] = usePersistence("weekStartsOn", 0); const [alertVideos, setAlertVideos] = usePersistence("alertVideos", true); + const [fallbackTimeout, setFallbackTimeout] = usePersistence( + "liveFallbackTimeout", + 3, + ); return ( <> @@ -161,6 +165,48 @@ export default function UiSettingsView() {

{t("general.liveDashboard.displayCameraNames.desc")}

+
+
+ +
+
+

{t("general.liveDashboard.liveFallbackTimeout.desc")}

+
+ +