From 7f7eefef7f099806b74e629070bf8269141dbea3 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 22 Sep 2025 21:21:51 -0500 Subject: [PATCH] Live view improvements (#20177) --- docs/docs/configuration/live.md | 23 +- web/src/components/player/MsePlayer.tsx | 35 +- web/src/components/player/WebRTCPlayer.tsx | 18 +- web/src/views/live/LiveCameraView.tsx | 367 ++++++++++----------- 4 files changed, 242 insertions(+), 201 deletions(-) diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index 3358f9d6b..581316647 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -176,7 +176,7 @@ For devices that support two way talk, Frigate can be configured to use the feat To use the Reolink Doorbell with two way talk, you should use the [recommended Reolink configuration](/configuration/camera_specific#reolink-doorbell) -As a starting point to check compatibility for your camera, view the list of cameras supported for two-way talk on the [go2rtc repository](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#two-way-audio). For cameras in the category `ONVIF Profile T`, you can use the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/)'s FeatureList to check for the presence of `AudioOutput`. A camera that supports `ONVIF Profile T` *usually* supports this, but due to inconsistent support, a camera that explicitly lists this feature may still not work. If no entry for your camera exists on the database, it is recommended not to buy it or to consult with the manufacturer's support on the feature availability. +As a starting point to check compatibility for your camera, view the list of cameras supported for two-way talk on the [go2rtc repository](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#two-way-audio). For cameras in the category `ONVIF Profile T`, you can use the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/)'s FeatureList to check for the presence of `AudioOutput`. A camera that supports `ONVIF Profile T` _usually_ supports this, but due to inconsistent support, a camera that explicitly lists this feature may still not work. If no entry for your camera exists on the database, it is recommended not to buy it or to consult with the manufacturer's support on the feature availability. ### Streaming options on camera group dashboards @@ -230,7 +230,26 @@ Note that disabling a camera through the config file (`enabled: False`) removes If you are using continuous streaming or you are loading more than a few high resolution streams at once on the dashboard, your browser may struggle to begin playback of your streams before the timeout. Frigate always prioritizes showing a live stream as quickly as possible, even if it is a lower quality jsmpeg stream. You can use the "Reset" link/button to try loading your high resolution stream again. - If you are still experiencing Frigate falling back to low bandwidth mode, you may need to adjust your camera's settings per the [recommendations above](#camera_settings_recommendations). + Errors in stream playback (e.g., connection failures, codec issues, or buffering timeouts) that cause the fallback to low bandwidth mode (jsmpeg) are logged to the browser console for easier debugging. These errors may include: + + - Network issues (e.g., MSE or WebRTC network connection problems). + - Unsupported codecs or stream formats (e.g., H.265 in WebRTC, which is not supported in some browsers). + - Buffering timeouts or low bandwidth conditions causing fallback to jsmpeg. + - Browser compatibility problems (e.g., iOS Safari limitations with MSE). + + To view browser console logs: + + 1. Open the Frigate Live View in your browser. + 2. Open the browser's Developer Tools (F12 or right-click > Inspect > Console tab). + 3. Reproduce the error (e.g., load a problematic stream or simulate network issues). + 4. Look for messages prefixed with the camera name. + + These logs help identify if the issue is player-specific (MSE vs. WebRTC) or related to camera configuration (e.g., go2rtc streams, codecs). If you see frequent errors: + + - Verify your camera's H.264/AAC settings (see [Frigate's camera settings recommendations](#camera_settings_recommendations)). + - Check go2rtc configuration for transcoding (e.g., audio to AAC/OPUS). + - Test with a different stream via the UI dropdown (if `live -> streams` is configured). + - For WebRTC-specific issues, ensure port 8555 is forwarded and candidates are set (see (WebRTC Extra Configuration)(#webrtc-extra-configuration)). 3. **It doesn't seem like my cameras are streaming on the Live dashboard. Why?** diff --git a/web/src/components/player/MsePlayer.tsx b/web/src/components/player/MsePlayer.tsx index 7c831e596..f7a77393f 100644 --- a/web/src/components/player/MsePlayer.tsx +++ b/web/src/components/player/MsePlayer.tsx @@ -84,6 +84,17 @@ function MSEPlayer({ return `${baseUrl.replace(/^http/, "ws")}live/mse/api/ws?src=${camera}`; }, [camera]); + const handleError = useCallback( + (error: LivePlayerError, description: string = "Unknown error") => { + // eslint-disable-next-line no-console + console.error( + `${camera} - MSE error '${error}': ${description} See the documentation: https://docs.frigate.video/configuration/live`, + ); + onError?.(error); + }, + [camera, onError], + ); + const handleLoadedMetadata = useCallback(() => { if (videoRef.current && setFullResolution) { setFullResolution({ @@ -237,9 +248,9 @@ function MSEPlayer({ onDisconnect(); } if (isIOS || isSafari) { - onError?.("mse-decode"); + handleError("mse-decode", "Safari cannot open MediaSource."); } else { - onError?.("startup"); + handleError("startup", "Error opening MediaSource."); } }); }, @@ -267,9 +278,9 @@ function MSEPlayer({ onDisconnect(); } if (isIOS || isSafari) { - onError?.("mse-decode"); + handleError("mse-decode", "Safari cannot open MediaSource."); } else { - onError?.("startup"); + handleError("startup", "Error opening MediaSource."); } }); }, @@ -297,7 +308,7 @@ function MSEPlayer({ if (wsRef.current) { onDisconnect(); } - onError?.("mse-decode"); + handleError("mse-decode", "Safari reported InvalidStateError."); return; } else { throw e; // Re-throw if it's not the error we're handling @@ -424,7 +435,10 @@ function MSEPlayer({ (bufferThreshold > 10 || bufferTime > 10) ) { onDisconnect(); - onError?.("stalled"); + handleError( + "stalled", + "Buffer time (10 seconds) exceeded, browser may not be playing media correctly.", + ); } const playbackRate = calculateAdaptivePlaybackRate( @@ -470,7 +484,7 @@ function MSEPlayer({ videoRef.current ) { onDisconnect(); - onError("stalled"); + handleError("stalled", "Media playback has stalled."); } }, timeoutDuration), ); @@ -479,6 +493,7 @@ function MSEPlayer({ bufferTimeout, isPlaying, onDisconnect, + handleError, onError, onPlaying, playbackEnabled, @@ -663,7 +678,7 @@ function MSEPlayer({ if (wsRef.current) { onDisconnect(); } - onError?.("startup"); + handleError("startup", "Browser reported a network error."); } if ( @@ -674,7 +689,7 @@ function MSEPlayer({ if (wsRef.current) { onDisconnect(); } - onError?.("mse-decode"); + handleError("mse-decode", "Safari reported decoding errors."); } setErrorCount((prevCount) => prevCount + 1); @@ -683,7 +698,7 @@ function MSEPlayer({ onDisconnect(); if (errorCount >= 3) { // too many mse errors, try jsmpeg - onError?.("startup"); + handleError("startup", `Max error count ${errorCount} exceeded.`); } else { reconnect(5000); } diff --git a/web/src/components/player/WebRTCPlayer.tsx b/web/src/components/player/WebRTCPlayer.tsx index 81d6a72dd..dc3b24653 100644 --- a/web/src/components/player/WebRTCPlayer.tsx +++ b/web/src/components/player/WebRTCPlayer.tsx @@ -37,6 +37,18 @@ export default function WebRtcPlayer({ return `${baseUrl.replace(/^http/, "ws")}live/webrtc/api/ws?src=${camera}`; }, [camera]); + // error handler + const handleError = useCallback( + (error: LivePlayerError, description: string = "Unknown error") => { + // eslint-disable-next-line no-console + console.error( + `${camera} - WebRTC error '${error}': ${description} See the documentation: https://docs.frigate.video/configuration/live`, + ); + onError?.(error); + }, + [camera, onError], + ); + // camera states const pcRef = useRef(); @@ -212,7 +224,7 @@ export default function WebRtcPlayer({ useEffect(() => { videoLoadTimeoutRef.current = setTimeout(() => { - onError?.("stalled"); + handleError("stalled", "WebRTC connection timed out."); }, 5000); return () => { @@ -327,7 +339,7 @@ export default function WebRtcPlayer({ document.visibilityState === "visible" && pcRef.current != undefined ) { - onError("stalled"); + handleError("stalled", "WebRTC connection stalled."); } }, 3000), ); @@ -344,7 +356,7 @@ export default function WebRtcPlayer({ // @ts-expect-error code does exist e.target.error.code == MediaError.MEDIA_ERR_NETWORK ) { - onError?.("startup"); + handleError("startup", "Browser reported a network error."); } }} /> diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index 5378f94c7..bcc18c765 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -28,7 +28,6 @@ import { import { Tooltip, TooltipContent, - TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { useResizeObserver } from "@/hooks/resize-observer"; @@ -116,6 +115,7 @@ import { SelectGroup, SelectItem, SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { usePersistence } from "@/hooks/use-persistence"; import { Label } from "@/components/ui/label"; @@ -499,122 +499,118 @@ export default function LiveCameraView({ ) : (
)} - -
- {fullscreen && ( - - )} - {supportsFullscreen && ( - - )} - {!isIOS && !isFirefox && preferredLiveMode != "jsmpeg" && ( - { - if (!pip) { - setPip(true); - } else { - document.exitPictureInPicture(); - setPip(false); - } - }} - disabled={!cameraEnabled} - /> - )} - {supports2WayTalk && ( - { - setMic(!mic); - if (!mic && !audio) { - setAudio(true); - } - }} - disabled={!cameraEnabled} - /> - )} - {supportsAudioOutput && preferredLiveMode != "jsmpeg" && ( - setAudio(!audio)} - disabled={!cameraEnabled} - /> - )} - + {fullscreen && ( + + )} + {supportsFullscreen && ( + -
-
+ )} + {!isIOS && !isFirefox && preferredLiveMode != "jsmpeg" && ( + { + if (!pip) { + setPip(true); + } else { + document.exitPictureInPicture(); + setPip(false); + } + }} + disabled={!cameraEnabled} + /> + )} + {supports2WayTalk && ( + { + setMic(!mic); + if (!mic && !audio) { + setAudio(true); + } + }} + disabled={!cameraEnabled} + /> + )} + {supportsAudioOutput && preferredLiveMode != "jsmpeg" && ( + setAudio(!audio)} + disabled={!cameraEnabled} + /> + )} + +
- - - - - -

