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}