mediadecode_ffmpeg/error.rs
1use derive_more::{IsVariant, TryUnwrap, Unwrap};
2use ffmpeg_next::Packet;
3
4use crate::backend::Backend;
5
6/// Crate result alias.
7pub type Result<T> = std::result::Result<T, Error>;
8
9/// Errors returned from [`crate::VideoDecoder`].
10///
11/// `Debug` is derived; the variants that wrap a payload struct
12/// (`HwDeviceInitFailed`, `AllBackendsFailed`, `FallbackFailed`)
13/// delegate their `Debug` to the payload, which is hand-written
14/// where needed because [`ffmpeg_next::Packet`] (carried by
15/// `AllBackendsFailed::unconsumed_packets` /
16/// `FallbackFailed::unconsumed_packets`) does not derive
17/// `Debug`. Those payloads summarize the packet count rather
18/// than dumping each packet's fields, which would be both noisy
19/// and useless for triage.
20///
21/// **Open fault taxonomy, so it is `#[non_exhaustive]`.** New ways to
22/// fail are discovered — a backend, a ceiling, a corruption a codec
23/// learns to report — and a consumer that meets one it has never heard
24/// of should take its generic-fault path. That is exactly what the
25/// wildcard arm this attribute forces is for. The two status
26/// vocabularies opposite it,
27/// [`Sent`](mediadecode::Sent) and [`Received`](mediadecode::Received),
28/// are exhaustive for the mirror-image reason: their arms are the
29/// substrate's fixed state set, and there the wildcard would be dead
30/// weight hiding a state a consumer forgot.
31#[derive(Debug, thiserror::Error, IsVariant, Unwrap, TryUnwrap)]
32#[unwrap(ref, ref_mut)]
33#[try_unwrap(ref, ref_mut)]
34#[non_exhaustive]
35pub enum Error {
36 /// An underlying FFmpeg error.
37 #[error("ffmpeg error: {0}")]
38 Ffmpeg(#[from] ffmpeg_next::Error),
39
40 /// A portable packet could not be rebuilt as an `AVPacket` on its
41 /// way into a decoder — see [`crate::boundary::PacketBuildError`].
42 #[error(transparent)]
43 PacketBuild(#[from] crate::boundary::PacketBuildError),
44
45 /// A stream's codec parameters hold more heap bytes than the
46 /// decoder tier will copy — see
47 /// [`crate::DEFAULT_MAX_CODEC_PARAMETER_BYTES`].
48 ///
49 /// The decoder tier has no options object of its own for this, so it
50 /// applies the default ceiling. A caller that needs a larger one
51 /// opens the parameters through the demux tier, where
52 /// [`DemuxLimits`](crate::DemuxLimits) carries the seat.
53 #[error(transparent)]
54 ParametersTooLarge(#[from] crate::demuxer::ParametersTooLarge),
55
56 /// A stream's channel layout is not a shape FFmpeg's own helpers can
57 /// be given, so the decoder was not opened over it.
58 ///
59 /// **Structural and permanent, never an allocation failure.**
60 /// `avcodec_parameters_to_context` reaches `av_channel_layout_copy`,
61 /// whose `memcpy` reads the custom map with no null check of its own,
62 /// and FFmpeg's describe and compare helpers compute
63 /// `nb_channels - popcount(mask)` — and take an integer square root
64 /// of it — without checking either. None of that is a shortage of
65 /// memory and none of it will be different on the next attempt, which
66 /// is why it does not share the allocation arm.
67 ///
68 /// See
69 /// [`layout_preflight`](crate::channel_layout::layout_preflight) for
70 /// the rule, which the demux admission pass and the outbound clone
71 /// apply too.
72 #[error(transparent)]
73 MalformedChannelLayout(#[from] crate::demuxer::ParametersLayoutShape),
74
75 /// A stream's channel layout declares a custom order without the map
76 /// that order requires — the shape `av_channel_layout_copy` would
77 /// `memcpy` from null. Structural and permanent, as above.
78 #[error(transparent)]
79 ChannelMapMissing(#[from] crate::demuxer::ParametersChannelMap),
80
81 /// `avcodec_find_decoder` returned null for the input codec id. The id
82 /// is reported as the raw integer (`AVCodecID` discriminant) — we do not
83 /// construct the bindgen `AVCodecID` enum from a runtime value, since
84 /// values outside our build's discriminant set would invoke UB.
85 #[error("no decoder for codec id {0}")]
86 NoCodec(u32),
87
88 /// The CPU frame a hardware->CPU transfer would allocate is larger
89 /// than [`FrameLimits::max_frame_bytes`](crate::FrameLimits::max_frame_bytes).
90 ///
91 /// The hardware road's own seat. `judge_buffer` — the allocator hook
92 /// that applies the byte ceiling to aligned dimensions — is **not** a
93 /// universal choke point: `ff_get_buffer` calls `hwaccel->alloc_frame`
94 /// directly for VideoToolbox h264/hevc/vp9 and never reaches
95 /// `get_buffer2` at all, and `av_hwframe_transfer_data` allocates its
96 /// CPU destination outside both. This is the seat for that second
97 /// road, judged before the transfer rather than after it.
98 #[error(transparent)]
99 HwTransferTooLarge(#[from] HwTransferTooLarge),
100
101 /// A frame's allocation would have cost more than
102 /// [`FrameLimits::max_frame_bytes`](crate::FrameLimits::max_frame_bytes),
103 /// so it was refused in the allocator, before the allocation.
104 #[error(transparent)]
105 FrameBudgetExceeded(#[from] FrameBudgetExceeded),
106
107 /// The stream's **coded** surface is over the frame ceiling, so the
108 /// hardware format was declined before its pool could be built.
109 ///
110 /// The two dimension vocabularies: `max_pixels` is applied by
111 /// `ff_set_dimensions` to a stream's *display* dims, and a cropped
112 /// stream can display 32x32 out of a 1920x1088 coded surface. What
113 /// gets allocated is the coded figure, so it is the one judged here —
114 /// and it is judged in **bytes**, priced through the allocator-parity
115 /// footprint against the caller's `max_frame_bytes`, because
116 /// `max_pixels` carries the caller's logical pixel limit and nothing
117 /// about cost.
118 #[error(transparent)]
119 HwSurfaceTooLarge(#[from] HwSurfaceTooLarge),
120
121 /// The codec does not advertise a hardware configuration matching the
122 /// requested backend (via `avcodec_get_hw_config`).
123 #[error("codec does not support backend {0:?}")]
124 BackendUnsupportedByCodec(Backend),
125
126 /// `av_hwdevice_ctx_create` failed for the requested backend. See
127 /// [`HwDeviceInitFailed`] for the payload details. `#[from]` gives
128 /// a free `impl From<HwDeviceInitFailed> for Error`, so inner
129 /// helpers that return `Result<_, HwDeviceInitFailed>` can be
130 /// `?`-propagated into `Error` directly.
131 #[error(transparent)]
132 HwDeviceInitFailed(#[from] HwDeviceInitFailed),
133
134 /// Auto-probe exhausted every backend in the platform's order. See
135 /// [`AllBackendsFailed`] for the payload details (in particular the
136 /// `unconsumed_packets` history that callers should replay through
137 /// their own software decoder for non-seekable inputs). `#[from]`
138 /// gives a free `impl From<AllBackendsFailed> for Error`.
139 #[error(transparent)]
140 AllBackendsFailed(#[from] AllBackendsFailed),
141
142 /// Surfaced by [`crate::FfmpegVideoStreamDecoder`] when a HW->SW
143 /// fallback attempt itself fails. See [`FallbackFailed`] for the
144 /// payload details (in particular the rescued `unconsumed_packets`
145 /// the HW path had already consumed from the caller). `#[from]`
146 /// gives a free `impl From<FallbackFailed> for Error`.
147 #[error(transparent)]
148 FallbackFailed(#[from] FallbackFailed),
149}
150
151/// Payload for [`Error::HwDeviceInitFailed`].
152///
153/// `av_hwdevice_ctx_create` failed for the requested backend.
154#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
155#[error("hardware device init failed for {backend:?}: {source}")]
156pub struct HwDeviceInitFailed {
157 /// Backend that failed to initialise.
158 backend: Backend,
159 /// Underlying FFmpeg error.
160 source: ffmpeg_next::Error,
161}
162
163impl HwDeviceInitFailed {
164 /// Constructs a new [`HwDeviceInitFailed`] payload.
165 #[inline]
166 pub const fn new(backend: Backend, source: ffmpeg_next::Error) -> Self {
167 Self { backend, source }
168 }
169 /// Backend that failed to initialise.
170 #[inline]
171 pub const fn backend(&self) -> Backend {
172 self.backend
173 }
174 /// Underlying FFmpeg error.
175 #[inline]
176 pub const fn source(&self) -> &ffmpeg_next::Error {
177 &self.source
178 }
179 /// Consume the payload, returning the backend identifier and the
180 /// moved FFmpeg error so callers can take ownership without
181 /// cloning.
182 #[inline]
183 pub fn into_parts(self) -> (Backend, ffmpeg_next::Error) {
184 (self.backend, self.source)
185 }
186}
187
188/// Where in a session's life an [`AllBackendsFailed`] was raised: the
189/// probe, before the first picture, always.
190///
191/// It is the only era a hardware failure is collected in. After the
192/// first picture a failure is that picture's own error, reported as it
193/// was minted, and nothing is collected or replayed — see
194/// [`crate::VideoDecoder`].
195#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant)]
196pub enum FallbackOrigin {
197 /// Before the first picture, while backends are on trial. Every failure
198 /// advances the probe, and when none is left
199 /// [`Error::AllBackendsFailed`] hands back the probe's buffered history
200 /// — possibly empty, when the failure landed on the first packet.
201 /// [`DecodePath::Auto`](crate::DecodePath::Auto) replays that history
202 /// into a software decoder and routes the still-unconsumed current
203 /// packet to it; the other paths report it.
204 Probe,
205}
206
207/// Payload for [`Error::AllBackendsFailed`].
208///
209/// Auto-probe exhausted every backend in the platform's order. Empty
210/// `attempts` means the platform has no hardware backends listed in
211/// [`crate::Backend`] for the current `target_os` — callers must
212/// fall back to a software decoder of their choice.
213///
214/// `unconsumed_packets` holds the packets the decoder accepted from
215/// the caller before the probe exhausted: copies the probe made of the
216/// packets fed via `send_packet`, each payload referenced by its packet
217/// alone, so the caller's own packets and these never share bytes. For
218/// non-seekable inputs (live streams, pipes, network sources) the
219/// caller cannot re-demux from start, so this crate surfaces the
220/// buffered history here so the caller can feed those packets directly
221/// into a software decoder of their choice. When `AllBackendsFailed` comes
222/// from [`crate::VideoDecoder::open`] (no packets were ever sent),
223/// this vec is empty.
224///
225/// `origin` is always [`FallbackOrigin::Probe`]: this is the probe's
226/// exhaustion, before the first picture. After it a hardware failure is
227/// that picture's own error.
228///
229/// `Debug` is hand-written: [`ffmpeg_next::Packet`] does not derive
230/// `Debug`, so we print `[N packets]` instead of dumping per-packet
231/// bytes, which would be both noisy and useless for triage.
232#[derive(thiserror::Error)]
233#[error("all hardware backends failed; attempts: {attempts:?}")]
234pub struct AllBackendsFailed {
235 /// Per-backend errors collected during probing, in the order tried.
236 attempts: Vec<(Backend, Box<Error>)>,
237 /// Packets the decoder consumed from the caller before exhaustion.
238 /// Replay them through a software decoder for non-seekable inputs.
239 unconsumed_packets: Vec<Packet>,
240 /// Where this was raised — the probe, always. See [`FallbackOrigin`].
241 origin: FallbackOrigin,
242}
243
244impl AllBackendsFailed {
245 /// Constructs a probe-era [`AllBackendsFailed`] payload — raised while the
246 /// inner decoder's probe is still active. `unconsumed_packets` is the probe's
247 /// buffered history (possibly empty if the failure landed on the first
248 /// packet). See [`FallbackOrigin::Probe`].
249 ///
250 /// Not `const fn`: the `Vec` arguments may carry destructors and
251 /// the const evaluator can't prove their drop safe for arbitrary
252 /// allocator state.
253 #[inline]
254 pub fn new(attempts: Vec<(Backend, Box<Error>)>, unconsumed_packets: Vec<Packet>) -> Self {
255 Self {
256 attempts,
257 unconsumed_packets,
258 origin: FallbackOrigin::Probe,
259 }
260 }
261 /// Per-backend errors collected during probing, in the order tried.
262 #[inline]
263 pub fn attempts(&self) -> &[(Backend, Box<Error>)] {
264 &self.attempts
265 }
266 /// Where this failure was raised: [`FallbackOrigin::Probe`], always —
267 /// see the type's documentation.
268 #[inline]
269 pub const fn origin(&self) -> FallbackOrigin {
270 self.origin
271 }
272 /// Packets the decoder consumed from the caller before exhaustion.
273 /// Replay them through a software decoder for non-seekable inputs.
274 #[inline]
275 pub fn unconsumed_packets(&self) -> &[Packet] {
276 &self.unconsumed_packets
277 }
278 /// Consume the payload, returning the moved unconsumed packets so
279 /// non-seekable callers can replay them through a software decoder
280 /// without cloning.
281 #[inline]
282 pub fn into_unconsumed_packets(self) -> Vec<Packet> {
283 self.unconsumed_packets
284 }
285 /// Consume the payload, returning the moved attempts log and
286 /// unconsumed packets.
287 #[inline]
288 pub fn into_parts(self) -> (Vec<(Backend, Box<Error>)>, Vec<Packet>) {
289 (self.attempts, self.unconsumed_packets)
290 }
291}
292
293impl std::fmt::Debug for AllBackendsFailed {
294 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 f.debug_struct("AllBackendsFailed")
296 .field("attempts", &self.attempts)
297 // `Packet` is not `Debug`; print just the count so the error is
298 // still useful for triage without dumping per-packet bytes.
299 .field(
300 "unconsumed_packets",
301 &format_args!("[{} packets]", self.unconsumed_packets.len()),
302 )
303 .field("origin", &self.origin)
304 .finish()
305 }
306}
307
308/// Payload for [`Error::FallbackFailed`].
309///
310/// Surfaced by [`crate::FfmpegVideoStreamDecoder`] when a HW->SW
311/// fallback attempt itself fails — e.g. the SW decoder failed to
312/// open, EOF replay returned EAGAIN past the bounded retry, or the
313/// per-frame replay queue exceeded its cap. The HW decoder has
314/// already consumed `unconsumed_packets` from the caller; we
315/// surface them here so non-seekable inputs (pipes, live streams)
316/// can drive their own decoder of last resort.
317///
318/// `Debug` is hand-written for the same reason as
319/// [`AllBackendsFailed`]: [`ffmpeg_next::Packet`] does not derive
320/// `Debug`.
321#[derive(thiserror::Error)]
322#[error("HW->SW fallback failed: {source}")]
323pub struct FallbackFailed {
324 /// Underlying error that aborted the fallback transition.
325 source: Box<Error>,
326 /// Packets that the HW path had consumed but had not yet decoded
327 /// at fallback time. The caller can replay them through a
328 /// software decoder of their choice.
329 unconsumed_packets: Vec<Packet>,
330}
331
332impl FallbackFailed {
333 /// Constructs a new [`FallbackFailed`] payload.
334 ///
335 /// Not `const fn`: the `Vec` argument may carry destructors.
336 #[inline]
337 pub fn new(source: Box<Error>, unconsumed_packets: Vec<Packet>) -> Self {
338 Self {
339 source,
340 unconsumed_packets,
341 }
342 }
343 /// Underlying error that aborted the fallback transition.
344 #[inline]
345 pub fn source(&self) -> &Error {
346 &self.source
347 }
348 /// Packets that the HW path had consumed but had not yet decoded
349 /// at fallback time.
350 #[inline]
351 pub fn unconsumed_packets(&self) -> &[Packet] {
352 &self.unconsumed_packets
353 }
354 /// Consume the payload, returning the moved unconsumed packets so
355 /// non-seekable callers can replay them through a software decoder
356 /// without cloning.
357 #[inline]
358 pub fn into_unconsumed_packets(self) -> Vec<Packet> {
359 self.unconsumed_packets
360 }
361 /// Consume the payload, returning the moved source error and
362 /// unconsumed packets.
363 #[inline]
364 pub fn into_parts(self) -> (Box<Error>, Vec<Packet>) {
365 (self.source, self.unconsumed_packets)
366 }
367}
368
369impl std::fmt::Debug for FallbackFailed {
370 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371 f.debug_struct("FallbackFailed")
372 .field("source", &self.source)
373 .field(
374 "unconsumed_packets",
375 &format_args!("[{} packets]", self.unconsumed_packets.len()),
376 )
377 .finish()
378 }
379}
380
381/// Payload for [`Error::HwTransferTooLarge`].
382///
383/// The CPU-side cost of a hardware->CPU download, priced before it
384/// happens.
385#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
386#[error("hw->cpu transfer would allocate {bytes} bytes for one frame, over a ceiling of {limit}")]
387pub struct HwTransferTooLarge {
388 bytes: usize,
389 limit: usize,
390}
391
392impl HwTransferTooLarge {
393 /// Constructs a `HwTransferTooLarge` payload.
394 #[inline]
395 pub const fn new(bytes: usize, limit: usize) -> Self {
396 Self { bytes, limit }
397 }
398 /// Bytes the destination frame would have cost.
399 #[inline]
400 pub const fn bytes(&self) -> usize {
401 self.bytes
402 }
403 /// The ceiling in force.
404 #[inline]
405 pub const fn limit(&self) -> usize {
406 self.limit
407 }
408}
409
410/// Payload for [`Error::HwSurfaceTooLarge`].
411#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
412#[error(
413 "the hardware surface pool would cost {bytes} bytes, over a ceiling of {limit}; \
414 the hardware format was declined before the pool was built"
415)]
416pub struct HwSurfaceTooLarge {
417 bytes: i64,
418 limit: i64,
419}
420
421impl HwSurfaceTooLarge {
422 /// Constructs a `HwSurfaceTooLarge` payload.
423 #[inline]
424 pub const fn new(bytes: i64, limit: i64) -> Self {
425 Self { bytes, limit }
426 }
427 /// What the pool would have cost, priced through the same
428 /// allocator-parity footprint every other judge uses.
429 #[inline]
430 pub const fn bytes(&self) -> i64 {
431 self.bytes
432 }
433 /// The ceiling in force.
434 #[inline]
435 pub const fn limit(&self) -> i64 {
436 self.limit
437 }
438}
439
440/// Which kind of frame a [`FrameBudgetExceeded`] refers to.
441#[derive(Debug, Clone, Copy, PartialEq, Eq)]
442pub enum FrameMedium {
443 /// A picture.
444 Video,
445 /// An audio frame.
446 Audio,
447}
448
449impl core::fmt::Display for FrameMedium {
450 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
451 match self {
452 Self::Video => f.write_str("picture"),
453 Self::Audio => f.write_str("audio frame"),
454 }
455 }
456}
457
458/// Payload for [`Error::FrameBudgetExceeded`].
459///
460/// The allocator judge refused a frame whose real cost — priced through
461/// the allocator-parity footprint, before `avcodec_default_get_buffer2`
462/// ran — exceeds the caller's ceiling.
463///
464/// # Why this has a name
465///
466/// A `get_buffer2` callback can only answer libavcodec with an errno,
467/// and `AVERROR(EINVAL)` is what libavcodec itself reports for corrupt
468/// input. Without a name, a caller could not tell "this file is broken"
469/// from "your budget refused this frame" — and only one of those is
470/// worth retrying with a larger ceiling.
471#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
472#[error("the {medium} would allocate {bytes} bytes, over a ceiling of {limit}")]
473pub struct FrameBudgetExceeded {
474 bytes: u64,
475 limit: u64,
476 medium: FrameMedium,
477}
478
479impl FrameBudgetExceeded {
480 /// Constructs a `FrameBudgetExceeded` payload.
481 #[inline]
482 pub const fn new(bytes: u64, limit: u64, medium: FrameMedium) -> Self {
483 Self {
484 bytes,
485 limit,
486 medium,
487 }
488 }
489 /// What the frame would have cost.
490 #[inline]
491 pub const fn bytes(&self) -> u64 {
492 self.bytes
493 }
494 /// The ceiling in force.
495 #[inline]
496 pub const fn limit(&self) -> u64 {
497 self.limit
498 }
499 /// Whether the frame was a picture or audio.
500 #[inline]
501 pub const fn medium(&self) -> FrameMedium {
502 self.medium
503 }
504}