RheoclesREE-oh-kleez

Troubleshooting

The things that go wrong, what they look like, and what they mean. Most of them are macOS telling you something quietly.

Nothing prompts, and nothing records

You arm a camera, macOS does not ask, the row goes ochre, and the file is empty. Open System Settings → Privacy & Security → Camera: Rheocles is not in the list.

That is not “denied”. An app that has never been able to ask does not appear at all, and there are two ways to be unable to ask under the hardened runtime: a missing Info.plist usage description, or a missing entitlement. Both are required. A release build from this repo has both; a local build signed differently may not.

If you are building from source, codesign --verify will report the bundle valid even when --deep has skipped an executable in Contents/Resources. Only notarisation catches it. Sign inside out.

Displays are missing after granting Screen Recording

The grant takes effect on the daemon’s next launch, not the current one. Quit Rheocles and open it again; GET /streams then lists every display and permissions.screen reads authorized. The first stream list in a fresh process is what raised the prompt, and that process keeps the answer it had when it started.

Arming answers 403

permission_denied: macOS has not granted the device class this stream needs. For camera and microphone the prompt is raised first, so a 403 means it was refused — the grant is under System Settings → Privacy & Security. For system audio the grant is System Audio Recording, under Screen & System Audio Recording.

Arming answers 503

device_unavailable: the device is gone, busy, or refused the configuration. A camera another app holds exclusively, a display that was just unplugged, a window that closed between the list and the arm. Refresh the list and try again; if it persists, the device is the problem, not the request.

The armed camera is 720p

Arming takes the device’s current format and holds the configuration lock for the whole armed period. A camera another app left at 720p stays at 720p until that app or the user changes it. active on the stream says what you will get; set the format in the other app, or disarm, change it, and arm again.

framesSeen is not counting

An armed stream that delivers frames counts up. One that does not is stuck — a window that went off-screen, a device that stalled — and a take started now would record nothing from it. Disarm and arm again; if it stays at zero, the device is not delivering.

Preview answers 503 no_frame

The device delivered nothing to grab — a camera with no signal, a capture card with nothing plugged in. Preview opens the device, waits briefly for one frame, and closes it; with no frame there is nothing to send. The same device will stall if armed. Check the signal, not the request.

The port is in use

The daemon comes up on 7447 and 7448. If another daemon is already there — the NativePHP app’s, or one left over — the app uses it rather than starting another, which is the intended behaviour. If something else is on 7447, the app says so and will not start a daemon until the port is free. Sonocles is on 7357 and 7358 and does not collide.

409 conflict on create

The destination already exists and is not empty. Rheocles never silently suffixes a path — a client that named a path meant it. Pass overwrite: true if you meant to replace it, or name it something else. The same code answers start on a take that is not created and stop on one that is not recording.

409 take_active on create

A take is recording, and the error names it. One active take at a time in the MVP; stop it first. A take that was created but never started does not block — the next create supersedes it and its manifest reads incomplete, reason superseded before start.

The manifest says superseded before start

You created a take, never cued it, and created another. Nothing but the manifest ever existed for the first one, so it is marked and left. It is not an error; it is the record of a take that did not happen.

Create refused for disk space

The pre-flight estimated the take’s size from the armed set’s measured bitrates and expectedDuration (default thirty minutes) and found the free space short. The error carries both numbers. Free space, shorten the estimate if you know the take is short, or disarm the stream that is costing the most.

The manifest says incomplete

A write failed mid-take — the disk filled, a volume unmounted, a device disappeared — and Rheocles stopped cleanly rather than continuing with a lie. reason says what, and each affected stream carries its own error. Every file is playable up to the last fragment, and the manifest is truthful about what it has.

A stream says no frames arrived

It was armed and delivered nothing for the whole take — a camera with its lid closed, a display that went to sleep, a window that was minimised. It wrote no file, and the take finished incomplete naming it. The warning was there before the cue: that stream’s framesSeen in GET /streams sat at 0 while armed. Watch it, or let the popover watch it for you.

framesDropped is not zero

The encoder was not ready for those frames under load. The timeline is still correct — the frames are missing, not shifted — so sync holds; what you have is a stutter. Fewer armed streams, ProRes instead of HEVC, or a less busy machine.

A display’s file has no drift

By design. A screen delivers a frame only when its content changes, so its cadence is the content’s and drift means nothing. drift is reported for cameras and microphones, whose devices have clocks of their own.

The manifest says incomplete, reason daemon stopped or daemon died

daemon stopped: the daemon was asked to quit mid-take and finalised it first — every writer closed, the manifest written. daemon died: it went without finalising — a crash, a kill -9, a power cut — and the next launch found the manifest still saying recording and marked it. Either way the files are fragmented MOVs that play to their last fragment and WAVs that read, and the manifest has every stream’s started and timecode. Nothing needs reconstructing, and nothing pretends the take finished.

Requests answer 401

The token does not match. It was rotated — from the popover or POST /token/rotate, which refuses the old token from the very next request — or the client is reading a stale copy. Re-read ~/Library/Application Support/Rheocles/token. A WebSocket that was authenticated before the rotation is still open but answers 401 to its next command until it sends { "auth" } with the new token.

PATCH /settings answers 409

You moved outputRoot while a take was created or recording. Its files are already reserved beneath the old root, so the root stays until the take is stopped. Change the codec any time; it applies to the next take.

A stalled event arrived

An armed or recording stream stopped delivering frames — the lid closed, the window minimised, the capture card lost its signal. The event carries the stream as it now is. If it is in a recording take and never resumes, the take finishes incomplete with no frames arrived for that stream; if it is merely armed, the cue will find nothing to write.

My late-joined file lands before the take

The stream joined after midnight into a take that started before it. Its time-of-day timecode is near zero, and the editor placed it twenty-four hours early. Every camera and field recorder shares this; there is no fix inside the file. Place it from the manifest’s started, which is absolute UTC — see Timecode and sync.

Two files are a frame apart

Look at drift for each camera and microphone in the manifest, in seconds. A capture device’s clock is not the host’s, and a long take can drift by a frame or two; the number is measured per stream and is there to conform with. An absent drift means it was not measured — a display or window, or a stream that wrote no frames — and is not the same as 0.

Six cameras are still armed

The mark in the menu bar is drawn in outline and the popover rows are ochre. That is what armed looks like, and armed streams cost CPU and hold their devices until you disarm them. Nothing disarms on your behalf.