RheoclesREE-oh-kleez

Recording a take

A take is created, cued, and stopped. Creating reserves the paths and writes the manifest before a single frame; starting is the cue.

A take is three calls. The first does everything that can be done before recording — snapshots the armed set, resolves the destination, pre-flights the disk, reserves every file name, writes the manifest — so that by the time the cue comes there is nothing left to decide.

Create, start, stop

call does state after
POST /takes snapshot the armed set, reserve paths, pre-flight the disk, write the manifest created
POST /takes/{id}/start the cue: every stream’s writer starts on frames that are already flowing recording
POST /takes/{id}/stop finalise every writer and the manifest complete
POST /takes → 201
→ { "name": "Episode 12", "expectedDuration": 3600 }
← { "take": {
"id": "20260912T040433-fd9q", "name": "Episode 12", "state": "created",
"created": "2026-09-12T04:04:33.235Z",
"outputRoot": "/Users/len/Movies/Rheocles",
"destination": "takes/2026-09-11/210433-episode-12",
"streams": [
{ "id": "display:F65F9C53-…", "kind": "display", "name": "Prompter XL",
"path": "prompter-xl.mov", "codec": "hevc", "framesWritten": 0, "events": [] },
{ "id": "microphone:…", "kind": "microphone", "name": "Scarlett 2i2 USB",
"path": "scarlett-2i2-usb.wav", "codec": "pcm_s24le", "framesWritten": 0, "events": [] }
],
"markers": [], "settings": { "codec": "hevc", "expectedDuration": 3600 } },
"warnings": [] }

The answer is the manifest, with state: created. Nothing is recording after create. This is the call Pteroprompter makes to learn where the footage will live before frame one, so the cue log can name files that do not exist yet. A created take can sit as long as you like; start is a separate, deliberate call.

POST /record is create followed by start in one call, with the same body and the same answer. It is what the popover’s Record button does. Use it when you do not need the paths ahead of time.

Where the files go

Every path is relative: the take folder to the output root, each file to the take folder. Join them if you need an absolute path; store them as given.

default override
take folder takes/<yyyy-MM-dd>/<HHmmss>[-<name slug>], local date and time destination, relative to the output root
each file the stream’s name, slugged — prompter-xl.mov, scarlett-2i2-usb.wav; windows are window-<bundle id>.mov; -2, -3 on a collision files, a map of stream id → file name

The same destination twice is a 409 conflict, unless overwrite: true. A destination is taken if the folder exists and is not empty. Rheocles never silently suffixes a path; a client that named a path meant it, and a file that quietly became -2 is how a take goes missing.

Disk pre-flight

Create refuses if there is not room. The armed set’s bitrates × expectedDuration (seconds; default 1800) gives an estimate — rough until the codec tiers are measured — and:

  • free space short of the estimate → 507 insufficient_storage, nothing created;
  • free space under twice the estimate → created, with a line in warnings.

If a write fails mid-take — disk full, volume unmounted — the take stops cleanly, every file is finalised as far as it got, and the manifest is marked incomplete with the reason. Nothing is deleted.

One at a time

While a take is recording, POST /takes and POST /record answer 409 take_active, naming it. Stop it first.

A take that was created but never started is superseded by the next create: its manifest is rewritten incomplete with reason superseded before start, since nothing but the manifest exists on disk. Concurrent takes with disjoint stream sets — a background screen recording spanning several camera takes — are conceivable and deliberately deferred.

POST /takes with no armed streams is a 400.

What lands on disk

Rheocles/ ← output root, one field on GET /
└── takes/2026-09-11/210433-episode-12/ ← destination
├── manifest.json
├── prompter-xl.mov HEVC, native resolution and rate, timecode track
├── elgato-4k-x.mov
├── scarlett-2i2-usb.wav Broadcast Wave, 48 kHz 24-bit, bext time reference
└── system-audio.wav
kind container codec carries its time as
camera, display, window QuickTime .mov, a fragment every second hevc or prores (ProRes 422), one setting for the whole take — codec on create, else the daemon’s settings.codec a tmcd track, local time of day, one sample per frame
microphone, system audio Broadcast Wave .wav, sizes patched every second pcm_s24le, 48 kHz bext TimeReference, samples since local midnight

