mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-07 15:22:48 +03:00
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:
@@ -1,6 +1,7 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -8,6 +9,11 @@ import { toast } from "sonner";
|
||||
import axios from "axios";
|
||||
import useSWR from "swr";
|
||||
|
||||
const EVENTS = "events";
|
||||
const CONTINUOUS = "continuous";
|
||||
|
||||
const MODES = [EVENTS, CONTINUOUS] as const;
|
||||
|
||||
type SetupRecordingProps = {
|
||||
cameraNames: string[];
|
||||
onNext: () => void;
|
||||
@@ -23,6 +29,7 @@ export default function SetupRecording({
|
||||
}: SetupRecordingProps) {
|
||||
const { t } = useTranslation(["views/setup"]);
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [mode, setMode] = useState<string>(EVENTS);
|
||||
const [retentionDays, setRetentionDays] = useState(10);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
@@ -41,14 +48,18 @@ export default function SetupRecording({
|
||||
const handleSave = useCallback(async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const record: Record<string, unknown> = { enabled };
|
||||
|
||||
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".
|
||||
record.continuous = { days: mode === CONTINUOUS ? retentionDays : 0 };
|
||||
}
|
||||
|
||||
await axios.put("config/set", {
|
||||
config_data: {
|
||||
record: {
|
||||
enabled,
|
||||
alerts: { retain: { days: retentionDays } },
|
||||
detections: { retain: { days: retentionDays } },
|
||||
},
|
||||
},
|
||||
config_data: { record },
|
||||
requires_restart: 1,
|
||||
});
|
||||
onNext();
|
||||
@@ -57,7 +68,7 @@ export default function SetupRecording({
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [enabled, retentionDays, onNext, t]);
|
||||
}, [enabled, mode, retentionDays, onNext, t]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 py-4">
|
||||
@@ -89,6 +100,36 @@ export default function SetupRecording({
|
||||
|
||||
{enabled && (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>{t("setupWizard.recording.modeLabel")}</Label>
|
||||
<RadioGroup value={mode} onValueChange={setMode}>
|
||||
{MODES.map((option) => (
|
||||
<div key={option} className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value={option}
|
||||
id={`recording-mode-${option}`}
|
||||
className={
|
||||
mode === option
|
||||
? "bg-selected from-selected/50 to-selected/90 text-selected"
|
||||
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`recording-mode-${option}`}
|
||||
className="cursor-pointer text-sm font-medium"
|
||||
>
|
||||
{t(`setupWizard.recording.modes.${option}.label`)}
|
||||
</label>
|
||||
</div>
|
||||
<p className="ml-6 text-xs text-muted-foreground">
|
||||
{t(`setupWizard.recording.modes.${option}.description`)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="retention-days">
|
||||
{t("setupWizard.recording.retentionDays")}
|
||||
@@ -99,21 +140,30 @@ 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
|
||||
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))
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(`setupWizard.recording.retentionHint.${mode}`)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{freeGb !== null && estimatedDays !== null && cameraCount > 0 && (
|
||||
<p className="rounded-md bg-muted p-3 text-sm text-muted-foreground">
|
||||
{t("setupWizard.recording.storageEstimate", {
|
||||
free: freeGb,
|
||||
days: estimatedDays,
|
||||
cameras: cameraCount,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{mode === CONTINUOUS &&
|
||||
freeGb !== null &&
|
||||
estimatedDays !== null &&
|
||||
cameraCount > 0 && (
|
||||
<p className="rounded-md bg-muted p-3 text-sm text-muted-foreground">
|
||||
{t("setupWizard.recording.storageEstimate", {
|
||||
free: freeGb,
|
||||
days: estimatedDays,
|
||||
cameras: cameraCount,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user