ll_hls_runtime/client/action.rs
1//! Actions the caller must perform IO for — [`Action`] out of
2//! [`crate::client::LlHlsClient::poll`].
3
4use alloc::format;
5use alloc::string::String;
6
7/// Identifies one fetchable resource: the initialisation segment, a Low-Latency
8/// HLS partial segment ("part", RFC 8216bis §4.4.4.9), or a whole media
9/// segment. Used to correlate an [`Action::FetchResource`] with the matching
10/// [`crate::client::LlHlsClient::on_resource`] call.
11///
12/// `Part`/`Segment` are keyed by Media Sequence Number (RFC 8216 §4.3.3.2) +
13/// (for parts) the 0-based Part Index within that segment — stable identity
14/// independent of the URI, since a delta/blocking reload can re-describe the
15/// same resource with the same URI more than once.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[non_exhaustive]
19pub enum ResourceId {
20 /// The Media Initialisation Section (`EXT-X-MAP`, RFC 8216bis §4.4.4.5).
21 Init,
22 /// One partial segment (RFC 8216bis §4.4.4.9): `msn`'s Media Sequence
23 /// Number, `part` its 0-based Part Index.
24 Part {
25 /// Media Sequence Number of the parent (possibly still-open) segment.
26 msn: u64,
27 /// 0-based Part Index within that segment.
28 part: u64,
29 },
30 /// One whole media segment (RFC 8216 §4.3.3, non-LL full-segment
31 /// fallback — a segment whose parts were never individually fetched).
32 Segment {
33 /// Media Sequence Number.
34 msn: u64,
35 },
36}
37
38/// A pending blocking Playlist Reload request (RFC 8216bis §6.2.5.2): the
39/// client has consumed the playlist up to (and possibly including) a given
40/// Partial Segment and wants the server to hold the response until something
41/// newer exists.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub struct BlockingReload {
45 /// `_HLS_msn` — Media Sequence Number of the next segment (open or not
46 /// yet begun) the client wants.
47 pub msn: u64,
48 /// `_HLS_part` — 0-based Part Index within `msn`. `None` means a bare
49 /// `_HLS_msn` request: RFC 8216bis §6.2.5.2 says this waits for `msn` to
50 /// become a **closed** Media Segment, distinct from `_HLS_part=0` (which
51 /// is satisfied by just the first part).
52 pub part: Option<u64>,
53}
54
55/// One unit of IO the caller must perform, in response to a
56/// [`crate::client::LlHlsClient::poll`] call. The client core never touches a socket
57/// or a clock itself — every `Action` names exactly what to fetch and, for a
58/// playlist reload, how to shape the request.
59#[derive(Debug, Clone, PartialEq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61#[non_exhaustive]
62pub enum Action {
63 /// Fetch (or long-poll) the Media Playlist.
64 FetchPlaylist {
65 /// The playlist URL.
66 url: String,
67 /// `Some` to add `_HLS_msn`/`_HLS_part` query parameters requesting a
68 /// Blocking Playlist Reload (RFC 8216bis §6.2.5.2); `None` for a
69 /// plain (non-blocking) GET — used when the last-seen playlist did
70 /// not advertise `CAN-BLOCK-RELOAD` support.
71 blocking: Option<BlockingReload>,
72 /// Add `_HLS_skip=YES` (RFC 8216bis §6.2.5.1) requesting a Playlist
73 /// Delta Update — only set once the client has already seen a full
74 /// playlist to reconstruct a delta response against, and the last
75 /// playlist advertised `CAN-SKIP-UNTIL`.
76 skip: bool,
77 },
78 /// Fetch one resource (init/part/segment).
79 FetchResource {
80 /// Correlates the eventual [`crate::client::LlHlsClient::on_resource`] call.
81 id: ResourceId,
82 /// The resource URL (already resolved against the playlist URL).
83 url: String,
84 /// `Some((offset, length))` to fetch only that byte sub-range
85 /// (RFC 8216bis §4.4.4.2/§4.4.4.9's `EXT-X-BYTERANGE`/`BYTERANGE`);
86 /// `None` fetches the entire resource.
87 byte_range: Option<(u64, u64)>,
88 },
89 /// A non-blocking-reload timing hint: wait about this many milliseconds
90 /// before issuing the next `FetchPlaylist`, derived from
91 /// `#EXT-X-TARGETDURATION` (RFC 8216 §4.3.3.1's "SHOULD NOT ... more
92 /// frequently than once every Target Duration" reload guidance, halved
93 /// for headroom) when the origin does not support blocking reload.
94 WaitMs(u64),
95}
96
97impl Action {
98 /// For [`Action::FetchPlaylist`], the fully query-augmented URL to fetch:
99 /// `_HLS_msn`/`_HLS_part` (RFC 8216bis §6.2.5.2) appended per
100 /// [`Self::FetchPlaylist`]'s `blocking` field, then `_HLS_skip=YES`
101 /// (RFC 8216bis §6.2.5.1) if `skip` is set. `None` for
102 /// [`Action::FetchResource`]/[`Action::WaitMs`] (nothing to augment —
103 /// use their `url`/byte-range fields directly).
104 ///
105 /// This is a convenience for building the real HTTP request; the typed
106 /// `blocking`/`skip` fields remain the source of truth (e.g. for a
107 /// caller using a query-parameter API rather than string concatenation).
108 pub fn playlist_request_url(&self) -> Option<String> {
109 match self {
110 Action::FetchPlaylist {
111 url,
112 blocking,
113 skip,
114 } => {
115 let mut u = url.clone();
116 if let Some(b) = blocking {
117 u = super::url::append_query(&u, &format!("_HLS_msn={}", b.msn));
118 if let Some(part) = b.part {
119 u = super::url::append_query(&u, &format!("_HLS_part={part}"));
120 }
121 }
122 if *skip {
123 u = super::url::append_query(&u, "_HLS_skip=YES");
124 }
125 Some(u)
126 }
127 Action::FetchResource { .. } | Action::WaitMs(_) => None,
128 }
129 }
130}