resolve hwaccel per camera and clarify recording retention

The hwaccel step listed every preset Frigate ships, so an Intel box was offered Raspberry Pi and Rockchip decoding, and the codec specific presets (`preset-intel-qsv-h264` vs `-h265`) were offered as global values that break as soon as two cameras use different codecs. `/hardware/hwaccel` now returns the decoding families the probed hardware can actually use, each carrying a preset per codec, and the wizard resolves the family against the detect stream codec the camera wizard already probed: one global `ffmpeg.hwaccel_args` when every camera agrees, per-camera `cameras.<name>.ffmpeg.hwaccel_args` when they don't. The global stays on `auto` in that case so cameras added later still resolve at startup. A gen13+ Intel machine keeps its QuickSync recommendation with mixed h264 and h265 cameras instead of dropping to vaapi.

The recording step's "Days to retain recordings" only wrote alert and detection retention, and the storage estimate under it assumed continuous recording. It now asks what to record in plain language, writes `record.continuous.days` to match, shows the estimate only for continuous, and drops the spinner arrows on the number input.
This commit is contained in:
Josh Hawkins
2026-08-22 11:51:08 -05:00
parent bb5e9264b0
commit d02811d2b4
13 changed files with 815 additions and 178 deletions
+95 -4
View File
@@ -85,7 +85,19 @@ test.describe("setup wizard hardware @high @mobile", () => {
await frigateApp.installDefaults({
// the base config snapshot predates the onboarding key, hence the cast
config: { onboarding: { setup_complete: false } } as never,
hwaccel: { preset: "preset-vaapi" },
hwaccel: {
recommended: "vaapi",
available: [
{ key: "vaapi", presets: { any: "preset-vaapi" } },
{
key: "intel-qsv",
presets: {
h264: "preset-intel-qsv-h264",
h265: "preset-intel-qsv-h265",
},
},
],
},
});
const saves = await captureSaves(page);
@@ -109,8 +121,9 @@ test.describe("setup wizard hardware @high @mobile", () => {
]);
expect(detectorSave?.config_data?.detect).toEqual({ enabled: true });
// Auto shows the preset derived from the chosen hardware and writes it
await expect(page.getByText("preset-vaapi")).toBeVisible();
// Auto names the family derived from the chosen hardware and writes its
// preset; VAAPI decodes any codec, so one global value covers every camera
await expect(page.getByText("Will use VAAPI (Intel/AMD)")).toBeVisible();
await page.getByRole("button", { name: "Next" }).click();
const hwaccelSave = saves.find((save) => save.config_data?.ffmpeg);
@@ -161,7 +174,7 @@ test.describe("setup wizard hardware @high @mobile", () => {
// with nothing derived, Auto advances without writing; skipping recording
// leaves no pending saves, so finishing needs no restart
const restarts = await captureRestarts(page);
await expect(page.getByText("No compatible GPU found")).toBeVisible();
await expect(page.getByText("No supported video card found")).toBeVisible();
await page.getByRole("button", { name: "Next" }).click();
await page.getByRole("button", { name: "Skip" }).click();
await expect(page.getByText("You're All Set!")).toBeVisible();
@@ -181,6 +194,84 @@ test.describe("setup wizard hardware @high @mobile", () => {
});
});
test("offers only the presets the hardware supports", async ({
frigateApp,
page,
}) => {
await frigateApp.installDefaults({
config: { onboarding: { setup_complete: false } } as never,
hardware: NVIDIA_HARDWARE,
hwaccel: {
recommended: "nvidia",
available: [{ key: "nvidia", presets: { any: "preset-nvidia" } }],
},
});
await captureSaves(page);
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
await gotoDetectorStep(page);
await page
.getByRole("button", { name: "Continue without detection" })
.click();
await expect(page.getByText("Hardware Acceleration")).toBeVisible();
// an NVIDIA box has no business being offered Rockchip or Pi decoding
await expect(
page.getByRole("radio", { name: "CUDA (NVIDIA)" }),
).toBeVisible();
await expect(
page.getByRole("radio", { name: /Raspberry Pi/ }),
).toBeHidden();
await expect(page.getByRole("radio", { name: /Rockchip/ })).toBeHidden();
// Auto and None are always available
await expect(page.getByRole("radio", { name: "Auto" })).toBeVisible();
await expect(
page.getByRole("radio", { name: "None (software decoding)" }),
).toBeVisible();
});
test("a codec specific family falls back to h264 with no cameras", async ({
frigateApp,
page,
}) => {
await frigateApp.installDefaults({
config: { onboarding: { setup_complete: false } } as never,
hwaccel: {
recommended: "jetson",
available: [
{
key: "jetson",
presets: {
h264: "preset-jetson-h264",
h265: "preset-jetson-h265",
},
},
],
},
});
const saves = await captureSaves(page);
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
await gotoDetectorStep(page);
await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText("Hardware Acceleration")).toBeVisible();
// the family is named once, not split into codec variants the user would
// have to choose between
await expect(
page.getByRole("radio", { name: "NVIDIA Jetson" }),
).toBeVisible();
await page.getByRole("button", { name: "Next" }).click();
// no camera was added through the wizard, so there is no codec to match
// and the family's first preset stands in
const hwaccelSave = saves.find((save) => save.config_data?.ffmpeg);
expect(hwaccelSave?.config_data?.ffmpeg).toEqual({
hwaccel_args: "preset-jetson-h264",
});
});
test("None writes an explicit empty hwaccel list", async ({
frigateApp,
page,