Add Apple compatibility switch to the camera wizard (#24115)

* add apple compatibility switch to the camera wizard

* don't require every record stream to be h265
This commit is contained in:
Josh Hawkins
2026-09-12 07:30:04 -06:00
committed by Nicolas Mowen
parent fb8ab56c41
commit 98706482f5
7 changed files with 430 additions and 2 deletions
@@ -23,6 +23,7 @@ import type {
import {
processCameraName,
calculateDetectDimensions,
hevcRecordingStreamId,
} from "@/utils/cameraUtil";
import { cn } from "@/lib/utils";
@@ -185,6 +186,11 @@ export default function CameraWizardDialog({
wizardData.cameraName,
);
// re-checked here: roles and codecs may have changed since it was set
const appleCompatibility =
!!wizardData.appleCompatibility &&
!!hevcRecordingStreamId(wizardData.streams);
// Convert wizard data to Frigate config format
const configData: CameraConfigData = {
cameras: {
@@ -192,6 +198,7 @@ export default function CameraWizardDialog({
enabled: true,
...(friendlyName && { friendly_name: friendlyName }),
ffmpeg: {
...(appleCompatibility && { apple_compatibility: true }),
inputs: wizardData.streams.map((stream, index) => {
if (stream.restream) {
const go2rtcStreamName =
@@ -26,7 +26,7 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import { isMobile } from "react-device-detect";
import { isIOS, isMobile, isSafari } from "react-device-detect";
import {
LuInfo,
LuExternalLink,
@@ -53,6 +53,7 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { hevcRecordingStreamId } from "@/utils/cameraUtil";
// Recording the sub stream from the same stream as record would just
// re-record the main stream, so the two roles are mutually exclusive.
@@ -389,6 +390,22 @@ export default function Step3StreamConfig({
const hasDetectRole = streams.some((s) => s.roles.includes("detect"));
const appleCompatibilityStreamId = useMemo(
() => hevcRecordingStreamId(streams),
[streams],
);
useEffect(() => {
// undefined, not false: a deliberate toggle-off must not be re-seeded
if (
(isSafari || isIOS) &&
appleCompatibilityStreamId &&
wizardData.appleCompatibility === undefined
) {
onUpdate({ appleCompatibility: true });
}
}, [appleCompatibilityStreamId, wizardData.appleCompatibility, onUpdate]);
return (
<div className="space-y-6">
<div className="text-sm text-secondary-foreground">
@@ -778,7 +795,7 @@ export default function Step3StreamConfig({
</PopoverContent>
</Popover>
</div>
<div className="rounded-lg bg-background p-3">
<div className="space-y-3 rounded-lg bg-background p-3">
<div className="flex items-center justify-between">
<span className="text-sm">
{t("cameraWizard.step3.go2rtc")}
@@ -788,6 +805,27 @@ export default function Step3StreamConfig({
onCheckedChange={() => setRestream(stream.id)}
/>
</div>
{appleCompatibilityStreamId === stream.id && (
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<div className="text-sm">
{t("cameraWizard.step3.appleCompatibility.title")}
</div>
<p className="text-xs text-muted-foreground">
{t(
"cameraWizard.step3.appleCompatibility.description",
)}
</p>
</div>
<Switch
checked={wizardData.appleCompatibility ?? false}
onCheckedChange={(checked) =>
onUpdate({ appleCompatibility: checked })
}
/>
</div>
)}
</div>
</div>
</CardContent>
@@ -268,6 +268,7 @@ export default function Step4Validation({
customUrl: wizardData.customUrl,
streams: wizardData.streams,
hasBackchannel: wizardData.hasBackchannel,
appleCompatibility: wizardData.appleCompatibility,
onvif: wizardData.onvif,
};