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, Clone, PartialEq)]
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    /// An `EXT-X-BYTERANGE` (or `EXT-X-PRELOAD-HINT` byte range,
83    /// RFC 8216bis §4.4.4.9/§4.4.5.3) `offset + length` overflowed `u64`.
84    /// Both values come straight from the remote origin's playlist text —
85    /// `broadcast_hls::MediaPlaylist::parse` places no upper bound on
86    /// either (confirmed: it is a bare `str::parse::<u64>()`) — so a
87    /// malicious or corrupt origin can advertise e.g.
88    /// `BYTERANGE:18446744073709551615@0`, or a chain of omitted-offset
89    /// ranges whose per-URL running cursor
90    /// ([`crate::client::HlsClient`]'s `byte_range_cursor`) accumulates past
91    /// `u64::MAX`. Rejected outright rather than saturating: a saturated
92    /// range would silently become a *different, wrong* byte range, and the
93    /// client would demux whatever bytes the origin happened to return for
94    /// it — a worse outcome than simply failing this one fetch.
95    #[error("byte range for {url:?} overflows u64: offset {offset} + length {length}")]
96    ByteRangeOverflow {
97        /// The resolved resource URL the range applies to.
98        url: String,
99        /// The range's starting offset (explicit, or the carried-forward
100        /// cursor for this URL).
101        offset: u64,
102        /// The range's length, as advertised by the playlist.
103        length: u64,
104    },
105
106    /// [`crate::client::HlsClient::on_resource`] was fed a second delivery
107    /// of a [`crate::client::ResourceId`] it has already accepted (for
108    /// example a driver retry racing a slow success). Rejected so a part's
109    /// samples are never emitted twice and the outstanding-fetch count never
110    /// drifts (the `Output` exactly-once contract).
111    #[error("resource {id:?} was already delivered")]
112    DuplicateResource {
113        /// The id delivered twice.
114        id: super::action::ResourceId,
115    },
116
117    /// `#EXT-X-MEDIA-SEQUENCE` plus the playlist's segment count overflows
118    /// `u64` (RFC 8216 §4.3.3.2 puts no upper bound on the tag's value, so a
119    /// hostile or corrupt origin can advertise one). Rejected outright
120    /// rather than wrapping the derived Media Sequence Numbers.
121    #[error("media sequence {media_sequence} + {segments} segments overflows u64")]
122    MediaSequenceOverflow {
123        /// The playlist's `EXT-X-MEDIA-SEQUENCE`.
124        media_sequence: u64,
125        /// The number of closed segments the playlist lists.
126        segments: usize,
127    },
128
129    /// [`crate::client::HlsClient::select_variant`] named a variant index that
130    /// does not exist, or was called before any master playlist had been seen.
131    #[error("no such variant: {index}")]
132    NoSuchVariant {
133        /// The requested index.
134        index: usize,
135    },
136
137    /// [`crate::client::HlsClient::on_key`] was fed bytes that are not exactly
138    /// one AES-128 key (RFC 8216 §5.1: a key file is 16 octets).
139    #[error("key {id:?} is {len} bytes, an AES-128 key is exactly 16")]
140    BadKeyLength {
141        /// The key whose response was the wrong size.
142        id: super::action::KeyId,
143        /// The number of bytes received.
144        len: usize,
145    },
146
147    /// [`crate::client::HlsClient::on_key`] was fed a [`super::action::KeyId`]
148    /// the client never assigned, or one whose key it already holds.
149    #[error("key delivered for an id the client did not request: {id:?}")]
150    UnrequestedKey {
151        /// The unexpected id.
152        id: super::action::KeyId,
153    },
154
155    /// A segment could not be AES-128 decrypted (ciphertext not a positive
156    /// multiple of 16 bytes, or invalid PKCS7 padding — e.g. the wrong key).
157    #[error("segment {id:?} could not be decrypted with key {key_id:?}")]
158    DecryptFailed {
159        /// The segment that failed to decrypt.
160        id: super::action::ResourceId,
161        /// The key it was decrypted with.
162        key_id: super::action::KeyId,
163    },
164
165    /// The playlist uses an encryption this client does not decrypt. The
166    /// segment is never passed through as plaintext.
167    #[error("unsupported encryption ({method}): {what}")]
168    UnsupportedEncryption {
169        /// The `METHOD` of the key that applies.
170        method: broadcast_hls::EncryptionMethod,
171        /// What is unsupported.
172        what: &'static str,
173    },
174
175    /// Encrypted segments waiting for their key exceeded the buffering cap;
176    /// the oldest was dropped and will be requested again by a later reload.
177    #[error("encrypted segments waiting for a key exceed the buffer cap; dropped {dropped:?}")]
178    PendingKeyedOverflow {
179        /// The ciphertext that was dropped.
180        dropped: super::action::ResourceId,
181    },
182
183    /// A delta playlist (`EXT-X-SKIP`) could not be merged into a full view,
184    /// so none of its segments was processed; a full playlist has been
185    /// requested instead.
186    #[error(
187        "delta playlist at media sequence {media_sequence} skipping {skipped_segments} segments cannot be merged"
188    )]
189    DeltaNotMergeable {
190        /// The delta's `EXT-X-MEDIA-SEQUENCE`.
191        media_sequence: u64,
192        /// Its `EXT-X-SKIP` `SKIPPED-SEGMENTS`.
193        skipped_segments: u64,
194    },
195}