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