mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-25 22:38:58 +03:00
* add onboarding wizard for new users * 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. * clean up * add light/dark mode icon switcher * use yml as default config file extension when not found * i18n tweaks * gate the setup wizard on cameras instead of a config key * render setup wizard steps by key * share the setup wizard e2e helpers and mock users * add an account step to the setup wizard * add setup wizard account step e2e coverage * cover the account step's restart behavior * button consistency * fix test * docs * fixes
256 lines
8.3 KiB
TypeScript
256 lines
8.3 KiB
TypeScript
/**
|
|
* Setup wizard hardware tests -- HIGH tier.
|
|
*
|
|
* Covers the detector step's probed radio list and the models: payload it
|
|
* writes, the model-required deferral for onnx hardware, the hwaccel step's
|
|
* Auto option writing the preset derived from the chosen hardware, and the
|
|
* completion screen only restarting when a saved step requires it.
|
|
*/
|
|
|
|
import { test, expect } from "../../fixtures/frigate-test";
|
|
import type { Page } from "@playwright/test";
|
|
import { gotoDetectorStep, installFirstRun } from "../../helpers/setup-wizard";
|
|
|
|
const NVIDIA_HARDWARE = [
|
|
{
|
|
key: "onnx:nvidia",
|
|
detector: "onnx",
|
|
name: "NVIDIA GeForce RTX 3060",
|
|
units: [{ device: "onnx:0", label: "NVIDIA GeForce RTX 3060" }],
|
|
count: 1,
|
|
unlimited: true,
|
|
},
|
|
{
|
|
key: "cpu",
|
|
detector: "cpu",
|
|
name: "CPU",
|
|
units: [{ device: "cpu", label: "CPU" }],
|
|
count: 1,
|
|
unlimited: true,
|
|
},
|
|
];
|
|
|
|
type SavedConfig = {
|
|
config_data?: {
|
|
models?: { devices: string[]; path?: string }[];
|
|
detect?: { enabled?: boolean };
|
|
ffmpeg?: { hwaccel_args?: string | string[] };
|
|
};
|
|
};
|
|
|
|
async function captureSaves(page: Page): Promise<SavedConfig[]> {
|
|
const saves: SavedConfig[] = [];
|
|
await page.route("**/api/config/set**", (route) => {
|
|
saves.push(route.request().postDataJSON() as SavedConfig);
|
|
return route.fulfill({ json: { success: true, require_restart: true } });
|
|
});
|
|
return saves;
|
|
}
|
|
|
|
async function captureRestarts(page: Page): Promise<string[]> {
|
|
const calls: string[] = [];
|
|
await page.route("**/api/restart", (route) => {
|
|
calls.push(route.request().url());
|
|
return route.fulfill({ json: { success: true, message: "Restarting" } });
|
|
});
|
|
return calls;
|
|
}
|
|
|
|
test.describe("setup wizard hardware @high @mobile", () => {
|
|
test("lists probed hardware and writes a models config", async ({
|
|
frigateApp,
|
|
page,
|
|
}) => {
|
|
await installFirstRun(frigateApp, page, {
|
|
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);
|
|
|
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
|
await gotoDetectorStep(page);
|
|
|
|
// the default hardware mock reports two Corals, an Intel GPU, and the CPU
|
|
await expect(
|
|
page.getByRole("radio", { name: /Coral EdgeTPU \(PCIe\) \(2\)/ }),
|
|
).toBeChecked();
|
|
await expect(page.getByText("Recommended")).toBeVisible();
|
|
|
|
await page.getByRole("button", { name: "Next" }).click();
|
|
|
|
await expect(page.getByText("Hardware Acceleration")).toBeVisible();
|
|
|
|
const detectorSave = saves.find((save) => save.config_data?.models);
|
|
expect(detectorSave?.config_data?.models).toEqual([
|
|
{ devices: ["edgetpu:pci:0"] },
|
|
]);
|
|
expect(detectorSave?.config_data?.detect).toEqual({ enabled: true });
|
|
|
|
// 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);
|
|
expect(hwaccelSave?.config_data?.ffmpeg).toEqual({
|
|
hwaccel_args: "preset-vaapi",
|
|
});
|
|
|
|
// the saved steps only take effect after a restart
|
|
const restarts = await captureRestarts(page);
|
|
await page.getByRole("button", { name: "Skip" }).click();
|
|
await expect(page.getByText("You're done!")).toBeVisible();
|
|
|
|
await expect(
|
|
page.getByText("Frigate needs to restart to apply your settings"),
|
|
).toBeVisible();
|
|
await page.getByRole("button", { name: "Apply & Restart" }).click();
|
|
|
|
await expect(page.getByText("Starting Frigate...")).toBeVisible();
|
|
expect(restarts).toHaveLength(1);
|
|
});
|
|
|
|
test("defers model setup for onnx hardware without Frigate+", async ({
|
|
frigateApp,
|
|
page,
|
|
}) => {
|
|
await installFirstRun(frigateApp, page, {
|
|
hardware: NVIDIA_HARDWARE,
|
|
});
|
|
const saves = await captureSaves(page);
|
|
|
|
await frigateApp.gotoAndWait("/", "text=Welcome to Frigate");
|
|
await gotoDetectorStep(page);
|
|
|
|
await expect(
|
|
page.getByRole("radio", { name: /NVIDIA GeForce RTX 3060/ }),
|
|
).toBeChecked();
|
|
|
|
await page
|
|
.getByRole("button", { name: "Continue without detection" })
|
|
.click();
|
|
|
|
// advances without touching the config
|
|
await expect(page.getByText("Hardware Acceleration")).toBeVisible();
|
|
expect(saves.filter((save) => save.config_data?.models)).toHaveLength(0);
|
|
|
|
// nothing derived and nothing saved, so finishing needs no restart
|
|
const restarts = await captureRestarts(page);
|
|
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 done!")).toBeVisible();
|
|
|
|
await expect(
|
|
page.getByText("Frigate needs to restart to apply your settings"),
|
|
).toBeHidden();
|
|
await page.getByRole("button", { name: "Go to Live View" }).click();
|
|
|
|
// hands off without restarting, and the wizard does not come back
|
|
await expect(page.getByText("Welcome to Frigate")).toBeHidden();
|
|
expect(restarts).toHaveLength(0);
|
|
});
|
|
|
|
test("offers only the presets the hardware supports", async ({
|
|
frigateApp,
|
|
page,
|
|
}) => {
|
|
await installFirstRun(frigateApp, page, {
|
|
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 installFirstRun(frigateApp, page, {
|
|
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();
|
|
|
|
await expect(
|
|
page.getByRole("radio", { name: "NVIDIA Jetson" }),
|
|
).toBeVisible();
|
|
await page.getByRole("button", { name: "Next" }).click();
|
|
|
|
// no camera was added, so there is no codec to match
|
|
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,
|
|
}) => {
|
|
await installFirstRun(frigateApp, page);
|
|
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();
|
|
|
|
await page.getByRole("radio", { name: "None (software decoding)" }).click();
|
|
await page.getByRole("button", { name: "Next" }).click();
|
|
|
|
const hwaccelSave = saves.find((save) => save.config_data?.ffmpeg);
|
|
expect(hwaccelSave?.config_data?.ffmpeg).toEqual({ hwaccel_args: [] });
|
|
});
|
|
});
|