diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index 954787a08a..84aa337071 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -38,7 +38,7 @@ Frigate [defaults to MSE](#why-does-frigate-prefer-mse-over-webrtc-for-live-view - The choices are saved **per device, per camera** in your browser's local storage. - **WebRTC is only selectable when it can actually work for that stream.** When it can't, the option is shown disabled with the reason inline, and a more detailed reason (the failing codecs, or why the connectivity check failed) is logged to your browser's console. Common reasons: - - **Not configured**: no `candidates` or `ice_servers` are set under `go2rtc.webrtc` (see [WebRTC extra configuration](#webrtc-extra-configuration)). + - **Not configured**: no `candidates` or `ice_servers` are set under `go2rtc.webrtc`, and the connectivity check failed with go2rtc's default candidates (see [WebRTC extra configuration](#webrtc-extra-configuration)). This is a setup note rather than an error, since live view works without WebRTC. - **Could not connect**: e.g. port `8555` isn't reachable, or a STUN/TURN server is misconfigured. Frigate runs a one-time WebRTC connectivity check when the Live view opens; the option may briefly show as "checking" while it runs. - **Unsupported video codec**: the stream's video codec can't be played over WebRTC in your browser, most commonly H.265/HEVC in Firefox or Edge. - **Unsupported audio codec**: WebRTC needs opus or G.711 audio, so a stream whose playback audio is only AAC (without an added opus/G.711 track) can't carry audio over WebRTC. See [Audio Support](#audio-support) for how to add one. diff --git a/web/e2e/specs/live-webrtc-availability.spec.ts b/web/e2e/specs/live-webrtc-availability.spec.ts index b36944138d..49fc5668bd 100644 --- a/web/e2e/specs/live-webrtc-availability.spec.ts +++ b/web/e2e/specs/live-webrtc-availability.spec.ts @@ -110,6 +110,15 @@ test.describe("WebRTC availability gating @critical", () => { // distinguishes enabled from disabled options. const mseOption = frigateApp.page.getByRole("option", { name: /MSE/ }); await expect(mseOption).not.toHaveAttribute("aria-disabled", "true"); + + // a missing setting, shown as a neutral note rather than an error + await expect(webrtcOption).toContainText( + "under go2rtc → webrtc → candidates", + ); + await expect(webrtcOption).toContainText( + "Optional: live view works without it.", + ); + await expect(webrtcOption.locator(".text-danger")).toHaveCount(0); }); test("the unavailable reason is logged to the console once", async ({ @@ -214,6 +223,62 @@ test.describe("WebRTC availability gating @critical", () => { await expect(webrtcOption).toContainText(/Checking WebRTC availability/i); }); + test("desktop: unset candidates run the probe before reporting not configured", async ({ + frigateApp, + }) => { + test.skip(frigateApp.isMobile, "Desktop dropdown only"); + + // Count probe connections. The probe fails fast against the preview + // server, which with go2rtc's default candidates means setup is missing. + let probes = 0; + await frigateApp.page.routeWebSocket("**/live/webrtc/api/ws**", (ws) => { + probes++; + ws.close(); + }); + + await frigateApp.installDefaults({ + config: { + go2rtc: { + streams: { front_door: ["rtsp://127.0.0.1:8554/front_door"] }, + }, + }, + }); + + await frigateApp.page.route("**/api/go2rtc/streams/front_door**", (route) => + route.fulfill({ + json: { + producers: [{ medias: ["video, recvonly, H264"] }], + consumers: [], + }, + }), + ); + + await frigateApp.goto("/#front_door"); + + const live = new LivePage(frigateApp.page, true); + await expect(live.backButton).toBeVisible({ timeout: 10_000 }); + + await frigateApp.page.locator("button:has(svg)").last().click(); + + const menu = frigateApp.page + .locator('[role="menu"], [data-radix-menu-content]') + .first(); + await expect(menu).toBeVisible({ timeout: 3_000 }); + await menu + .locator('div:has(> label[for="streaming-mode"]) [role="combobox"]') + .first() + .click(); + + const webrtcOption = frigateApp.page.getByRole("option", { + name: /WebRTC/, + }); + await expect(webrtcOption).toBeVisible({ timeout: 3_000 }); + await expect(webrtcOption).toContainText( + "under go2rtc → webrtc → candidates", + ); + expect(probes).toBeGreaterThan(0); + }); + test("desktop: JSMpeg is no longer offered in the technology selector", async ({ frigateApp, }) => { diff --git a/web/public/locales/en/views/live.json b/web/public/locales/en/views/live.json index c5b370cea0..c4f3f91b0b 100644 --- a/web/public/locales/en/views/live.json +++ b/web/public/locales/en/views/live.json @@ -187,7 +187,8 @@ }, "unavailable": { "browser": "Your browser does not support WebRTC.", - "not-configured": "WebRTC is not configured. Set go2rtc webrtc candidates or ice_servers.", + "not-configured": "To enable, add this server's IP:8555 under go2rtc → webrtc → candidates.", + "notConfiguredOptional": "Optional: live view works without it. WebRTC is mainly needed for two-way talk.", "unreachable": "WebRTC could not connect. Check that port 8555 is reachable and any STUN/TURN server is correct.", "video-codec": "This stream's video codec is not supported by WebRTC in this browser.", "audio-codec": "This stream's audio codec is not supported by WebRTC. Transcode to opus or G.711 with go2rtc.", diff --git a/web/src/components/player/StreamTechnologySelect.tsx b/web/src/components/player/StreamTechnologySelect.tsx index fd08185510..36042c4b7e 100644 --- a/web/src/components/player/StreamTechnologySelect.tsx +++ b/web/src/components/player/StreamTechnologySelect.tsx @@ -7,7 +7,7 @@ import { } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { LivePlayerMode, WebRTCUnavailableReason } from "@/types/live"; -import { LuX } from "react-icons/lu"; +import { LuInfo, LuX } from "react-icons/lu"; import { useTranslation } from "react-i18next"; type StreamTechnologySelectProps = { @@ -28,6 +28,7 @@ export default function StreamTechnologySelect({ const { t } = useTranslation(["views/live"]); const isChecking = webRTCUnavailableReason === "checking"; + const isNotConfigured = webRTCUnavailableReason === "not-configured"; return (