Skip to main content

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}