diff --git a/web/e2e/specs/system-health.spec.ts b/web/e2e/specs/system-health.spec.ts index 0bb1cc1a6b..bbdda90e11 100644 --- a/web/e2e/specs/system-health.spec.ts +++ b/web/e2e/specs/system-health.spec.ts @@ -14,6 +14,17 @@ const NOW = Math.floor(Date.now() / 1000); // the fixture detector runs at 75.5 ms, above the live warning threshold const QUIET_STATS = { detectors: { cpu: { inference_speed: 10 } } }; +// the fixture has no go2rtc streams, which gives every camera a live view hint +const RESTREAMED = { + go2rtc: { + streams: { + front_door: ["rtsp://x"], + backyard: ["rtsp://x"], + garage: ["rtsp://x"], + }, + }, +}; + const ERROR_NOTICE = { id: "model_download_failed:yolo/model.onnx", kind: "model_download_failed", @@ -51,6 +62,7 @@ test.describe("System — Health tab @medium", () => { frigateApp, }) => { await frigateApp.installDefaults({ + config: RESTREAMED, stats: QUIET_STATS, notices: [ERROR_NOTICE, EVENT_NOTICE], }); @@ -84,6 +96,7 @@ test.describe("System — Health tab @medium", () => { for (const action of ["acknowledge", "mute"] as const) { test(`${action} posts and removes the row`, async ({ frigateApp }) => { await frigateApp.installDefaults({ + config: RESTREAMED, stats: QUIET_STATS, notices: [EVENT_NOTICE], }); @@ -126,7 +139,10 @@ test.describe("System — Health tab @medium", () => { } test("empty state with no notices", async ({ frigateApp }) => { - await frigateApp.installDefaults({ stats: QUIET_STATS }); + await frigateApp.installDefaults({ + config: RESTREAMED, + stats: QUIET_STATS, + }); await frigateApp.goto("/system#health"); await expect( @@ -329,6 +345,7 @@ test.describe("System — Health tab @medium", () => { frigateApp, }) => { await frigateApp.installDefaults({ + config: RESTREAMED, stats: QUIET_STATS, notices: [ERROR_NOTICE, EVENT_NOTICE], }); @@ -765,6 +782,7 @@ test.describe("System — Health notices sources @medium", () => { test("status bar problems stay out of the list", async ({ frigateApp }) => { test.skip(frigateApp.isMobile, "Status bar is desktop-only"); await frigateApp.installDefaults({ + config: RESTREAMED, stats: { service: { retention_unmet: true }, cameras: { front_door: { camera_fps: 0 } }, @@ -829,6 +847,28 @@ test.describe("System — Health notices sources @medium", () => { ).toBeVisible({ timeout: 15_000 }); }); + test("a camera without a go2rtc stream gets a live view hint", async ({ + frigateApp, + }) => { + await frigateApp.installDefaults({ + config: { go2rtc: { streams: { front_door: ["rtsp://x"] } } }, + stats: QUIET_STATS, + }); + await frigateApp.goto("/system#health"); + + const row = frigateApp.page.getByTestId( + "health-problem-config:live:no-go2rtc-stream:camera.backyard", + ); + await expect(row).toBeVisible({ timeout: 15_000 }); + await expect(row).toHaveAttribute("data-severity", "info"); + await expect(row).toContainText("lower frame rate and no audio"); + await expect( + frigateApp.page.getByTestId( + "health-problem-config:live:no-go2rtc-stream:camera.front_door", + ), + ).toHaveCount(0); + }); + test("a global config problem is not repeated per camera", async ({ frigateApp, }) => { @@ -893,7 +933,10 @@ test.describe("System — Health notices sources @medium", () => { test("empty state when stats, config, and registry are clean", async ({ frigateApp, }) => { - await frigateApp.installDefaults({ stats: QUIET_STATS }); + await frigateApp.installDefaults({ + config: RESTREAMED, + stats: QUIET_STATS, + }); await frigateApp.goto("/system#health"); await expect( @@ -1130,6 +1173,7 @@ test.describe("System — Health notices sources @medium", () => { }) => { test.skip(frigateApp.isMobile, "Status bar is desktop-only"); await frigateApp.installDefaults({ + config: RESTREAMED, stats: QUIET_STATS, notices: [EVENT_NOTICE], }); diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 5bbe7b80d1..72b4f169f7 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1997,6 +1997,9 @@ "genaiImageSourceRecordingsRecordDisabled": "Image source is set to 'recordings', but recording is disabled. Frigate will fall back to preview images.", "genaiImageSourceRecordingsRecordRuntimeDisabled": "Image source is set to 'recordings', but recording is currently turned off for this camera even though your config enables it. Frigate will fall back to preview images." }, + "live": { + "noGo2rtcStream": "Live view for this camera is using a basic player with a lower frame rate and no audio. Set up a go2rtc stream for this camera to get smoother video and audio." + }, "audio": { "noAudioRole": "No streams have the audio role defined. You must enable the audio role for audio detection to function." }, diff --git a/web/src/components/config-form/section-configs/live.ts b/web/src/components/config-form/section-configs/live.ts index 2e13527382..c3603b19f6 100644 --- a/web/src/components/config-form/section-configs/live.ts +++ b/web/src/components/config-form/section-configs/live.ts @@ -1,8 +1,24 @@ +import { isRestreamedStream } from "@/utils/liveTranscode"; import type { SectionConfigOverrides } from "./types"; const live: SectionConfigOverrides = { base: { sectionDocs: "/configuration/live", + messages: [ + { + key: "no-go2rtc-stream", + health: true, + messageKey: "configMessages.live.noGo2rtcStream", + severity: "info", + docLink: "/configuration/live", + condition: (ctx) => { + if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false; + return !Object.values(ctx.fullCameraConfig.live.streams).some( + (name) => isRestreamedStream(ctx.fullConfig, name), + ); + }, + }, + ], restartRequired: [], fieldOrder: ["streams", "transcode", "height", "quality"], fieldGroups: {},