Skip to main content

Module dvr

Module dvr 

Source
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:

FileContent
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.idxSidecar 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.idxNext 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 IndexEntry and DvrRecorder::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:

  1. The current period has been open longer than period_duration_secs (time-based rollover — configurable, default 3 hours).
  2. 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.
  3. Recording starts (the very first period).
  4. 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 EitProgrammeevent_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’s timeline_position in nanoseconds (absolute, from the Trunk’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 the moof box, 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 SegmentEgress implementation that owns one pinning SegmentCursor, drains it via Self::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.json alongside the period file and its index — see the module docs.
IndexEntry
One entry in the byte-range index sidecar (pN.idx).

Enums§

ArchiveOverrunSerde
Serde-friendly ArchiveOverrun — lowercase string tokens matching the operator-facing names in the module docs.