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.