playout-runtime
Sans-IO linear channel playout (issue
#748): a schedule model
(programme / ad / slate), transition planning across the join between two
sources, and the SCTE-35 splice_insert() emission points a transition implies.
What it does / what it does not
Does. Models a linear channel as an ordered schedule of entries, finds the
next join the schedule implies, and computes it: the channel-timeline instant,
the PTS-rebase offset that lands the incoming source's clock continuously on the
shared timeline, and a discontinuity flag when the codec configuration changes.
It also builds (and can wrap into a splice_info_section) the SCTE-35
splice_insert() for entering or returning from a break, with its target
conditioned against real candidate boundaries. Used by a channel-playout
orchestrator that owns the clock and the media.
Does not.
- No transcoding or conforming: a differing codec config is a discontinuity to signal, never something re-encoded. Codec bitstreams are never touched.
- No sample-timestamp rewriting:
transmux's IR transform would applypts_rebase_offset; this crate computes the number, not the rewrite.broadcast_hls::mark_init_discontinuitieswould act ondiscontinuityat the playlist level; neither is a dependency here. - No boundary conditioning of its own: it delegates to
ssai_runtime::splice::condition_splice_point_wrappingso two implementations cannot disagree about the same boundary. - No schedule wire format and no SCTE 224 (ESNI): the schedule is an in-memory
model of our own (there is no
Parse/Serializepair; SCTE 224 covers policy and blackout signalling, not channel assembly). - No HTTP, no tokio, no real-time clock; a
multimuxadapter driving a channel clock is future work.
Standards supported
| Standard | Edition | Scope | Status | Notes |
|---|---|---|---|---|
| ANSI/SCTE 35 | 2023r1 (vendored, private/specs-md/ansi_scte_35_2023r1_dpi_cueing.md) |
splice_insert() (section 9.7.3, Table 10) built via scte35-splice: event id, out_of_network_indicator, program splice, splice_time(), break_duration() with auto_return; clear splice_info_section wrapper |
Partial | Only splice_insert; no time_signal, segmentation descriptors, encryption or components. Units must be 90 kHz ticks |
| (none: schedule model) | n/a | Programme / ad / slate schedule | Not a standard | SCTE 224 deliberately not adopted |
Features
| Feature | Default | Effect |
|---|---|---|
std |
yes | Links std (and the siblings'). Off gives #![no_std] + alloc. |
serde |
no | serde derives on the schedule, transition and decision types. |
Quick start
use ;
let mut schedule = new;
schedule
.push
.unwrap;
// A 30 s ad at 10 minutes (90 kHz ticks), cut from an asset whose own clock
// starts at 5_000, with a different codec configuration.
schedule
.push
.unwrap;
// The join: when, how to rebase the incoming clock, and whether to signal a
// discontinuity.
let t = next_transition.unwrap;
assert_eq!;
assert!;
assert_eq!;
// The splice_insert() that entering the break implies, snapped to the nearest
// real boundary (2_000 ticks before the nominal instant beats 3_000 after).
let = build_splice_insert
.unwrap;
assert_eq!;
assert!;
What is implemented
schedule:Schedule(new,push,entries,len,is_empty,active_at) ofScheduleEntry(id,EntryKindprogramme / ad / slate,planned_start,source_start_pts,CodecConfigId).transition:next_transition(&Schedule, now_pts)returns aPlannedTransition(from,to,plan);TransitionPlan::plan(from, to)computesat_pts,pts_rebase_offset(i128, never clamped) anddiscontinuity;TransitionPlan::rebasemaps a source PTS onto the channel timeline (Nonerather than wrapping out of range) andrebase_wrappedunrolls a raw 33-bit PTS first.scte35:build_splice_insert(BreakEdge, event_id, requested_pts, candidates, max_delta_ticks, break_duration_ticks)returns theConditionedSplicePointand theSpliceInsert;to_sectionwraps it in a clearSpliceInfoSection. Distances are circular modulo 2^33.error:Error(includingSpliceConditioning).
Timeline continuity: the primary tested property
Issue #748: "a schedule that plays the right thing at the wrong timestamp is
worse than no scheduler." The tests hold TransitionPlan to that bar:
transition::tests::rebase_lands_the_incoming_source_continuously_on_the_channel_timeline: once rebased, the incoming source's PTS sequence lands exactly on the channel timeline with no gap and no overlap.transition::tests::rebase_returns_none_rather_than_wrapping_out_of_range.tests/real_fixture_transition.rsplans a programme to ad transition against the real, non-IDR-aligned cue infixtures/scte35-ssai/(DASH-IFlivesim2, Apache-2.0): the nearest keyframe is 6000 ticks (67 ms) after the cue's nominal instant, measured independently from the fragment's own boxes (seefixtures/scte35-ssai/PROVENANCE.md), and the emitted cue carries the conditioned instant, not the raw request.
Examples
schedule_and_transition: a programme, ad, programme schedule and every transition it implies.emit_splice_from_real_cue: decode the real cue, plan the transition, condition the splice point, emit and byte-exact round-trip thesplice_insert().
Design notes
- Sans-IO,
no_std+alloc,#![forbid(unsafe_code)], structuredthiserrorerrors; spec enums carry the workspacename()+Displaylabel pair. - The hard part is the transition, not the schedule: units are 90 kHz ticks and
the caller must unwrap any source-clock wrap before
rebase(or userebase_wrapped). scte35-splice's ownSerializebuilds the cue bytes; nothing is hand-assembled.
Related crates
Depends on broadcast-common, ssai-runtime (splice-point conditioning) and
scte35-splice (the cue); dev-depends on mp4-emsg for the real-fixture tests.
transmux applies the rebase; multimux would drive it.
Spec references
- SCTE-35
splice_insert(): ANSI/SCTE 35 2023r1 section 9.7.3, transcribed atscte35-splice/docs/. - Splice-point conditioning:
ssai-runtime, issue #929.
MSRV and license
MSRV 1.95.0. Licensed under either of Apache License, Version 2.0 or MIT license at your option.