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