diff --git a/frigate/test/test_hwaccel_recommendation.py b/frigate/test/test_hwaccel_recommendation.py index 8cd9dae44d..e334296ad3 100644 --- a/frigate/test/test_hwaccel_recommendation.py +++ b/frigate/test/test_hwaccel_recommendation.py @@ -121,8 +121,7 @@ class TestIntelGeneration(HwaccelRecommendationTestCase): self.assertEqual(self.recommend(["openvino:GPU"], codecs={"h264"}), "intel-qsv") def test_gen13_prefers_qsv_for_mixed_codecs(self): - # every camera resolves the family to its own codec, so mixed streams - # are no longer a reason to drop to vaapi + # each camera resolves the family to its own codec self.write_cpuinfo("13th Gen Intel(R) Core(TM) i5-13500") self.assertEqual( self.recommend(["openvino:GPU"], codecs={"h264", "h265"}), "intel-qsv" diff --git a/frigate/util/hwaccel.py b/frigate/util/hwaccel.py index cdfbc8d08d..88ce16ce73 100644 --- a/frigate/util/hwaccel.py +++ b/frigate/util/hwaccel.py @@ -26,7 +26,6 @@ logger = logging.getLogger(__name__) # root the /proc reads use, so tests can point them at a fixture tree PROC_ROOT = "/proc" -# stands in for the codec of a preset that decodes anything ANY_CODEC = "any" # a Raspberry Pi has no detection hardware of its own, so it gets a key here @@ -35,11 +34,10 @@ RASPBERRY_PI = "raspberrypi" # ffprobe names h265 streams hevc CODEC_ALIASES = {"hevc": "h265"} -# marketing name of a newer Intel CPU, e.g. "13th Gen Intel(R) Core(TM) i5-13500" +# e.g. "13th Gen Intel(R) Core(TM) i5-13500" INTEL_GEN_PATTERN = re.compile(r"(\d+)th Gen") -# Core Ultra dropped that prefix and is newer than every numbered generation +# Core Ultra dropped the generation prefix and is newer than all of them INTEL_ULTRA_PATTERN = re.compile(r"Core\(TM\) Ultra") -# stands in for a generation newer than any numbered one INTEL_GEN_LATEST = 99 # per the hwaccel docs, gen13+ and Arc prefer qsv while older is safest on @@ -47,8 +45,7 @@ INTEL_GEN_LATEST = 99 INTEL_QSV_MIN_GEN = 13 INTEL_QSV_SUPPORTED_GEN = 8 -# detection hardware whose GPU also decodes video, in recommendation priority -# order +# decode capable detection hardware, in recommendation priority order DECODE_HARDWARE = ( "onnx:nvidia", "tensorrt", @@ -150,7 +147,6 @@ def _intel_families(generation: int | None) -> list[HwaccelFamily]: if generation is not None and generation >= INTEL_QSV_MIN_GEN: return [FAMILY_QSV, FAMILY_VAAPI] - # gen8 to gen12 can do either, and the docs call vaapi the safer default return [FAMILY_VAAPI, FAMILY_QSV] @@ -253,8 +249,8 @@ def hwaccel_options( def _recommend(families: list[HwaccelFamily], codecs_known: bool) -> str: """Pick the family to default to out of the ones this hardware can use.""" if not codecs_known: - # nothing says which codec a camera will send, and a codec specific - # family would have to guess one, so anything that decodes them all wins + # a codec specific family would have to guess a codec for cameras + # that do not exist yet for family in families: if ANY_CODEC in family.presets: return family.key diff --git a/web/e2e/specs/setup/wizard-hardware.spec.ts b/web/e2e/specs/setup/wizard-hardware.spec.ts index 4f1456ce8f..e64d4677ff 100644 --- a/web/e2e/specs/setup/wizard-hardware.spec.ts +++ b/web/e2e/specs/setup/wizard-hardware.spec.ts @@ -57,16 +57,12 @@ async function captureRestarts(page: Page): Promise { } async function gotoDetectorStep(page: Page) { - // welcome -> camera -> detector await page.getByRole("button", { name: "Get Started" }).click(); await expect(page.getByText("Add Your First Camera")).toBeVisible(); - // The camera step's Next button only renders once addedCameras is - // non-empty, and that state is local to the step (it isn't seeded from - // the cameras already in config). Opening and cancelling the add-camera - // dialog is enough: its close handler force-refetches /api/config, which - // picks up the cameras already present in the mocked config and reveals - // Next -- see SetupCamera.tsx's handleClose. + // the camera step's Next only renders once its local addedCameras fills, + // which SetupCamera does by refetching config when the dialog closes, so + // opening and cancelling the dialog is what reveals Next await page.getByRole("button", { name: "Add Camera" }).click(); await page.getByRole("button", { name: "Cancel" }).click(); @@ -112,7 +108,6 @@ test.describe("setup wizard hardware @high @mobile", () => { await page.getByRole("button", { name: "Next" }).click(); - // detector -> hwaccel await expect(page.getByText("Hardware Acceleration")).toBeVisible(); const detectorSave = saves.find((save) => save.config_data?.models); @@ -121,8 +116,7 @@ test.describe("setup wizard hardware @high @mobile", () => { ]); expect(detectorSave?.config_data?.detect).toEqual({ enabled: true }); - // Auto names the family derived from the chosen hardware and writes its - // preset; VAAPI decodes any codec, so one global value covers every camera + // 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(); @@ -131,8 +125,7 @@ test.describe("setup wizard hardware @high @mobile", () => { hwaccel_args: "preset-vaapi", }); - // hwaccel -> recording (skipped) -> complete, where the saved steps only - // take effect after a restart and the primary action says so + // 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 All Set!")).toBeVisible(); @@ -171,8 +164,7 @@ test.describe("setup wizard hardware @high @mobile", () => { await expect(page.getByText("Hardware Acceleration")).toBeVisible(); expect(saves.filter((save) => save.config_data?.models)).toHaveLength(0); - // with nothing derived, Auto advances without writing; skipping recording - // leaves no pending saves, so finishing needs no restart + // 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(); @@ -184,8 +176,8 @@ test.describe("setup wizard hardware @high @mobile", () => { ).toBeHidden(); await page.getByRole("button", { name: "Go to Live View" }).click(); - // hands off without restarting; the mocked config still reports - // onboarding incomplete, so the reload lands back on the wizard + // the mocked config still reports onboarding incomplete, so the reload + // lands back on the wizard await expect(page.getByText("Welcome to Frigate")).toBeVisible(); expect(restarts).toHaveLength(0); const finishSave = saves.find((save) => save.config_data?.onboarding); @@ -257,15 +249,12 @@ test.describe("setup wizard hardware @high @mobile", () => { 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 + // 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", diff --git a/web/src/components/settings/CameraWizardDialog.tsx b/web/src/components/settings/CameraWizardDialog.tsx index caf02bcd03..f1cd4d8788 100644 --- a/web/src/components/settings/CameraWizardDialog.tsx +++ b/web/src/components/settings/CameraWizardDialog.tsx @@ -74,8 +74,7 @@ const STEPS = [ type CameraWizardDialogProps = { open: boolean; onClose: () => void; - // reports each camera the wizard saves, so callers can act on the streams - // that were probed without probing them again + // lets callers reuse what was probed here instead of probing again onCameraAdded?: (camera: { name: string; detectCodec?: string }) => void; }; diff --git a/web/src/components/setup/SetupCamera.tsx b/web/src/components/setup/SetupCamera.tsx index ce740764ce..b87d989245 100644 --- a/web/src/components/setup/SetupCamera.tsx +++ b/web/src/components/setup/SetupCamera.tsx @@ -25,8 +25,8 @@ export default function SetupCamera({ onNext, onBack }: SetupCameraProps) { const handleClose = useCallback(() => { setShowWizard(false); - // Delay to let CameraWizardDialog's config save and go2rtc setup complete, - // then force a fresh fetch from the server (bypassing SWR cache) + // the dialog's config save and go2rtc setup need a moment to land, and + // the SWR cache would serve the pre-save config setTimeout(() => { fetch(`${window.baseUrl || ""}api/config`) .then((res) => res.json()) @@ -37,9 +37,7 @@ export default function SetupCamera({ onNext, onBack }: SetupCameraProps) { setAddedCameras(cameraNames); } }) - .catch(() => { - // Fetch failed, stay on this step - }); + .catch(() => {}); }, 1000); }, [mutateConfig, addedCameras]); diff --git a/web/src/components/setup/SetupComplete.tsx b/web/src/components/setup/SetupComplete.tsx index 7616ac166e..4e6a89135f 100644 --- a/web/src/components/setup/SetupComplete.tsx +++ b/web/src/components/setup/SetupComplete.tsx @@ -87,7 +87,6 @@ export default function SetupComplete({ const handleFinish = useCallback(async () => { setFinishing(true); try { - // Mark setup as complete await axios.put("config/set", { config_data: { onboarding: { setup_complete: true }, @@ -95,8 +94,7 @@ export default function SetupComplete({ requires_restart: 0, }); - // camera adds were applied live, so with no pending requires_restart - // saves the wizard can hand off without restarting + // camera adds were applied live, so nothing is waiting on a restart if (!restartRequired) { window.location.href = window.baseUrl || "/"; return; @@ -104,10 +102,8 @@ export default function SetupComplete({ setRestarting(true); - // Trigger restart await axios.post("restart"); - // Poll for server availability with max retries let retries = 0; const maxRetries = 60; // 2 minutes max pollRef.current = setInterval(async () => { @@ -116,7 +112,6 @@ export default function SetupComplete({ if (pollRef.current) { clearInterval(pollRef.current); } - // Give up polling, redirect anyway window.location.href = window.baseUrl || "/"; return; } @@ -129,7 +124,7 @@ export default function SetupComplete({ window.location.href = window.baseUrl || "/"; } } catch { - // Server not ready yet + // not back yet } }, 2000); } catch { diff --git a/web/src/components/setup/SetupDetector.tsx b/web/src/components/setup/SetupDetector.tsx index 6dce2e0994..a3710baa91 100644 --- a/web/src/components/setup/SetupDetector.tsx +++ b/web/src/components/setup/SetupDetector.tsx @@ -20,8 +20,8 @@ import { LuExternalLink } from "react-icons/lu"; import { toast } from "sonner"; import useSWR from "swr"; -// detectors that ship no default model; configuring them without one leaves -// the detector unable to start +// these ship no default model, so configuring one without a model leaves the +// detector unable to start const MODEL_REQUIRED_DETECTORS = ["onnx", "tensorrt"]; const CPU_FALLBACK: DetectionHardware[] = [ @@ -64,7 +64,7 @@ export default function SetupDetector({ }); const plusEnabled = Boolean(config?.plus?.enabled); - // the cpu is always probed, so an empty or failed probe leaves a fallback + // the cpu is always probed, so an empty list means the probe failed const options = useMemo( () => (hardware && hardware.length > 0 ? hardware : CPU_FALLBACK), [hardware], @@ -121,7 +121,6 @@ export default function SetupDetector({ const handleSave = useCallback(async () => { if (needsModel && !plusModelId) { - // no model to run on this hardware yet, so leave the config untouched onSkip(selected.key); return; } diff --git a/web/src/components/setup/SetupHwAccel.tsx b/web/src/components/setup/SetupHwAccel.tsx index 38141f0f84..f0775292f4 100644 --- a/web/src/components/setup/SetupHwAccel.tsx +++ b/web/src/components/setup/SetupHwAccel.tsx @@ -11,7 +11,6 @@ import useSWR from "swr"; const AUTO = "auto"; const NONE = "none"; -// the codec key of a preset that decodes anything const ANY_CODEC = "any"; // ffprobe names h265 streams hevc @@ -24,11 +23,9 @@ function normalizeCodec(codec: string): string { type SetupHwAccelProps = { detectorHardwareKey?: string; - // detect stream codec of each camera added in the wizard, keyed by camera - // name. hwaccel only applies to the detect stream. + // camera name -> detect stream codec, the only stream hwaccel applies to detectCodecs: Record; - // saved reports whether a config write happened, so the wizard knows - // whether finishing requires a restart + // saved tells the wizard whether finishing needs a restart onNext: (saved: boolean) => void; onBack: () => void; onSkip: () => void; @@ -99,14 +96,11 @@ export default function SetupHwAccel({ return { ffmpeg: { hwaccel_args: shared } }; } - // this family decodes one codec per preset, so each camera needs the - // preset matching its own detect stream const perCamera = cameraCodecs .map((entry) => ({ ...entry, preset: family.presets[entry.codec] })) .filter((entry) => entry.preset); if (perCamera.length === 0) { - // no camera to match, so fall back to the family's first preset const fallback = Object.values(family.presets)[0]; return fallback ? { ffmpeg: { hwaccel_args: fallback } } : null; } @@ -114,12 +108,11 @@ export default function SetupHwAccel({ const presets = new Set(perCamera.map((entry) => entry.preset)); if (presets.size === 1 && perCamera.length === cameraCodecs.length) { - // every camera wants the same preset, so one global value says it return { ffmpeg: { hwaccel_args: [...presets][0] } }; } - // the global stays on auto, so cameras added later still get resolved - // at startup rather than inheriting one camera's codec + // the global stays on auto so cameras added later resolve at startup + // instead of inheriting one camera's codec return { cameras: Object.fromEntries( perCamera.map((entry) => [ @@ -137,13 +130,12 @@ export default function SetupHwAccel({ const configData = selected === NONE - ? // an empty string would make config/set delete the key (reviving the - // "auto" default), so an explicit no-hwaccel is an empty list + ? // an empty string would make config/set delete the key, reviving + // the "auto" default { ffmpeg: { hwaccel_args: [] } } : configFor(families.find((family) => family.key === key)); - // Auto with nothing derived writes nothing: the config default of "auto" - // stays in place and the backend decides at startup + // nothing to write leaves the config default of "auto" in place if (!configData) { onNext(false); return; diff --git a/web/src/components/setup/SetupRecording.tsx b/web/src/components/setup/SetupRecording.tsx index b8cbe3c99f..dc234df5ca 100644 --- a/web/src/components/setup/SetupRecording.tsx +++ b/web/src/components/setup/SetupRecording.tsx @@ -35,7 +35,6 @@ export default function SetupRecording({ const { data: stats } = useSWR("stats", { revalidateOnFocus: false }); - // Calculate storage estimate const storageInfo = stats?.service?.storage?.["/tmp/frigate/recordings"]; const freeGb = storageInfo ? Math.round(storageInfo.free / 1024) : null; const cameraCount = cameraNames.length; @@ -53,8 +52,7 @@ export default function SetupRecording({ if (enabled) { record.alerts = { retain: { days: retentionDays } }; record.detections = { retain: { days: retentionDays } }; - // continuous keeps every segment, so it is the one that needs turning - // on. Its default of 0 already means "only what was detected". + // written even when off, so switching modes back turns it off again record.continuous = { days: mode === CONTINUOUS ? retentionDays : 0 }; } @@ -140,8 +138,7 @@ export default function SetupRecording({ min={1} max={365} value={retentionDays} - // the spinner arrows are noise at this size, and the field is - // still typeable and arrow-key steppable without them + // drop the spinner arrows; typing and arrow keys still work className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" onChange={(e) => setRetentionDays(Math.max(1, parseInt(e.target.value) || 1)) diff --git a/web/src/pages/SetupWizard.tsx b/web/src/pages/SetupWizard.tsx index 0ace4d2e9f..f7db8255f9 100644 --- a/web/src/pages/SetupWizard.tsx +++ b/web/src/pages/SetupWizard.tsx @@ -24,10 +24,9 @@ type WizardState = { currentStep: number; cameraNames: string[]; detectorHardwareKey?: string; - // detect stream codec of each added camera, keyed by camera name + // camera name -> detect stream codec detectCodecs: Record; - // whether any step wrote config with requires_restart, which only takes - // effect after a restart (camera adds apply live and don't count) + // camera adds apply live, so they don't count toward needing a restart restartRequired: boolean; configuredSteps: { camera: boolean; diff --git a/web/src/types/hardware.ts b/web/src/types/hardware.ts index 8f31506a40..254e27ce6c 100644 --- a/web/src/types/hardware.ts +++ b/web/src/types/hardware.ts @@ -14,8 +14,7 @@ export type DetectionHardware = { export type HwaccelFamily = { key: string; - // the preset for each codec this family decodes, or a single "any" preset - // when it decodes every codec + // keyed by codec, or a single "any" preset when it decodes every codec presets: Record; };