{label}

-
-
- + + + + + +

{label}

+
+
); } @@ -961,59 +955,56 @@ function PtzControlPanel({ )} {ptz?.features?.includes("pt-r-fov") && ( - - - - - - -

- {clickOverlay - ? t("ptz.move.clickMove.disable") - : t("ptz.move.clickMove.enable")} -

-
-
-
+ + + + + +

+ {clickOverlay + ? t("ptz.move.clickMove.disable") + : t("ptz.move.clickMove.enable")} +

+
+
)} {(ptz?.presets?.length ?? 0) > 0 && ( - + - - - - - e.preventDefault()} - > - {ptz?.presets.map((preset) => ( - sendPtz(`preset_${preset}`)} - > - {preset} - - ))} - - + + +

{t("ptz.presets")}

-
+ + e.preventDefault()} + > + {ptz?.presets.map((preset) => ( + sendPtz(`preset_${preset}`)} + > + {preset} + + ))} + + )}
); @@ -1401,9 +1392,11 @@ function FrigateCameraFeatures({ }} > - {Object.keys(camera.live.streams).find( - (key) => camera.live.streams[key] === streamName, - )} + + {Object.keys(camera.live.streams).find( + (key) => camera.live.streams[key] === streamName, + )} + @@ -1733,9 +1726,11 @@ function FrigateCameraFeatures({ }} > - {Object.keys(camera.live.streams).find( - (key) => camera.live.streams[key] === streamName, - )} + + {Object.keys(camera.live.streams).find( + (key) => camera.live.streams[key] === streamName, + )} +