client_stepping/
client_stepping.rs1use std::num::NonZeroUsize;
16use std::time::Duration;
17
18use broadcast_common::Timestamp;
19use hls_runtime::client::{Action, HlsClient};
20use hls_runtime::server::{BlockingQuery, DEFAULT_TRACK_ID, HlsBody, HlsOrigin, HlsRequest};
21use media_plane::egress::{AwaitPolicy, EgressResponse, ServedEgress};
22use media_plane::trunk::{PartEntry, SegmentEntry, Trunk, TrunkConfig};
23use transmux::SegmentMeta;
24
25const PLAYLIST_URL: &str = "http://origin/live/media.m3u8";
26
27fn nz(n: usize) -> NonZeroUsize {
28 NonZeroUsize::new(n).expect("example capacity must be non-zero")
29}
30
31fn canned_playlist() -> String {
37 let trunk = Trunk::new(TrunkConfig::new(nz(16), nz(4), nz(8), nz(4), nz(16)));
38 let writer = trunk
39 .segment_writer()
40 .expect("first (and only) segment writer");
41 let origin = HlsOrigin::builder(std::sync::Arc::clone(&trunk))
42 .target_duration_secs(1.0)
43 .window_segments(nz(4))
44 .low_latency(500)
45 .build()
46 .expect("both required fields set");
47 origin.set_init(vec![0xAA; 32]);
48
49 writer.publish_part(PartEntry::new(
50 vec![0x01; 16],
51 1,
52 0,
53 Duration::from_millis(500),
54 true,
55 ));
56 writer.publish_segment(SegmentEntry::new(
57 vec![0x02; 32],
58 1,
59 Duration::from_secs(1),
60 Timestamp::from_nanos(0),
61 SegmentMeta {
62 discontinuous: false,
63 },
64 ));
65 writer.publish_part(PartEntry::new(
66 vec![0x03; 16],
67 2,
68 0,
69 Duration::from_millis(500),
70 true,
71 ));
72
73 match origin.resolve(
74 HlsRequest::Playlist {
75 track_id: DEFAULT_TRACK_ID,
76 query: BlockingQuery::default(),
77 },
78 Timestamp::from_nanos(0),
79 AwaitPolicy::new(Timestamp::from_nanos(0)),
80 ) {
81 EgressResponse::Ready {
82 body: HlsBody::Playlist(m),
83 ..
84 } => m,
85 other => panic!("expected Ready(Playlist), got {other:?}"),
86 }
87}
88
89fn main() {
90 let playlist = canned_playlist();
91 println!("--- canned media.m3u8 ---\n{playlist}");
92
93 let mut client = HlsClient::new(PLAYLIST_URL);
94
95 match client.poll() {
99 Some(Action::FetchPlaylist {
100 url,
101 blocking,
102 skip,
103 }) => {
104 assert_eq!(url, PLAYLIST_URL);
105 assert!(blocking.is_none());
106 assert!(!skip);
107 println!("action: FetchPlaylist {{ url: {url:?}, blocking: None }}");
108 }
109 other => panic!("expected the seeded FetchPlaylist, got {other:?}"),
110 }
111
112 client
115 .on_playlist(playlist.as_bytes())
116 .expect("the canned playlist parses");
117
118 let mut saw_blocking_reload = false;
123 while let Some(action) = client.poll() {
124 match &action {
125 Action::FetchResource { id, url, .. } => {
126 println!("action: FetchResource {{ id: {id:?}, url: {url:?} }}");
127 }
128 Action::FetchPlaylist {
129 url,
130 blocking: Some(b),
131 ..
132 } => {
133 println!(
134 "action: FetchPlaylist {{ url: {url:?}, blocking: {b:?} }} <- blocking reload"
135 );
136 saw_blocking_reload = true;
137 }
138 Action::FetchPlaylist {
139 url,
140 blocking: None,
141 ..
142 } => {
143 println!("action: FetchPlaylist {{ url: {url:?}, blocking: None }}");
144 }
145 Action::WaitMs(ms) => println!("action: WaitMs({ms})"),
146 _ => {}
149 }
150 }
151 assert!(
152 saw_blocking_reload,
153 "this crate's own origin renderer defaults CAN-BLOCK-RELOAD=YES, so the \
154 next reload the client schedules must be a blocking one"
155 );
156}