The Two-Step Read
Every frame Oriel reads from a recording, for any purpose, takes the same two steps. First, one index lookup: Store.segment_at(input, t) asks SQLite for the closed segment whose start is at or before t and whose end is after it. No file is opened for that. A moment inside the segment the recorder still has open, which has no sidecar yet, is the one exception: the engine takes that file's path and start from the recorder's status instead. Second, one short ffmpeg process on that one segment, seeking to the offset t - start_at inside it. The house sign states the rule as an invariant: every segment has a sidecar, and every frame read starts from the index; no read decodes a whole file.
The measured costs on the home Mac show why it matters: an exact 4K frame in 82 ms; on the 1080p, 40 Mb/s recordings since, an exact frame in 185 ms. A design that had to open and scan an hour-long file would be orders of magnitude slower, and the cost would grow with the length of the recording. Here it does not grow at all.
Three Ways to Read a Frame
backend/frames.py has three readers, and choosing between them is most of the craft:
| Reader | What it returns | How | Measured |
|---|---|---|---|
extract | the exact frame at a moment, full resolution | -ss before -i, decode to that frame, raw RGB out | 185 ms for 1080p (470 ms as PNG, same pixels) |
view | a keyframe near a moment, fitted small | keyframes only, no accurate seek, ffmpeg scales and encodes JPEG | 97 ms of ffmpeg CPU per 1080p frame |
| keyframe pass | every keyframe in a window | non-key packets dropped at the demuxer | 0.38 s for a 20 s window |
Two details carry most of the speed. In extract, the frame comes out as raw RGB bytes of the size the sidecar already knows, so ffmpeg does not pack a PNG and Python does not unpack one; a frame that comes out a different size is read again the slow way. In the keyframe pass, the flags -discard nokey plus -skip_frame nokey drop non-key packets at the demuxer. With -skip_frame nokey alone, ffmpeg still hands every packet to the decoder to parse and throw away: 2.3 s for a 20 s window and 6.2 s for 60 s, against 0.38 s and 0.69 s with the demuxer dropping them, the same frames byte for byte.
The Read That Answered Nothing
Fast readers have sharp edges, and one of them cost Oriel four days. A sidecar's duration ends one frame after the last frame starts. A keyframe-only read answers with a keyframe near its moment, usually the next one, and sometimes the one just before ( on a test clip with a keyframe every 0.25 s, asking ffmpeg 9.0.2 for 1.01 s or 1.02 s still returned the 1.00 s keyframe, so it is a sample, never an exact frame). Ask such a read for a moment near the very end of a segment, far enough past the last keyframe, and ffmpeg finds nothing and writes nothing. (Not just any moment past it: on the test clip, asking 0.01 to 0.03 s after the last keyframe still returned that keyframe, and 0.05 s after it returned nothing.) On the ffmpeg 8 the house ran then, it also exited with status 0; ffmpeg 9.0.2 answers the same request with an error status (234) and still no bytes, so a reader has to treat both as "no frame". The background pass that checks recordings for people (track six) sampled exactly such a moment, and that one unreadable read stopped the pass every time it came round: from 2026-11-02 to 11-06 nothing behind that segment was checked. The fix was twofold: sample a frame and a GOP short of a segment's end, and hold back an unreadable segment for an hour so the pass moves on to the ones behind it.
The general lesson is the one every pipeline eventually learns: a process that exits 0 has not necessarily done anything. Check the output, not only the status.
"Now" Is a Few Seconds Ago
A read of the live moment has its own wrinkle. A fragment reaches disk about a second after its frames are captured, so the engine treats "now" in a recording as LIVE_LAG_SECONDS (2.5 s) ago, which guarantees the newest frame it asks for is readable. Even then, that moment can sit in a segment that rotated a moment ago and whose sidecar is not yet written, so the index has no row for it yet. Instead of refusing at once ("nothing recorded"), a live read waits up to four seconds, retrying every half second, for the moment to appear in the index. A refusal after that wait is a real refusal; one before it would have been a race.