Found on 25 August 2026 trying to answer a direct question — "is the encoder dropping less at 120 fps?" — and discovering the app cannot be asked it.
What the hardware offers, and what the app keeps
AVFoundationCaptureDevice.enumerateCapability() collapses the format list to the best rate per (lens, resolution):
letkey="\(lens.rawValue)-\(dims.width)x\(dims.height)"iflet existing =best[key], existing.fps >= maxRate {continue}best[key]= modeOn an iPhone 16 the wide camera actually offers, measured on the device:
1440x1080 @ 60 fps YES intrinsics
1920x1080 @ 30 fps YES intrinsics
1920x1080 @ 60 fps YES intrinsics
1920x1080 @ 120 fps YES intrinsics
1920x1080 @ 240 fps NO intrinsics
⛔ Four of those five are discarded.claimed keeps only 1920x1080 @ 240 for that lens and resolution, so 1080p120, 1080p60 and 1080p30 do not exist as far as the rest of the application is concerned.
What that does in practice
AppModel.remeasure(atMost: 120) filters capability.claimed on fps <= 120 and takes the best remaining. With every 1080p rate below 240 collapsed away, the best thing left is a different resolution entirely:
requested cap 120 fps → selected 3840x2160 @ 60
⛔ Asking for 120 fps silently gives you 4K60, which is the direction REQ-RES-1 rules out in as many words — target 1080p at the highest sustainable rate, and do not reach for 4K. And it performs as you would expect: on that mode the app realised 43.1 fps against a claimed 60, with a 8703 ms gap and 190 encoder-busy drops.
⚠ The comment explaining the collapse is right about the wrong thing."A device reports dozens of formats that differ only in pixel encoding and photo capability; the capability card is about geometry and rate." True — for the capability card, which is one sentence on A1. It is not true for mode selection, and the same collapsed list serves both.
⛔ And it hides the one mode that solves #19
The intrinsics table above is the sharp part. 1920x1080 @ 240 is the only 1080p row that does not deliver an intrinsic matrix, and #19 is open precisely because the declaration overclaims intrinsics: per_frame at that rate. 1920x1080 @ 120 delivers them — and is exactly the row the collapse throws away.
So the trade a user might reasonably want to make — half the frame rate, but real per-frame intrinsics — is not expressible anywhere in this application, and the mode that would make it is invisible.
Worth deciding
- Keep the collapsed list for the capability card, and carry the full enumeration for selection? The two want different things from the same walk.
- If the full list is kept, what does the user-facing choice look like — a rate picker, or a declared preference like "prefer intrinsics over rate"?
- ⚠
enumerateCapability runs on every refreshCapability(); keeping every format grows the declaration, which is already sized in #98's findings against a 64 KiB origination queue. Check the declaration still fits before widening what is declared.
Exit criterion
⛔ A rate the hardware offers can be selected and armed. Specifically: 1920x1080 @ 120 is reachable, arms, and reports per-frame intrinsics on the Capture — and asking for at most 120 fps never returns a 4K mode.
What the hardware offers, and what the app keeps
AVFoundationCaptureDevice.enumerateCapability()collapses the format list to the best rate per (lens, resolution):On an iPhone 16 the wide camera actually offers, measured on the device:
⛔ Four of those five are discarded.
claimedkeeps only1920x1080 @ 240for that lens and resolution, so 1080p120, 1080p60 and 1080p30 do not exist as far as the rest of the application is concerned.What that does in practice
AppModel.remeasure(atMost: 120)filterscapability.claimedonfps <= 120and takes the best remaining. With every 1080p rate below 240 collapsed away, the best thing left is a different resolution entirely:⛔ Asking for 120 fps silently gives you 4K60, which is the direction REQ-RES-1 rules out in as many words — target 1080p at the highest sustainable rate, and do not reach for 4K. And it performs as you would expect: on that mode the app realised 43.1 fps against a claimed 60, with a 8703 ms gap and 190 encoder-busy drops.
⚠ The comment explaining the collapse is right about the wrong thing."A device reports dozens of formats that differ only in pixel encoding and photo capability; the capability card is about geometry and rate." True — for the capability card, which is one sentence on A1. It is not true for mode selection, and the same collapsed list serves both.
⛔ And it hides the one mode that solves #19
The intrinsics table above is the sharp part.
1920x1080 @ 240is the only 1080p row that does not deliver an intrinsic matrix, and #19 is open precisely because the declaration overclaimsintrinsics: per_frameat that rate.1920x1080 @ 120delivers them — and is exactly the row the collapse throws away.So the trade a user might reasonably want to make — half the frame rate, but real per-frame intrinsics — is not expressible anywhere in this application, and the mode that would make it is invisible.
Worth deciding
enumerateCapabilityruns on everyrefreshCapability(); keeping every format grows the declaration, which is already sized in #98's findings against a 64 KiB origination queue. Check the declaration still fits before widening what is declared.Exit criterion
⛔ A rate the hardware offers can be selected and armed. Specifically:
1920x1080 @ 120is reachable, arms, and reports per-frame intrinsics on the Capture — and asking for at most 120 fps never returns a 4K mode.