RheoclesREE-oh-kleez

Streams and arming

Every input on the machine is a stream with a stable id. Armed means the device is live. Three verbs, and only these.

GET /streams lists every input Rheocles can record, with a stable id, a kind, a human name, a model, a capability summary, and whether it is armed. Nothing in the list is on until you arm it.

Kinds

kind what how it is found
display every screen, and screen-like devices such as the Elgato Prompter ScreenCaptureKit
window an individual window ScreenCaptureKit, a different filter
camera every video device, including Continuity Camera and external UVC AVFoundation
microphone every audio input device AVFoundation
systemAudio what the machine is playing, as one stream a Core Audio tap
GET /streams
{ "streams": [
{ "id": "display:56A96CFC-7F21-168E-0857-D6964E3302DB", "kind": "display",
"name": "BenQ PD3220U", "model": "vendor 2513 model 32813",
"capabilities": { "video": { "width": 3840, "height": 2160, "maxFrameRate": 60 } },
"armed": false },
{ "id": "microphone:Scarlett_2i2", "kind": "microphone",
"name": "Scarlett 2i2 USB", "model": "Scarlett 2i2 USB:1235:8210",
"capabilities": { "audio": { "sampleRate": 48000, "channels": 2 } },
"armed": true,
"active": { "audio": { "sampleRate": 48000, "channels": 2 } },
"framesSeen": 4812 } ],
"permissions": { "camera": "notDetermined", "microphone": "authorized", "screen": "authorized" } }

Ids are <kind>:<identifier>, URL-safe, and stable for as long as the thing they name is: cameras and microphones use the device’s unique id, displays their CoreGraphics UUID (which survives reconnects where the display number does not), windows their window number, which survives nothing and is not meant to. The order is fixed — displays, windows, cameras, microphones, system audio — so the list is stable between calls as devices come and go.

Three identity fields — id, name, model — are all recorded in the manifest, because display ids change across reconnects and a take must stay legible after the hardware does.

capabilities.video is native pixels and the highest advertised rate; the signal’s real rate can be lower, and the writer follows the frames, not this field. Recording is always 48 kHz 24-bit BWF whatever capabilities.audio says the device is doing.

active and framesSeen appear only while armed. active is the format the device is actually delivering — which is what a take records — and framesSeen is how many frames or audio buffers it has delivered since arming. A live device counts up; a stuck one does not, and that is how a client tells the difference before the cue.

permissions is what macOS has let this process do — authorized, denied, restricted or notDetermined. Screen Recording gates displays and windows both: with screen anything but authorized the list simply has none, and this field is how a client tells “no displays” from “not allowed to see them”.

Windows are supported by the engine and the API from day one and hidden in the popover behind a Show windows setting: window lists are long and volatile, and the UI for choosing one well is a later design pass. Per-process audio is later too; systemAudio is the whole output for now.

Armed means live

An armed stream has its capture session running — frames flowing, discarded — so that when the cue comes the writer starts on frames that already exist, rather than waiting hundreds of milliseconds for a device to spin up. That is the entire reason there is an arm button rather than only a record button: arming is what makes “on one cue” true.

Terminal window
curl -s -H "$H" -X POST localhost:7447/streams/camera:0x2300000fd9009c/arm -d '{"armed": true}'
→ the stream as it now is
{ "id": "camera:0x2300000fd9009c", "kind": "camera", "name": "Elgato 4K X", "model": "…",
"capabilities": { "video": { "width": 3840, "height": 2160, "maxFrameRate": 30 } },
"armed": true,
"active": { "video": { "width": 1280, "height": 720, "maxFrameRate": 120 } },
"framesSeen": 0 }

Both directions are idempotent and answer the stream as it now is. Every change of armed state is also pushed as a stream event to anything listening on /events or the WebSocket.

What arming holds, per kind

kind while armed and
camera an AVCaptureSession at the device’s current format, and the configuration lock for the whole armed period another app can still open the camera; it gets our format and cannot change it. A camera another app left at 720p stays at 720p until that app or the user changes it — active says what you will get
microphone an AVCaptureSession delivering 48 kHz 24-bit LPCM
display, window an SCStream at the display’s refresh rate, complete frames only a window that closes ends its session; the stream then reads as not armed
systemAudio a Core Audio process tap on every process, clocked by the default output device needs the System Audio Recording grant, which macOS asks for on the first arm

When arming fails

404 not_found no such id — a window that closed, a device that unplugged
403 permission_denied macOS has not granted the device class. For camera and microphone the prompt is raised first, so a 403 means it was refused
503 device_unavailable the device is gone, busy, or refused the configuration
501 unsupported this kind cannot be captured yet

Three verbs, and only these

verb when does stamps?
arm / disarm any time device live or not never
join while a take is active starts that stream’s writer now, adds its file to the manifest with the time it actually began
leave while a take is active finalises that stream’s file and marks it complete; the stream stays armed; the take continues for the others the file’s end

start on a take is join for every armed stream at once. That is the cue.

join on a stream that is not yet armed arms it first, so one call gets a cold stream into a running take — it just does not get there on frames that already existed. Disarming a joined stream implies leave. Nothing else is implicit.

late join
T=localhost:7447/takes/tk_7f3a
curl -s -H "$H" -X POST $T/join -d '{"stream": "window:11597"}'
curl -s -H "$H" -X POST $T/leave -d '{"stream": "window:11597"}'

Late join

A file that started four minutes into the take is stamped four minutes into the take, and an editor places it at four minutes. Nothing about one stream’s timing depends on another’s — each file carries its own time-of-day timecode from its own first written frame, and the manifest records each stream’s started separately. See Timecode and sync.

The honest case: a 4K display capture that is only needed for a two-minute demo in the middle of an hour-long take. Leave it unarmed so the cue does not take it, join it a moment before the demo — which arms it, and costs the spin-up — and leave it after, and the other fifty-eight minutes do not cost an encoder. The dishonest case is anything where the join and leave are the edit.

What the popover shows

state row menu bar mark
idle dim whole mark at 40 %
armed ochre, level meter live for audio strokes in outline
recording oxide, elapsed time strokes filled
joined late oxide a shorter stroke, starting right of the bar