Skip to main content

client_stepping/
client_stepping.rs

1//! Drive the sans-IO LL-HLS **client** engine (`ll_hls_runtime::client`)
2//! against a canned Media Playlist — no socket, no real network. The
3//! playlist text itself comes from this crate's own origin engine
4//! ([`ll_hls_runtime::server::LlHlsOrigin`]), so it is guaranteed
5//! well-formed LL-HLS syntax (the exact symmetric counterpart
6//! `MediaPlaylist::parse` is written against) rather than hand-typed text
7//! that could drift from what the parser actually accepts.
8//!
9//! # Usage
10//!
11//! ```bash
12//! cargo run --example client_stepping -p ll-hls-runtime
13//! ```
14
15use std::num::NonZeroUsize;
16use std::time::Duration;
17
18use broadcast_common::Timestamp;
19use ll_hls_runtime::client::{Action, LlHlsClient};
20use ll_hls_runtime::server::{
21    BlockingQuery, DEFAULT_TRACK_ID, LlHlsBody, LlHlsOrigin, LlHlsRequest,
22};
23use media_plane::egress::{AwaitPolicy, EgressResponse, ServedEgress};
24use media_plane::trunk::{PartEntry, SegmentEntry, Trunk, TrunkConfig};
25use transmux::SegmentMeta;
26
27const PLAYLIST_URL: &str = "http://origin/live/media.m3u8";
28
29fn nz(n: usize) -> NonZeroUsize {
30    NonZeroUsize::new(n).expect("example capacity must be non-zero")
31}
32
33/// Builds a small, valid canned Media Playlist: one closed segment plus an
34/// open segment with one part already landed — enough to exercise an init
35/// fetch, a part fetch, a preload-hint prefetch, and (since this crate's own
36/// renderer defaults `CAN-BLOCK-RELOAD=YES`) a Blocking Playlist Reload for
37/// the next request.
38fn canned_playlist() -> String {
39    let trunk = Trunk::new(TrunkConfig::new(nz(16), nz(4), nz(8), nz(4), nz(16)));
40    let writer = trunk
41        .segment_writer()
42        .expect("first (and only) segment writer");
43    let origin = LlHlsOrigin::new(std::sync::Arc::clone(&trunk), 1.0, 500, nz(4));
44    origin.set_init(vec![0xAA; 32]);
45
46    writer.publish_part(PartEntry::new(
47        vec![0x01; 16],
48        1,
49        0,
50        Duration::from_millis(500),
51        true,
52    ));
53    writer.publish_segment(SegmentEntry::new(
54        vec![0x02; 32],
55        1,
56        Duration::from_secs(1),
57        Timestamp::from_nanos(0),
58        SegmentMeta {
59            discontinuous: false,
60        },
61    ));
62    writer.publish_part(PartEntry::new(
63        vec![0x03; 16],
64        2,
65        0,
66        Duration::from_millis(500),
67        true,
68    ));
69
70    match origin.resolve(
71        LlHlsRequest::Playlist {
72            track_id: DEFAULT_TRACK_ID,
73            query: BlockingQuery::default(),
74        },
75        Timestamp::from_nanos(0),
76        AwaitPolicy::new(Timestamp::from_nanos(0)),
77    ) {
78        EgressResponse::Ready {
79            body: LlHlsBody::Playlist(m),
80            ..
81        } => m,
82        other => panic!("expected Ready(Playlist), got {other:?}"),
83    }
84}
85
86fn main() {
87    let playlist = canned_playlist();
88    println!("--- canned media.m3u8 ---\n{playlist}");
89
90    let mut client = LlHlsClient::new(PLAYLIST_URL);
91
92    // The client always seeds a plain (non-blocking) GET first — it hasn't
93    // seen a playlist yet, so it doesn't know the origin supports blocking
94    // reload.
95    match client.poll() {
96        Some(Action::FetchPlaylist {
97            url,
98            blocking,
99            skip,
100        }) => {
101            assert_eq!(url, PLAYLIST_URL);
102            assert!(blocking.is_none());
103            assert!(!skip);
104            println!("action: FetchPlaylist {{ url: {url:?}, blocking: None }}");
105        }
106        other => panic!("expected the seeded FetchPlaylist, got {other:?}"),
107    }
108
109    // Feed the canned playlist in response to that (imagined) GET — no HTTP
110    // client is ever involved.
111    client
112        .on_playlist(playlist.as_bytes())
113        .expect("the canned playlist parses");
114
115    // Drain every action the client now wants performed: the closed
116    // segment's bytes, the open segment's landed part, the init segment
117    // (from `#EXT-X-MAP`), the preload-hinted next part, and finally a
118    // Blocking Playlist Reload naming the next Media Sequence Number/part.
119    let mut saw_blocking_reload = false;
120    while let Some(action) = client.poll() {
121        match &action {
122            Action::FetchResource { id, url, .. } => {
123                println!("action: FetchResource {{ id: {id:?}, url: {url:?} }}");
124            }
125            Action::FetchPlaylist {
126                url,
127                blocking: Some(b),
128                ..
129            } => {
130                println!(
131                    "action: FetchPlaylist {{ url: {url:?}, blocking: {b:?} }}  <- blocking reload"
132                );
133                saw_blocking_reload = true;
134            }
135            Action::FetchPlaylist {
136                url,
137                blocking: None,
138                ..
139            } => {
140                println!("action: FetchPlaylist {{ url: {url:?}, blocking: None }}");
141            }
142            Action::WaitMs(ms) => println!("action: WaitMs({ms})"),
143            // `Action` is `#[non_exhaustive]` — a future variant is simply
144            // not printed by this demo, not a compile break.
145            _ => {}
146        }
147    }
148    assert!(
149        saw_blocking_reload,
150        "this crate's own origin renderer defaults CAN-BLOCK-RELOAD=YES, so the \
151         next reload the client schedules must be a blocking one"
152    );
153}