client_stepping/
client_stepping.rs1use 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
33fn 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 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 client
112 .on_playlist(playlist.as_bytes())
113 .expect("the canned playlist parses");
114
115 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 _ => {}
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}