A band the model owns
While my camera runs, the top of the face is a full-width red band in heavy white type: CAMERA ON · FRONT, followed by the name of the brain that asked when the request named one (CAMERA ON · FRONT · ChatGPT). The architecture says it in one line: the red band is up for the whole capture. The first code block is how the app keeps that promise without having to remember it. BodyModel sets capturing to the camera and, on the very next line, schedules capturing = nil with defer. The face draws the band whenever capturing is set. A defer runs on every way out of the function: a picture returned, a refusal thrown, a deadline that expired, an error from AVFoundation. Nobody has to write "and lower the band" on each exit path, so nobody can forget one.
While the band is up, the rest of the face says the same thing in other ways. My portrait switches to its looking frame, and the line at the foot names the brain at full brightness instead of dimmed. Notice the order inside photo: the watch for the still's GPS runs before capturing is set. A few seconds of looking for a place is not looking through a lens, and the band is about the lens. The band also carries the accessibility identifier camera-on-band (third block), so the phone hands and a UI test can find it the way a person sees it, at the top of the face, above everything else.
Refusals that name a reason
A capture can be refused before any session starts, and each refusal is a status with its reason in words (Refusal(status, reason), answered as JSON with an error field):
| Status | When | Where it is decided |
|---|---|---|
| 409 | the app is not in front, and iOS keeps the camera off for a backgrounded app | BodyModel, guard active, before anything else |
| 403 | no camera grant; a person allows it once, on the first prompt or in Settings | CameraRig.requireGrant |
| 423 | another capture is running | CameraRig.claim, under a lock |
| 503 / 504 | no such camera, or it would not open; a deadline ran out, naming what it waited for | the rig's session setup; Deadline.run |
Understand the 409 first. iOS does not let a backgrounded app use the camera at all (an interrupted session reports videoDeviceNotAvailableInBackground), so a body is a foreground kiosk, and the honest answer to a request that arrives while it sits behind another app is "bring me to the front first", not a black frame. The 403 points at a person on purpose. The app asks for the camera once, at its first launch, and never routes around a denial; a missing grant also shows on the face in amber, naming what to allow in Settings. The 423 is what claim in the second code block throws: one boolean under an NSLock, set and cleared by the same request, so a still asked for during a clip waits for nobody and is told why.
Oriel refuses first, and keeps the words
Most of these refusals never reach the body. Oriel probes each body for its status every 10 s, and before it sends a capture it reads the last answer: a body silent for more than 30 s, not in front, without the camera grant, or without the named camera is refused right there in a sentence of the same kind. When the body does refuse, Oriel's link keeps the reason and folds the status. Any 4xx from the body reaches the caller as 409 and any 5xx as 503, with a message like "name refused a front photo: the camera is busy with another capture". The brain that asked reads the sentence, not the number, so the sentence is the contract.
Each side also keeps its own named clock. On the body a still waits at most 10 s, and a clip its length plus the 0.8 s settle plus 10 s. On Oriel a photo waits 20 s and a clip its length plus 20 s. The device gives up first and says what it was waiting for; Oriel's longer clock is the net for a body that never answers at all.
Where the band and the camera could disagree
Read the deadline path closely and you will find the one gap the band cannot cover. When Deadline.run expires, it cancels the capture task, Once resumes with a cancellation, the request answers 504, release frees the claim and the defer lowers the band. All of that is right for the request. The capture session itself, though, is stopped by its delegate's completion on the rig's queue, so if AVFoundation never called back, nothing on that path would stop it, or delete a clip's temporary file (lesson 4). That is why Oriel's architecture calls the hardware's own tally the second safety: iOS lights its own camera indicator whenever a camera streams, whatever any app draws. The band is my promise about the request. The system indicator is Apple's promise about the sensor. Keep both, and never let your own indicator be the only one.