Skip to main content

hls_runtime/client/
error.rs

1//! Error type returned by [`crate::client::HlsClient`].
2
3use alloc::string::String;
4use thiserror::Error;
5
6/// Crate-wide result alias.
7pub type Result<T> = core::result::Result<T, Error>;
8
9/// Error variants [`crate::client::HlsClient`] can return.
10#[derive(Debug, Error)]
11#[non_exhaustive]
12pub enum Error {
13    /// The playlist bytes fed to [`crate::client::HlsClient::on_playlist`]
14    /// are not valid UTF-8 (RFC 8216 §4.1 playlists are UTF-8 text).
15    #[error("playlist is not valid UTF-8: {0}")]
16    PlaylistNotUtf8(#[from] core::str::Utf8Error),
17
18    /// `broadcast_hls::MediaPlaylist::parse` rejected the playlist text (a
19    /// known tag with a missing/unparsable required attribute).
20    #[error("playlist parse: {0}")]
21    PlaylistParse(#[from] broadcast_hls::Error),
22
23    /// [`crate::client::HlsClient::on_resource`] was fed bytes for a
24    /// [`crate::client::ResourceId`] the client never requested (a
25    /// caller/driver bug, or a stale/duplicate delivery after the client
26    /// already moved past it).
27    #[error("resource delivered for an id the client did not request: {id:?}")]
28    UnrequestedResource {
29        /// The unexpected id.
30        id: super::action::ResourceId,
31    },
32
33    /// Defensive-only: a Part/Segment reached the demux step with no init
34    /// segment cached. In normal operation this cannot happen —
35    /// [`crate::client::HlsClient::on_resource`] buffers any Part/Segment
36    /// that arrives before the init segment and replays it once the init is
37    /// delivered, so callers may complete fetches in any order.
38    #[error("resource {id:?} delivered before the init segment was available")]
39    InitNotYetAvailable {
40        /// The id that could not be demuxed yet.
41        id: super::action::ResourceId,
42    },
43
44    /// Demuxing the concatenation of the init segment + a fetched Part/Segment
45    /// resource (via `transmux::Fmp4Demux`) failed.
46    #[error("demux of resource {id:?} failed: {source}")]
47    Demux {
48        /// The id whose bytes failed to demux.
49        id: super::action::ResourceId,
50        /// The underlying transmux error.
51        #[source]
52        source: transmux::Error,
53    },
54
55    /// A segment of an fMP4 playlist is a progressive movie (`moov`, no
56    /// `moof`): HLS fMP4 segments carry movie fragments (RFC 8216 §3.3), so no
57    /// sample can be taken from it.
58    #[error("resource {id:?} is a non-fragmented MP4: HLS fMP4 segments carry movie fragments")]
59    NotFragmented {
60        /// The id whose bytes are a progressive MP4.
61        id: super::action::ResourceId,
62    },
63
64    /// A fetched Part/Segment answered with bytes that are not media for its
65    /// container: for MPEG-TS (ISO/IEC 13818-1 §2.4.3.2) no sync-byte lattice, for ISO BMFF (ISO/IEC 14496-12 §8.8.4) no complete
66    /// `moof` followed by its `mdat`. The resource is marked unrequested, so a
67    /// retry ([`crate::client::HlsClient::retry_resource`] or the next
68    /// playlist) fetches it again. Structurally valid media that yields no
69    /// samples is not this error.
70    #[error("resource {id:?} is not media for its container")]
71    NotMedia {
72        /// The id whose bytes are not media.
73        id: super::action::ResourceId,
74    },
75
76    /// A fetched Media Initialization Section holds no `moov` box (it is not an
77    /// ISO BMFF init at all, or it is truncated). The init is marked failed, so
78    /// [`crate::client::HlsClient::retry_init`] fetches it again.
79    #[error("media initialization section holds no complete moov box")]
80    InitUndecodable,
81
82    /// A playlist URI (segment/part/map/rendition-report) could not be
83    /// resolved against the playlist's own URL.
84    #[error("could not resolve URI {uri:?} against base {base:?}: {reason}")]
85    UriResolve {
86        /// The (possibly relative) URI from the playlist.
87        uri: String,
88        /// The base URL it was resolved against.
89        base: String,
90        /// Human-readable explanation.
91        reason: &'static str,
92    },
93
94    /// An `EXT-X-BYTERANGE` (or `EXT-X-PRELOAD-HINT` byte range,
95    /// RFC 8216bis §4.4.4.9/§4.4.5.3) `offset + length` overflowed `u64`.
96    /// Both values come straight from the remote origin's playlist text —
97    /// `broadcast_hls::MediaPlaylist::parse` places no upper bound on
98    /// either (confirmed: it is a bare `str::parse::<u64>()`) — so a
99    /// malicious or corrupt origin can advertise e.g.
100    /// `BYTERANGE:18446744073709551615@0`, or a chain of omitted-offset
101    /// ranges whose per-URL running cursor
102    /// ([`crate::client::HlsClient`]'s `byte_range_cursor`) accumulates past
103    /// `u64::MAX`. Rejected outright rather than saturating: a saturated
104    /// range would silently become a *different, wrong* byte range, and the
105    /// client would demux whatever bytes the origin happened to return for
106    /// it — a worse outcome than simply failing this one fetch.
107    #[error("byte range for {url:?} overflows u64: offset {offset} + length {length}")]
108    ByteRangeOverflow {
109        /// The resolved resource URL the range applies to.
110        url: String,
111        /// The range's starting offset (explicit, or the carried-forward
112        /// cursor for this URL).
113        offset: u64,
114        /// The range's length, as advertised by the playlist.
115        length: u64,
116    },
117
118    /// [`crate::client::HlsClient::on_resource`] was fed a second delivery
119    /// of a [`crate::client::ResourceId`] it has already accepted (for
120    /// example a driver retry racing a slow success). Rejected so a part's
121    /// samples are never emitted twice and the outstanding-fetch count never
122    /// drifts (the `Output` exactly-once contract).
123    #[error("resource {id:?} was already delivered")]
124    DuplicateResource {
125        /// The id delivered twice.
126        id: super::action::ResourceId,
127    },
128
129    /// `#EXT-X-MEDIA-SEQUENCE` plus the playlist's segment count overflows
130    /// `u64` (RFC 8216 §4.3.3.2 puts no upper bound on the tag's value, so a
131    /// hostile or corrupt origin can advertise one). Rejected outright
132    /// rather than wrapping the derived Media Sequence Numbers.
133    #[error("media sequence {media_sequence} + {segments} segments overflows u64")]
134    MediaSequenceOverflow {
135        /// The playlist's `EXT-X-MEDIA-SEQUENCE`.
136        media_sequence: u64,
137        /// The number of closed segments the playlist lists.
138        segments: usize,
139    },
140
141    /// [`crate::client::HlsClient::select_variant`] named a variant index that
142    /// does not exist, or was called before any master playlist had been seen.
143    #[error("no such variant: {index}")]
144    NoSuchVariant {
145        /// The requested index.
146        index: usize,
147    },
148
149    /// [`crate::client::HlsClient::on_key`] was fed bytes that are not exactly
150    /// one AES-128 key (RFC 8216 §5.1: a key file is 16 octets).
151    #[error("key {id:?} is {len} bytes, an AES-128 key is exactly 16")]
152    BadKeyLength {
153        /// The key whose response was the wrong size.
154        id: super::action::KeyId,
155        /// The number of bytes received.
156        len: usize,
157    },
158
159    /// [`crate::client::HlsClient::on_key`] was fed a [`super::action::KeyId`]
160    /// the client never assigned, or one whose key it already holds.
161    #[error("key delivered for an id the client did not request: {id:?}")]
162    UnrequestedKey {
163        /// The unexpected id.
164        id: super::action::KeyId,
165    },
166
167    /// A segment could not be AES-128 decrypted (ciphertext not a positive
168    /// multiple of 16 bytes, or invalid PKCS7 padding — e.g. the wrong key).
169    #[error("segment {id:?} could not be decrypted with key {key_id:?}")]
170    DecryptFailed {
171        /// The segment that failed to decrypt.
172        id: super::action::ResourceId,
173        /// The key it was decrypted with.
174        key_id: super::action::KeyId,
175    },
176
177    /// The playlist uses an encryption this client does not decrypt. The
178    /// segment is never passed through as plaintext.
179    #[error("unsupported encryption ({method}): {what}")]
180    UnsupportedEncryption {
181        /// The `METHOD` of the key that applies.
182        method: broadcast_hls::EncryptionMethod,
183        /// What is unsupported.
184        what: &'static str,
185    },
186
187    /// Encrypted segments waiting for their key exceeded the buffering cap;
188    /// the oldest was dropped and will be requested again by a later reload.
189    #[error("encrypted segments waiting for a key exceed the buffer cap; dropped {dropped:?}")]
190    PendingKeyedOverflow {
191        /// The ciphertext that was dropped.
192        dropped: super::action::ResourceId,
193    },
194
195    /// A delta playlist (`EXT-X-SKIP`) could not be merged into a full view,
196    /// so none of its segments was processed; a full playlist has been
197    /// requested instead.
198    #[error(
199        "delta playlist at media sequence {media_sequence} skipping {skipped_segments} segments cannot be merged"
200    )]
201    DeltaNotMergeable {
202        /// The delta's `EXT-X-MEDIA-SEQUENCE`.
203        media_sequence: u64,
204        /// Its `EXT-X-SKIP` `SKIPPED-SEGMENTS`.
205        skipped_segments: u64,
206    },
207}