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:2026 §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 /// A playlist reference (a segment, Part, init, key or variant URI) could
130 /// not be resolved against the playlist URL: it carried an ASCII control
131 /// character, or the two did not form a URL (issue #2458). The reference is
132 /// never sanitised and never requested; playback carries on with the rest
133 /// of the playlist.
134 #[error("unresolvable playlist reference {uri:?}: {source}")]
135 UnresolvableReference {
136 /// The reference exactly as the playlist wrote it.
137 uri: String,
138 /// Why it could not be resolved.
139 #[source]
140 source: broadcast_common::url_ref::Error,
141 },
142
143 /// [`crate::client::HlsClient::select_variant`] named a variant index that
144 /// does not exist, or was called before any master playlist had been seen.
145 #[error("no such variant: {index}")]
146 NoSuchVariant {
147 /// The requested index.
148 index: usize,
149 },
150
151 /// [`crate::client::HlsClient::on_key`] was fed bytes that are not exactly
152 /// one AES-128 key (RFC 8216 §5.1: a key file is 16 octets).
153 #[error("key {id:?} is {len} bytes, an AES-128 key is exactly 16")]
154 BadKeyLength {
155 /// The key whose response was the wrong size.
156 id: super::action::KeyId,
157 /// The number of bytes received.
158 len: usize,
159 },
160
161 /// [`crate::client::HlsClient::on_key`] was fed a [`super::action::KeyId`]
162 /// the client never assigned, or one whose key it already holds.
163 #[error("key delivered for an id the client did not request: {id:?}")]
164 UnrequestedKey {
165 /// The unexpected id.
166 id: super::action::KeyId,
167 },
168
169 /// A segment could not be AES-128 decrypted (ciphertext not a positive
170 /// multiple of 16 bytes, or invalid PKCS7 padding — e.g. the wrong key).
171 #[error("segment {id:?} could not be decrypted with key {key_id:?}")]
172 DecryptFailed {
173 /// The segment that failed to decrypt.
174 id: super::action::ResourceId,
175 /// The key it was decrypted with.
176 key_id: super::action::KeyId,
177 },
178
179 /// The playlist uses an encryption this client does not decrypt. The
180 /// segment is never passed through as plaintext.
181 #[error("unsupported encryption ({method}): {what}")]
182 UnsupportedEncryption {
183 /// The `METHOD` of the key that applies.
184 method: broadcast_hls::EncryptionMethod,
185 /// What is unsupported.
186 what: &'static str,
187 },
188
189 /// Encrypted segments waiting for their key exceeded the buffering cap;
190 /// the oldest was dropped and will be requested again by a later reload.
191 #[error("encrypted segments waiting for a key exceed the buffer cap; dropped {dropped:?}")]
192 PendingKeyedOverflow {
193 /// The ciphertext that was dropped.
194 dropped: super::action::ResourceId,
195 },
196
197 /// A delta playlist (`EXT-X-SKIP`) could not be merged into a full view,
198 /// so none of its segments was processed; a full playlist has been
199 /// requested instead.
200 #[error(
201 "delta playlist at media sequence {media_sequence} skipping {skipped_segments} segments cannot be merged"
202 )]
203 DeltaNotMergeable {
204 /// The delta's `EXT-X-MEDIA-SEQUENCE`.
205 media_sequence: u64,
206 /// Its `EXT-X-SKIP` `SKIPPED-SEGMENTS`.
207 skipped_segments: u64,
208 },
209}