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::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::HlsClient::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::HlsClient::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::HlsClient::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}