Skip to main content

origin_playlist/
origin_playlist.rs

1//! Drive the sans-IO LL-HLS **origin** engine (`hls_runtime::server`)
2//! with zero IO: publish synthetic init/segment/part bytes into a
3//! [`Trunk`], then render playlists and resolve resources through
4//! [`HlsOrigin`]'s [`ServedEgress`] impl exactly as an HTTP adapter would —
5//! no socket, no clock, no async runtime.
6//!
7//! A real pipeline (a segmenter feeding a `TrunkWriter`) would publish real
8//! encoded media; here the bytes are synthetic placeholders, since this
9//! example is about the origin engine's *decision logic* (blocking-reload/
10//! part availability, playlist rendering), not encoding.
11//!
12//! # Usage
13//!
14//! ```bash
15//! cargo run --example origin_playlist -p hls-runtime
16//! ```
17
18use std::num::NonZeroUsize;
19use std::time::Duration;
20
21use broadcast_common::Timestamp;
22use hls_runtime::server::{
23    BlockingQuery, DEFAULT_TRACK_ID, HlsBody, HlsOrigin, HlsRequest, master_playlist_m3u8,
24};
25use media_plane::egress::{AwaitPolicy, EgressResponse, ServedEgress};
26use media_plane::trunk::{PartEntry, SegmentEntry, Trunk, TrunkConfig};
27use transmux::SegmentMeta;
28
29/// Target full-segment duration, in seconds.
30const TARGET_DURATION_SECS: f64 = 1.0;
31/// LL-HLS part target, in milliseconds.
32const PART_TARGET_MS: u32 = 500;
33/// Rolling window depth: full segments this origin advertises in a
34/// rendered playlist.
35const WINDOW_SEGMENTS: usize = 4;
36
37fn nz(n: usize) -> NonZeroUsize {
38    NonZeroUsize::new(n).expect("example capacity must be non-zero")
39}
40
41/// Every call in this example is a plain, non-blocking `resolve` — no
42/// request here ever needs to wait, so `now`/`await_policy` are nominal
43/// zero values throughout.
44fn resolve(origin: &HlsOrigin, request: HlsRequest) -> EgressResponse<HlsBody> {
45    origin.resolve(
46        request,
47        Timestamp::from_nanos(0),
48        AwaitPolicy::new(Timestamp::from_nanos(0)),
49    )
50}
51
52fn main() {
53    let trunk = Trunk::new(TrunkConfig::new(nz(16), nz(4), nz(8), nz(4), nz(16)));
54    let writer = trunk
55        .segment_writer()
56        .expect("first (and only) segment writer");
57    let origin = HlsOrigin::builder(std::sync::Arc::clone(&trunk))
58        .target_duration_secs(TARGET_DURATION_SECS)
59        .window_segments(nz(WINDOW_SEGMENTS))
60        .low_latency(PART_TARGET_MS)
61        .build()
62        .expect("both required fields set");
63    origin.set_init(vec![0xAA; 32]);
64
65    // Segment 1 closes with two parts.
66    writer.publish_part(PartEntry::new(
67        vec![0x01; 16],
68        1,
69        0,
70        Duration::from_millis(500),
71        true,
72    ));
73    writer.publish_part(PartEntry::new(
74        vec![0x02; 16],
75        1,
76        1,
77        Duration::from_millis(500),
78        false,
79    ));
80    writer.publish_segment(SegmentEntry::new(
81        vec![0x03; 32],
82        1,
83        Duration::from_secs(1),
84        Timestamp::from_nanos(0),
85        SegmentMeta {
86            discontinuous: false,
87        },
88    ));
89
90    // Segment 2 is still open, with only its first part landed so far.
91    writer.publish_part(PartEntry::new(
92        vec![0x04; 16],
93        2,
94        0,
95        Duration::from_millis(500),
96        true,
97    ));
98
99    println!("--- master.m3u8 ---");
100    println!("{}", master_playlist_m3u8("media.m3u8"));
101
102    println!("--- media.m3u8 ---");
103    match resolve(
104        &origin,
105        HlsRequest::Playlist {
106            track_id: DEFAULT_TRACK_ID,
107            query: BlockingQuery::default(),
108        },
109    ) {
110        EgressResponse::Ready {
111            body: HlsBody::Playlist(m),
112            ..
113        } => println!("{m}"),
114        other => panic!("expected Ready(Playlist), got {other:?}"),
115    }
116
117    // A plain (non-blocking) request is Ready immediately.
118    let outcome = resolve(
119        &origin,
120        HlsRequest::Playlist {
121            track_id: DEFAULT_TRACK_ID,
122            query: BlockingQuery::default(),
123        },
124    );
125    assert!(matches!(
126        outcome,
127        EgressResponse::Ready {
128            body: HlsBody::Playlist(_),
129            ..
130        }
131    ));
132    println!("resolve(Playlist, no query)     -> Ready");
133
134    // A blocking-reload request for a segment that hasn't closed yet: with
135    // `await_policy`'s deadline already at `now`, this immediately reports
136    // the awaited condition has run out of patience rather than serving a
137    // fabricated Ready.
138    let outcome = resolve(
139        &origin,
140        HlsRequest::Playlist {
141            track_id: DEFAULT_TRACK_ID,
142            query: BlockingQuery {
143                hls_msn: Some(5),
144                hls_part: None,
145            },
146        },
147    );
148    assert_eq!(outcome, EgressResponse::NotFound);
149    println!("resolve(Playlist, _HLS_msn=5)   -> NotFound (Await's patience already expired)");
150
151    // A `_HLS_msn` unreasonably far beyond the live edge is rejected outright
152    // (RFC 8216bis §6.2.5.2 abuse prevention) rather than ever Await-ing.
153    let outcome = resolve(
154        &origin,
155        HlsRequest::Playlist {
156            track_id: DEFAULT_TRACK_ID,
157            query: BlockingQuery {
158                hls_msn: Some(999),
159                hls_part: None,
160            },
161        },
162    );
163    assert!(matches!(outcome, EgressResponse::BadRequest { .. }));
164    println!("resolve(Playlist, _HLS_msn=999) -> BadRequest (abuse bound)");
165
166    // `Resource`: the init segment and the closed segment are Ready...
167    match resolve(
168        &origin,
169        HlsRequest::Resource {
170            name: "init-1.mp4".to_string(),
171        },
172    ) {
173        EgressResponse::Ready { .. } => println!("resolve(Resource, init-1.mp4)     -> Ready"),
174        other => panic!("expected Ready, got {other:?}"),
175    }
176    match resolve(
177        &origin,
178        HlsRequest::Resource {
179            name: "seg-1-1.m4s".to_string(),
180        },
181    ) {
182        EgressResponse::Ready { .. } => println!("resolve(Resource, seg-1-1.m4s)    -> Ready"),
183        other => panic!("expected Ready, got {other:?}"),
184    }
185    // ...a live part of the still-open segment is Ready too...
186    match resolve(
187        &origin,
188        HlsRequest::Resource {
189            name: "part-1-2.0.m4s".to_string(),
190        },
191    ) {
192        EgressResponse::Ready { .. } => println!("resolve(Resource, part-1-2.0.m4s) -> Ready"),
193        other => panic!("expected Ready, got {other:?}"),
194    }
195    // ...a preload-hinted part not yet produced reports NotFound once this
196    // call's `await_policy` has already expired (a real HTTP adapter would
197    // instead give it a real deadline and block on `Trunk::listen()`)...
198    match resolve(
199        &origin,
200        HlsRequest::Resource {
201            name: "part-1-2.1.m4s".to_string(),
202        },
203    ) {
204        EgressResponse::NotFound => {
205            println!(
206                "resolve(Resource, part-1-2.1.m4s) -> NotFound (Await's patience already expired)"
207            )
208        }
209        other => panic!("expected NotFound, got {other:?}"),
210    }
211    // ...and an unrecognised filename is a plain 404.
212    match resolve(
213        &origin,
214        HlsRequest::Resource {
215            name: "nope.txt".to_string(),
216        },
217    ) {
218        EgressResponse::NotFound => println!("resolve(Resource, nope.txt)       -> NotFound"),
219        other => panic!("expected NotFound, got {other:?}"),
220    }
221}