pub struct HlsClient { /* private fields */ }Expand description
A driveable, sans-IO Low-Latency HLS (RFC 8216bis) playback client.
HlsClient never touches a socket or a clock. The caller drives it:
HlsClient::newseeds the firstAction::FetchPlaylist; drain it withHlsClient::polland perform the GET.- Feed the response back with
HlsClient::on_playlist(playlist) orHlsClient::on_resource(init/part/segment bytes) —Action::FetchResource’sidcorrelates the two. - Drain
HlsClient::pollagain for the next round of actions (a new reload, newly discoverable parts, a preload-hint prefetch, …) andHlsClient::next_outputfor newly availableOutputs.
§Behaviour
- Reload scheduling (issue #717 slice 2): once a playlist advertises
EXT-X-SERVER-CONTROL/EXT-X-PART-INFand the origin’sCAN-BLOCK-RELOADattribute isYES(broadcast_hls::LowLatencyConfig::can_block_reloadistrue— not merelybroadcast_hls::MediaPlaylist::low_latencybeingSome, since an origin may carry parts/PART-INF while still advertisingCAN-BLOCK-RELOAD=NO), every reload is a Blocking Playlist Reload (RFC 8216bis §6.2.5.2) naming the next not-yet-seen Partial Segment’s_HLS_msn/_HLS_part. Otherwise reloads are plain GETs paced by anAction::WaitMshint derived from#EXT-X-TARGETDURATION.EXT-X-SKIP/CAN-SKIP-UNTILPlaylist Delta Updates (RFC 8216bis §4.4.5.2) are requested once a full-playlist baseline exists, and merged back into a full view before further processing — seemerge_deltainternally. - Fetch pipeline (slice 3): the
EXT-X-PRELOAD-HINTed part is fetched ahead of its own appearance as a numberedEXT-X-PART;BYTERANGEparts are supported, including the RFC 8216bis §4.4.4.9 “omitted offset means immediately after the previous sub-range of the same resource” rule (tracked per resource URL). The Media Initialization Section (EXT-X-MAP) is fetched once and reused for every following resource until the map changes. - Dedup / coalescing: once any of a segment’s parts have been
individually fetched, that segment is never re-fetched whole — when it
later closes (
#EXTINF+URI), the client only fetches whichever of its parts (if any) are still missing, and marks the segment “delivered” once every part is accounted for (fetched, orGAP=YES). A playlist whose segments carry no parts at all (a non-LL origin) falls back to fetching the whole segment resource — the two paths never overlap for a single segment, so a part’s samples are never double-counted against its parent’s. - Output adapter (slice 4): exactly one
Output::Initprecedes anyOutput::Samples; parts/segments are demuxed viatransmux::Fmp4Demux(by concatenating the cached init bytes with the fetched resource — this crate never re-implements ISOBMFF box parsing, only reuses transmux’s), soOutput::Samplescarries real access units, not opaque container bytes.#EXT-X-DISCONTINUITYon a segment surfaces asOutput::Discontinuityimmediately before that segment’s first samples. Known limitation: an in-progress (OpenSegment) segment carries no discontinuity flag of its own (only a closedMediaSegmentdoes) — if every part of a segment was already delivered while it was still open, a discontinuity revealed only once it closes is signalled late (after those parts’ samples, not before). This is a gap in the current wire model (broadcast_hls::OpenSegment), not something this crate can fix locally. - Classic MPEG-TS-segment HLS (issue #760): a playlist that never
advertises an
EXT-X-MAP(HLS v3, the dominant legacy/IPTV form — self-contained.tssegments carrying their own PAT/PMT/PES, no separate init resource) routes each fetched Part/Segment throughtransmux::TsDemuxinstead, content-sniffed by the MPEG-TS sync byte rather than blocked on an init fetch that will never come. The first successfully demuxed segment’s recoveredTrackSpecs synthesize the oneOutput::Initthis crate’s contract requires (viatransmux::build_init_segment) so downstream callers (e.g.multimux’sHlsPull, which recovers track specs fromOutput::Init) need no TS-specific handling of their own. The fMP4/CMAF plus LL (parts/preload-hint) path above is entirely unchanged; the two never overlap for a single playlist.
Implementations§
Source§impl HlsClient
impl HlsClient
Sourcepub fn new(playlist_url: impl Into<String>) -> Self
pub fn new(playlist_url: impl Into<String>) -> Self
Create a new client for the Media Playlist at playlist_url, seeding
the first Action::FetchPlaylist (a plain, non-blocking GET — the
client does not yet know whether the origin supports blocking reload).
Examples found in repository?
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 // The client always seeds a plain (non-blocking) GET first — it hasn't
96 // seen a playlist yet, so it doesn't know the origin supports blocking
97 // reload.
98 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 // Feed the canned playlist in response to that (imagined) GET — no HTTP
113 // client is ever involved.
114 client
115 .on_playlist(playlist.as_bytes())
116 .expect("the canned playlist parses");
117
118 // Drain every action the client now wants performed: the closed
119 // segment's bytes, the open segment's landed part, the init segment
120 // (from `#EXT-X-MAP`), the preload-hinted next part, and finally a
121 // Blocking Playlist Reload naming the next Media Sequence Number/part.
122 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 // `Action` is `#[non_exhaustive]` — a future variant is simply
147 // not printed by this demo, not a compile break.
148 _ => {}
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}Sourcepub fn playlist_url(&self) -> &str
pub fn playlist_url(&self) -> &str
The Media Playlist URL this client is following.
Sourcepub fn poll(&mut self) -> Option<Action>
pub fn poll(&mut self) -> Option<Action>
Drain the next IO Action the caller must perform, if any.
Examples found in repository?
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 // The client always seeds a plain (non-blocking) GET first — it hasn't
96 // seen a playlist yet, so it doesn't know the origin supports blocking
97 // reload.
98 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 // Feed the canned playlist in response to that (imagined) GET — no HTTP
113 // client is ever involved.
114 client
115 .on_playlist(playlist.as_bytes())
116 .expect("the canned playlist parses");
117
118 // Drain every action the client now wants performed: the closed
119 // segment's bytes, the open segment's landed part, the init segment
120 // (from `#EXT-X-MAP`), the preload-hinted next part, and finally a
121 // Blocking Playlist Reload naming the next Media Sequence Number/part.
122 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 // `Action` is `#[non_exhaustive]` — a future variant is simply
147 // not printed by this demo, not a compile break.
148 _ => {}
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}Sourcepub fn next_output(&mut self) -> Option<Output>
pub fn next_output(&mut self) -> Option<Output>
Drain the next Output event, if any.
Sourcepub fn on_playlist(&mut self, bytes: &[u8]) -> Result<()>
pub fn on_playlist(&mut self, bytes: &[u8]) -> Result<()>
Feed a freshly fetched Media Playlist response.
§Errors
Error::PlaylistNotUtf8 / Error::PlaylistParse on malformed
input.
Examples found in repository?
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 // The client always seeds a plain (non-blocking) GET first — it hasn't
96 // seen a playlist yet, so it doesn't know the origin supports blocking
97 // reload.
98 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 // Feed the canned playlist in response to that (imagined) GET — no HTTP
113 // client is ever involved.
114 client
115 .on_playlist(playlist.as_bytes())
116 .expect("the canned playlist parses");
117
118 // Drain every action the client now wants performed: the closed
119 // segment's bytes, the open segment's landed part, the init segment
120 // (from `#EXT-X-MAP`), the preload-hinted next part, and finally a
121 // Blocking Playlist Reload naming the next Media Sequence Number/part.
122 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 // `Action` is `#[non_exhaustive]` — a future variant is simply
147 // not printed by this demo, not a compile break.
148 _ => {}
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}Sourcepub fn on_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()>
pub fn on_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()>
Feed the bytes fetched for a previously requested ResourceId
(init/part/segment). Part/Segment resources delivered before the
init segment are buffered internally and demuxed once the init
arrives — the caller’s fetches may complete in any order.
§Errors
Error::UnrequestedResource if id was never requested (the
requested bookkeeping — or, for Init, init_uri — has no record
of it): a caller/driver bug, or a stale/duplicate delivery after the
client already moved past this id.
Error::Demux if transmux::Fmp4Demux rejects the concatenation of
the cached init + bytes.
Sourcepub fn on_error(&mut self, id: Option<ResourceId>)
pub fn on_error(&mut self, id: Option<ResourceId>)
Report that a previously requested ResourceId (or the playlist
itself, via None) failed. Clears the id’s “requested” bookkeeping
so the next Self::on_playlist call naturally re-requests it (no
automatic retry timer — the caller drives retry cadence).