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