Skip to main content

playout_runtime/
lib.rs

1//! Sans-IO linear channel playout — issue
2//! [#748](https://github.com/fishloa/rust-broadcast/issues/748).
3//!
4//! A schedule model for assembling a linear channel from an ordered list of
5//! sources ([`schedule`]), the transition planning that turns "what's next"
6//! into a concrete timeline-continuity plan ([`transition`]), and the
7//! SCTE-35 `splice_insert()` emission points a transition implies
8//! ([`scte35`]).
9//!
10//! # What it does / what it does not
11//!
12//! **Does:** finds the next join a schedule implies and computes it (channel
13//! instant, PTS-rebase offset, discontinuity flag), and builds the
14//! `splice_insert()` for entering or returning from a break, conditioned against
15//! real candidate boundaries.
16//!
17//! **Does not:**
18//!
19//! - **No transcoding or conforming.** A differing codec config is a
20//!   discontinuity to *signal* ([`transition::TransitionPlan::discontinuity`]).
21//! - **No sample-timestamp rewriting.** `transmux`'s IR transform would apply
22//!   [`transition::TransitionPlan::pts_rebase_offset`]; this crate computes the
23//!   number. `broadcast_hls::mark_init_discontinuities` would act on the
24//!   discontinuity flag; neither is a dependency here.
25//! - **No boundary conditioning of its own:** [`scte35::build_splice_insert`]
26//!   calls `ssai_runtime::splice::condition_splice_point_wrapping`.
27//! - **No schedule wire format, no SCTE 224** (it covers policy and blackout
28//!   signalling, not channel assembly): [`schedule::Schedule`] is an in-memory
29//!   model with no `Parse`/`Serialize` pair.
30//! - **No HTTP, no tokio**, no real-time clock; a `multimux` adapter is future
31//!   work.
32//!
33//! # Standards supported
34//!
35//! | Standard | Edition | Scope | Status |
36//! |---|---|---|---|
37//! | ANSI/SCTE 35 | 2023r1 | `splice_insert()` (section 9.7.3, Table 10) via `scte35-splice`; clear `splice_info_section` wrapper | Partial: `splice_insert` only (no `time_signal`, segmentation descriptors, encryption, components); 90 kHz ticks |
38//! | schedule model | n/a | programme / ad / slate | Not a standard (SCTE 224 not adopted) |
39//!
40//! # Features
41//!
42//! | Feature | Default | Effect |
43//! |---|---|---|
44//! | `std` | yes | Links `std`; off gives `#![no_std]` + `alloc` |
45//! | `serde` | no | `serde` derives on the public types |
46//!
47//! # Quick start
48//!
49//! ```rust
50//! use playout_runtime::{
51//!     BreakEdge, CodecConfigId, EntryKind, Schedule, ScheduleEntry, build_splice_insert,
52//!     next_transition,
53//! };
54//!
55//! let mut schedule = Schedule::new();
56//! schedule
57//!     .push(ScheduleEntry {
58//!         id: "news".into(),
59//!         kind: EntryKind::Programme,
60//!         planned_start: 0,
61//!         source_start_pts: 0,
62//!         codec_config: CodecConfigId(1),
63//!     })
64//!     .unwrap();
65//! // A 30 s ad at 10 minutes (90 kHz ticks), cut from an asset whose own clock
66//! // starts at 5_000, with a different codec configuration.
67//! schedule
68//!     .push(ScheduleEntry {
69//!         id: "ad".into(),
70//!         kind: EntryKind::Ad,
71//!         planned_start: 54_000_000,
72//!         source_start_pts: 5_000,
73//!         codec_config: CodecConfigId(2),
74//!     })
75//!     .unwrap();
76//!
77//! // The join: when, how to rebase the incoming clock, and whether to signal a
78//! // discontinuity.
79//! let t = next_transition(&schedule, 0).unwrap();
80//! assert_eq!(t.plan.at_pts, 54_000_000);
81//! assert!(t.plan.discontinuity);
82//! assert_eq!(t.plan.rebase(5_000), Some(54_000_000));
83//!
84//! // The splice_insert() that entering the break implies, snapped to the nearest
85//! // real boundary (2_000 ticks before the nominal instant beats 3_000 after).
86//! let (point, insert) = build_splice_insert(
87//!     BreakEdge::Enter,
88//!     1,
89//!     t.plan.at_pts,
90//!     &[53_998_000, 54_003_000],
91//!     5_000,
92//!     Some(2_700_000),
93//! )
94//! .unwrap();
95//! assert_eq!(point.snapped_pts, 53_998_000);
96//! assert!(insert.out_of_network_indicator);
97//! ```
98//!
99//! # What is implemented
100//!
101//! - [`schedule`]: [`Schedule`] of [`ScheduleEntry`] ([`EntryKind`],
102//!   [`CodecConfigId`]).
103//! - [`transition`]: [`next_transition`] returns a [`PlannedTransition`];
104//!   [`TransitionPlan::plan`] computes `at_pts`, `pts_rebase_offset` (`i128`,
105//!   never clamped) and `discontinuity`; [`TransitionPlan::rebase`] returns
106//!   `None` rather than wrapping, and [`TransitionPlan::rebase_wrapped`] unrolls a
107//!   raw 33-bit PTS first.
108//! - [`scte35`]: [`build_splice_insert`] (circular distances modulo 2^33) and
109//!   [`to_section`], with [`BreakEdge`] naming the break edge.
110//!
111//! # Design notes
112//!
113//! Sans-IO, `no_std` + `alloc`, `#![forbid(unsafe_code)]`, structured [`Error`]s.
114//! "A schedule that plays the right thing at the wrong timestamp is worse than no
115//! scheduler" — timeline correctness is the primary tested property (including a
116//! real, non-IDR-aligned cue from `fixtures/scte35-ssai/`). Units are 90 kHz ticks.
117//!
118//! # Related crates
119//!
120//! Depends on `broadcast-common`, `ssai-runtime` and `scte35-splice`. MSRV 1.95.0;
121//! MIT OR Apache-2.0.
122//!
123#![no_std]
124#![forbid(unsafe_code)]
125#![cfg_attr(docsrs, feature(doc_cfg))]
126
127extern crate alloc;
128
129pub mod error;
130pub mod schedule;
131pub mod scte35;
132pub mod transition;
133
134pub use error::{Error, Result};
135pub use schedule::{CodecConfigId, EntryKind, Schedule, ScheduleEntry};
136pub use scte35::{BreakEdge, ConditionedSplicePoint, build_splice_insert, to_section};
137pub use transition::{PlannedTransition, TransitionPlan, next_transition};