Video files carry no audio track; audio is always its own file. Streams record at the format the device is delivering when armed — active on the stream — and per-stream codec, resolution and rate are deliberately not exposed.

The HEVC tier

HEVC is the default because at the tier Rheocles uses it is visually indistinguishable from ProRes 422 on the hardest content it records. That is a measurement, not a claim; the method and every number are in docs/CAPTURE.md.

The tier is 0.15 bits per pixel per frame, so it is independent of frame rate and resolution. What that comes to, and what the encoder actually produced at that tier on the measured source:

nominal at 0.15 bpp measured at 0.15 bpp
1080p, 30 fps 9.3 Mbps 8.3 Mbps · SSIM 0.9956 · XPSNR 47.0 dB
1080p, 60 fps 18.7 Mbps
4K, 30 fps 37.3 Mbps 30.0 Mbps · SSIM 0.9977 · XPSNR 52.0 dB
4K, 60 fps 74.6 Mbps

The method. Two real captures, recorded by Rheocles itself as ProRes 422 from a BenQ PD3220U at its native 3840×2160: a terminal scrolling colour-coded hex over a photographic wallpaper (continuous motion plus fine high-contrast text — harder than camera footage), and a static desktop with a browser full of JSON. Each was re-encoded with hevc_videotoolbox — the same VideoToolbox encoder AVAssetWriter uses — across a sweep from 0.04 to 0.30 bpp, and compared to the ProRes reference by XPSNR (perceptually weighted PSNR; ~45 dB is transparent) and SSIM (> 0.995 indistinguishable, > 0.997 excellent). The 1080p rows are the same captures downscaled, not independently shot footage.

What it found. 4K reaches transparency early — SSIM 0.997 / XPSNR 50 by 0.12 bpp on the motion source — and the curves flatten above 0.15, where another 7 Mbps buys +0.0004 SSIM. 1080p wants slightly more per pixel and knees at about 0.15. A 100 % crop of the motion source at 0.10 bpp is indistinguishable from ProRes by eye. So 0.15 bpp is kept as the single default with headroom; 4K at 0.12 bpp (about 20 % smaller, still transparent) is a refinement the data would justify if file size ever matters.

The disk pre-flight estimates from the same figures: HEVC at 0.15 bpp, ProRes 422 at about 2.4 bpp (1080p30 ≈ 147 Mbps).

What the counters mean

framesWritten is frames for video and samples for audio, actually written. framesDropped (video, absent when zero) counts frames the encoder was not ready for under load — the timeline stays correct, the frame is simply not there. drift is on Timecode and sync.

A screen delivers a frame only when its content changes, so a prompter holding a page would otherwise produce a file with a hole in it. A ~1 fps keepalive re-feeds the last frame, so a static screen still spans the take and still flushes fragments.

A stream that delivered no frames at all while armed — a camera with the lid closed — writes no file, and the take finishes incomplete with that stream’s error reading no frames arrived. Its framesSeen in GET /streams sat at 0 the whole time it was armed, which is the signal to watch before the cue.

Crash safety

Every MOV is written with a one-second movie fragment interval, so a process that dies mid-take leaves a file that plays up to the last fragment; every WAV has its RIFF sizes patched every second, so it reads. The manifest is rewritten atomically — temp file, then rename — on every state change, so it is never half-written; and because a full disk is the one failure that could stop the final manifest landing, every take folder holds a 64 KB reserve that is freed for exactly that write. This is proven, not hoped: kill -9 mid-take recovers every MOV, leaves every WAV readable, and the manifest says recording because that is the truth. A dead process leaves playable files and a truthful manifest; that is a design constraint, not a recovery feature.

Broadcast Wave stops at 4 GB. At 48 kHz, 24-bit, that is 144 kB a second per channel: about 8.3 hours mono, about 4.1 hours stereo. It is the format’s limit and Rheocles does not work around it.

No pause

There is no pause. Stop, and start a new take. A paused file would need either a gap in its timecode or a lie in it, and neither is worth a button.