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}