Expand description
DVR durable segment archive — a media_plane::egress::SegmentEgress
implementation that persists finished segments to disk as contiguous
period files (one container file per period epoch), with a byte-range
index and configurable retention. The operator chooses the
media_plane::trunk::ArchiveOverrun policy for the
loss/stall/drop trade when the live ring wants to evict a pinned entry.
§On-disk layout
<archive_root>/<route_name>/ — one directory per route, containing:
| File | Content |
|---|---|
p0.<ext> | Period container file. For fMP4: init segment followed by concatenated media fragments — init at the head makes the file independently playable (concatenate and demux). For MPEG-TS: concatenated 188-byte packets, each segment carrying its own PAT/PMT in-band. |
p0.idx | Sidecar byte-range index: a sorted list of (seq, start_pts_ns, byte_offset, byte_len) entries, one per segment in the period file. Append-only, flushed synchronously as each segment lands. JSON (human-readable, diffable) with write-then-rename for atomicity. |
p1.<ext>, p1.idx | Next period. A new period is started when period_duration_secs elapses OR when the fMP4 init segment changes mid-recording (issue #781). |
Why one container file per period, not per-segment files:
- fMP4: a CMAF track file is init + concatenated fragments. Writing that as one file is naturally valid — the init at the head makes the file independently playable (concatenate and demux). This dissolves the init-segment blocker from fix1.md: the init is the head of the recording, not a separate object that can be forgotten.
- TS: MPEG-TS packets are a continuous 188-byte stream with each
segment carrying its own PAT/PMT in-band. Concatenation is natively
valid — N segments appended together is a directly playable
.ts. - Operationally: a 3-hour period covers a feature film in one file. Someone pulling a recording to watch or hand over gets one file, not hundreds of fragments they have to reassemble.
- Durability: a byte-range index per period is flushed after each
append. A period file whose index is lost is unusable data, so the
index is a first-class, rebuildable artifact — see
IndexEntryandDvrRecorder::rebuild_index, which rescans the period file to reconstruct it. This recovery covers fMP4 periods only; the TS rescan is not implemented, so a TS period that loses its index stays unusable. Recovery is also not automatic — nothing calls it on startup; a caller must invoke it.
§Period lifecycle
A new period file is started when:
- The current period has been open longer than
period_duration_secs(time-based rollover — configurable, default 3 hours). - The fMP4 init segment changes mid-recording (mid-stream track addition — issue #781 republishes init bytes). Segments recorded after the change need the new init; appending them behind the old one would corrupt the file. The old period file is closed and a new one is opened with the new init at its head.
- Recording starts (the very first period).
- The tracked service’s EIT present event changes (issue #903 — see
“Programme-aligned rolling”, below). Opt-in via
dvb_service_id.
Each period file and its index are self-contained: concatenating the period file from byte 0 and demuxing it recovers the track’s codec configuration and decodable samples for every segment in that period.
§Programme-aligned rolling (issue #903)
A fixed time slice (the default 3-hour period) cuts a recording mid-
programme essentially always — a conventional PVR instead rolls its
recording on the programme boundary, so one recording is one programme.
For a DVB source, that boundary is the Event Information Table
present/following transition (ETSI EN 300 468 §5.2.4): each service
carries an EIT p/f actual section (table_id 0x4E) naming the event
currently on air (running_status == 4, “running” — Table 6) and the
event due next. When the broadcaster’s head-end re-signals the section
with a different event now running, that is a real programme boundary
— not a guess, not a clock tick.
Setting DvrConfig::dvb_service_id to the service this route records
opts a DvrRecorder into tracking that service’s EIT p/f: feed it raw
TS packets via DvrRecorder::feed_si (the ingest side does this only
for the TS-carrying sources that have SI to feed — RTSP/SRT-as-RTP/
RTMP/HLS-pull ingests have none), and when the present event’s
event_id changes, the recorder rolls to a new period immediately,
ahead of period_duration_secs. The new period is tagged with an
EitProgramme — event_id, service_id, title (from the event’s
short_event_descriptor, EN 300 468 §6.2.37), announced start time and
duration — written as pN.event.json alongside the period file and its
index, so an operator can find a programme rather than a timestamp.
dvb_service_id is None by default — recording never silently starts
guessing at a service. Left None, or fed a stream with no SI at all
(every non-DVB source), a DvrRecorder behaves exactly as before this
issue: pure time-based periods.
The time-based period is kept as both the fallback and the hard cap
even when dvb_service_id is set: an EPG that never signals a
transition (a stale/frozen EIT carousel) must not produce an unbounded
recording. period_duration_secs rolls the file regardless of whether
an EIT transition has been observed — see
DvrRecorder::poll_and_persist’s time-based rollover check, which
runs unconditionally alongside the EIT-driven one. A hard-cap roll
re-tags the new period with the same EitProgramme (the programme
has not actually changed) so retention/naming stay honest about what
each file actually contains.
§Initi at the head (fMP4 only)
For fMP4 archives, the period file begins with the init segment.
The init is not available at construction time — the segmenter produces
it after the first track set is known — so the caller passes the current
init bytes to poll_and_persist on every poll. The recorder
opens the period file and writes the init as the first bytes the instant
it is available.
For MPEG-TS archives, no init is written (TS segments are self-
describing). The asymmetry is explicit in code — see the ".m4s" vs.
".ts" branches in poll_and_persist and start_period.
§Index format
The index sidecar (pN.idx) is a JSON array of objects, one per segment:
[
{"seq": 1, "start_pts_ns": 0, "byte_offset": 1234, "byte_len": 45678},
{"seq": 2, "start_pts_ns": 2000000000, "byte_offset": 46912, "byte_len": 49123}
]seq: segment sequence number (matches_HLS_msn).start_pts_ns: segment’stimeline_positionin nanoseconds (absolute, from theTrunk’s timeline — what #900 uses for time-based seek).byte_offset: byte offset of this segment within the period file (0-based, pointing to the first byte of the segment’s data — for fMP4 this is the start of themoofbox, not the init, because the init is the period file’s head, before byte_offset 0).byte_len: exact byte length of this segment within the period file.
Byte offsets are measured from the start of the period file (byte 0).
For fMP4, the init comes first, so the first segment’s byte_offset is
init_bytes.len(). For TS, the first segment’s byte_offset is 0.
§Retention
Operates on whole period files, quantised to the period. Two axes:
retention_periods— keep at most this many period files.retention_bytes— keep at most this many total bytes across all period files (file size, not segment payload).
When the limit is exceeded, the oldest period file AND its index are deleted together. Retention is checked after each segment append; it never stays above the limit between polls.
§ArchiveOverrun in operator terms
The per-route overrun field (default: ArchiveOverrun::Gap) surfaces
the three-way trade from media_plane::trunk::ArchiveOverrun:
"gap"(default): when the live ring evicts a segment the recorder hasn’t yet consumed, the recording gets a hole — a gap marker is recorded and the index notes the loss. The archive is incomplete but live ingest is unaffected."stall": the recorder applies real back-pressure — segment publication blocks until the recorder consumes far enough. The archive is lossless, but a slow disk or a hung recorder can stall live output for every viewer."terminate": drop the recorder’s pin when the live ring overruns — recording stops, and no further segments are written. Existing files on disk are kept (they were successfully recorded).
§Recording does not perturb live serving
The recorder drains a separate pinning SegmentCursor — it reads the
same SegmentEntry values every other cursor reads, with exactly the
same zero-copy fan-out (Bytes refcount bump). Live LL-HLS/DASH output
is unaffected: the recorder never holds a lock the live path needs,
and it never mutates Trunk state.
Structs§
- DvrConfig
- Per-route DVR configuration.
- DvrRecorder
- The DVR recorder: a
SegmentEgressimplementation that owns one pinningSegmentCursor, drains it viaSelf::poll_and_persist, and appends finished segments to a period container file with a byte-range index sidecar. - EitProgramme
- Programme identity for one period file, derived from the tracked
service’s EIT present event (ETSI EN 300 468 §5.2.4) at the moment the
period was opened (or, if the event became known only afterwards, at the
moment it did). Written as
pN.event.jsonalongside the period file and its index — see the module docs. - Index
Entry - One entry in the byte-range index sidecar (
pN.idx).
Enums§
- Archive
Overrun Serde - Serde-friendly
ArchiveOverrun— lowercase string tokens matching the operator-facing names in the module docs.