Skip to main content

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, Clone, 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  /// `avcodec_find_decoder` returned null for the input codec id. The id
57  /// is reported as the raw integer (`AVCodecID` discriminant) — we do not
58  /// construct the bindgen `AVCodecID` enum from a runtime value, since
59  /// values outside our build's discriminant set would invoke UB.
60  #[error("no decoder for codec id {0}")]
61  NoCodec(u32),
62
63  /// The CPU frame a hardware->CPU transfer would allocate is larger
64  /// than [`FrameLimits::max_frame_bytes`](crate::FrameLimits::max_frame_bytes).
65  ///
66  /// The hardware road's own seat. `judge_buffer` — the allocator hook
67  /// that applies the byte ceiling to aligned dimensions — is **not** a
68  /// universal choke point: `ff_get_buffer` calls `hwaccel->alloc_frame`
69  /// directly for VideoToolbox h264/hevc/vp9 and never reaches
70  /// `get_buffer2` at all, and `av_hwframe_transfer_data` allocates its
71  /// CPU destination outside both. This is the seat for that second
72  /// road, judged before the transfer rather than after it.
73  #[error(transparent)]
74  HwTransferTooLarge(#[from] HwTransferTooLarge),
75
76  /// A frame's allocation would have cost more than
77  /// [`FrameLimits::max_frame_bytes`](crate::FrameLimits::max_frame_bytes),
78  /// so it was refused in the allocator, before the allocation.
79  #[error(transparent)]
80  FrameBudgetExceeded(#[from] FrameBudgetExceeded),
81
82  /// The stream's **coded** surface is over the frame ceiling, so the
83  /// hardware format was declined before its pool could be built.
84  ///
85  /// The two dimension vocabularies: `max_pixels` is applied by
86  /// `ff_set_dimensions` to a stream's *display* dims, and a cropped
87  /// stream can display 32x32 out of a 1920x1088 coded surface. What
88  /// gets allocated is the coded figure, so it is the one judged here —
89  /// and it is judged in **bytes**, priced through the allocator-parity
90  /// footprint against the caller's `max_frame_bytes`, because
91  /// `max_pixels` carries the caller's logical pixel limit and nothing
92  /// about cost.
93  #[error(transparent)]
94  HwSurfaceTooLarge(#[from] HwSurfaceTooLarge),
95
96  /// The codec does not advertise a hardware configuration matching the
97  /// requested backend (via `avcodec_get_hw_config`).
98  #[error("codec does not support backend {0:?}")]
99  BackendUnsupportedByCodec(Backend),
100
101  /// `av_hwdevice_ctx_create` failed for the requested backend. See
102  /// [`HwDeviceInitFailed`] for the payload details. `#[from]` gives
103  /// a free `impl From<HwDeviceInitFailed> for Error`, so inner
104  /// helpers that return `Result<_, HwDeviceInitFailed>` can be
105  /// `?`-propagated into `Error` directly.
106  #[error(transparent)]
107  HwDeviceInitFailed(#[from] HwDeviceInitFailed),
108
109  /// Auto-probe exhausted every backend in the platform's order. See
110  /// [`AllBackendsFailed`] for the payload details (in particular the
111  /// `unconsumed_packets` history that callers should replay through
112  /// their own software decoder for non-seekable inputs). `#[from]`
113  /// gives a free `impl From<AllBackendsFailed> for Error`.
114  #[error(transparent)]
115  AllBackendsFailed(#[from] AllBackendsFailed),
116
117  /// Surfaced by [`crate::FfmpegVideoStreamDecoder`] when a HW->SW
118  /// fallback attempt itself fails. See [`FallbackFailed`] for the
119  /// payload details (in particular the rescued `unconsumed_packets`
120  /// the HW path had already consumed from the caller). `#[from]`
121  /// gives a free `impl From<FallbackFailed> for Error`.
122  #[error(transparent)]
123  FallbackFailed(#[from] FallbackFailed),
124}
125
126/// Payload for [`Error::HwDeviceInitFailed`].
127///
128/// `av_hwdevice_ctx_create` failed for the requested backend.
129#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
130#[error("hardware device init failed for {backend:?}: {source}")]
131pub struct HwDeviceInitFailed {
132  /// Backend that failed to initialise.
133  backend: Backend,
134  /// Underlying FFmpeg error.
135  source: ffmpeg_next::Error,
136}
137
138impl HwDeviceInitFailed {
139  /// Constructs a new [`HwDeviceInitFailed`] payload.
140  #[inline]
141  pub const fn new(backend: Backend, source: ffmpeg_next::Error) -> Self {
142    Self { backend, source }
143  }
144  /// Backend that failed to initialise.
145  #[inline]
146  pub const fn backend(&self) -> Backend {
147    self.backend
148  }
149  /// Underlying FFmpeg error.
150  #[inline]
151  pub const fn source(&self) -> &ffmpeg_next::Error {
152    &self.source
153  }
154  /// Consume the payload, returning the backend identifier and the
155  /// moved FFmpeg error so callers can take ownership without
156  /// cloning.
157  #[inline]
158  pub fn into_parts(self) -> (Backend, ffmpeg_next::Error) {
159    (self.backend, self.source)
160  }
161}
162
163/// Where in the decoder's life a [`AllBackendsFailed`] was raised.
164///
165/// The [`crate::FfmpegVideoStreamDecoder`] wrapper routes its software-fallback
166/// replay on **this explicit signal** rather than inferring origin from whether
167/// `unconsumed_packets` is empty. Both origins can carry an empty
168/// `unconsumed_packets` — a probe-era failure on the *first* packet (a
169/// side-data / byte / packet cap trip, or an `av_packet_ref` ENOMEM) has no
170/// prior history to surface, exactly like every post-commit failure — so
171/// emptiness cannot disambiguate them. Conflating the two made the wrapper
172/// treat a probe-era first-packet cap trip as post-commit: it would append a
173/// clone of the borrowed current packet to an empty replay set and skip the
174/// post-fallback `send_packet`, silently dropping that packet if the clone
175/// failed.
176#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant)]
177pub enum FallbackOrigin {
178  /// Raised while the inner decoder's probe was still active (before the first
179  /// frame). `unconsumed_packets` is the probe's buffered history (possibly
180  /// empty when the failure landed on the very first packet). The wrapper
181  /// replays that history and then routes the still-unconsumed current packet
182  /// to the new software decoder itself.
183  Probe,
184  /// Raised after the probe collapsed (the committed backend failed at
185  /// runtime). `unconsumed_packets` is always empty — the probe buffer is gone
186  /// — so the wrapper does not replay: it opens a software decoder cold,
187  /// forwards only the failing call's current packet (or EOF), and resyncs at
188  /// the next keyframe, accepting a bounded, logged gap (degrade-and-continue).
189  PostCommit,
190}
191
192/// Payload for [`Error::AllBackendsFailed`].
193///
194/// Auto-probe exhausted every backend in the platform's order. Empty
195/// `attempts` means the platform has no hardware backends listed in
196/// [`crate::Backend`] for the current `target_os` — callers must
197/// fall back to a software decoder of their choice.
198///
199/// `unconsumed_packets` holds the packets the decoder accepted from
200/// the caller before the probe exhausted (refcounted shallow clones
201/// of the packets fed via `send_packet`). For non-seekable inputs
202/// (live streams, pipes, network sources) the caller cannot
203/// re-demux from start, so this crate surfaces the buffered history
204/// here so the caller can feed those packets directly into a
205/// software decoder of their choice. When `AllBackendsFailed` comes
206/// from [`crate::VideoDecoder::open`] (no packets were ever sent),
207/// this vec is empty.
208///
209/// `origin` records whether the failure happened during the probe or after the
210/// committed backend collapsed at runtime — the explicit signal the wrapper
211/// routes on (see [`FallbackOrigin`]). It is never inferred from
212/// `unconsumed_packets.is_empty()`, which both origins can satisfy.
213///
214/// `Debug` is hand-written: [`ffmpeg_next::Packet`] does not derive
215/// `Debug`, so we print `[N packets]` instead of dumping per-packet
216/// bytes, which would be both noisy and useless for triage.
217#[derive(Clone, thiserror::Error)]
218#[error("all hardware backends failed; attempts: {attempts:?}")]
219pub struct AllBackendsFailed {
220  /// Per-backend errors collected during probing, in the order tried.
221  attempts: Vec<(Backend, Box<Error>)>,
222  /// Packets the decoder consumed from the caller before exhaustion.
223  /// Replay them through a software decoder for non-seekable inputs.
224  unconsumed_packets: Vec<Packet>,
225  /// Whether this was raised during the probe or post-commit. The wrapper's
226  /// fallback replay routes on this, never on `unconsumed_packets` emptiness.
227  origin: FallbackOrigin,
228}
229
230impl AllBackendsFailed {
231  /// Constructs a probe-era [`AllBackendsFailed`] payload — raised while the
232  /// inner decoder's probe is still active. `unconsumed_packets` is the probe's
233  /// buffered history (possibly empty if the failure landed on the first
234  /// packet). See [`FallbackOrigin::Probe`].
235  ///
236  /// Not `const fn`: the `Vec` arguments may carry destructors and
237  /// the const evaluator can't prove their drop safe for arbitrary
238  /// allocator state.
239  #[inline]
240  pub fn new(attempts: Vec<(Backend, Box<Error>)>, unconsumed_packets: Vec<Packet>) -> Self {
241    Self {
242      attempts,
243      unconsumed_packets,
244      origin: FallbackOrigin::Probe,
245    }
246  }
247  /// Constructs a post-commit [`AllBackendsFailed`] payload — raised after the
248  /// probe collapsed, when the committed backend failed at runtime.
249  /// `unconsumed_packets` is always empty (the probe buffer is gone); the
250  /// wrapper's retained GOP window supplies the replay set. See
251  /// [`FallbackOrigin::PostCommit`].
252  #[inline]
253  pub fn new_post_commit(attempts: Vec<(Backend, Box<Error>)>) -> Self {
254    Self {
255      attempts,
256      unconsumed_packets: Vec::new(),
257      origin: FallbackOrigin::PostCommit,
258    }
259  }
260  /// Per-backend errors collected during probing, in the order tried.
261  #[inline]
262  pub fn attempts(&self) -> &[(Backend, Box<Error>)] {
263    &self.attempts
264  }
265  /// Where this failure was raised — the explicit probe-vs-post-commit signal
266  /// the wrapper routes its fallback replay on.
267  #[inline]
268  pub const fn origin(&self) -> FallbackOrigin {
269    self.origin
270  }
271  /// Packets the decoder consumed from the caller before exhaustion.
272  /// Replay them through a software decoder for non-seekable inputs.
273  #[inline]
274  pub fn unconsumed_packets(&self) -> &[Packet] {
275    &self.unconsumed_packets
276  }
277  /// Consume the payload, returning the moved unconsumed packets so
278  /// non-seekable callers can replay them through a software decoder
279  /// without cloning.
280  #[inline]
281  pub fn into_unconsumed_packets(self) -> Vec<Packet> {
282    self.unconsumed_packets
283  }
284  /// Consume the payload, returning the moved attempts log and
285  /// unconsumed packets.
286  #[inline]
287  pub fn into_parts(self) -> (Vec<(Backend, Box<Error>)>, Vec<Packet>) {
288    (self.attempts, self.unconsumed_packets)
289  }
290}
291
292impl std::fmt::Debug for AllBackendsFailed {
293  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294    f.debug_struct("AllBackendsFailed")
295      .field("attempts", &self.attempts)
296      // `Packet` is not `Debug`; print just the count so the error is
297      // still useful for triage without dumping per-packet bytes.
298      .field(
299        "unconsumed_packets",
300        &format_args!("[{} packets]", self.unconsumed_packets.len()),
301      )
302      .field("origin", &self.origin)
303      .finish()
304  }
305}
306
307/// Payload for [`Error::FallbackFailed`].
308///
309/// Surfaced by [`crate::FfmpegVideoStreamDecoder`] when a HW->SW
310/// fallback attempt itself fails — e.g. the SW decoder failed to
311/// open, EOF replay returned EAGAIN past the bounded retry, or the
312/// per-frame replay queue exceeded its cap. The HW decoder has
313/// already consumed `unconsumed_packets` from the caller; we
314/// surface them here so non-seekable inputs (pipes, live streams)
315/// can drive their own decoder of last resort.
316///
317/// `Debug` is hand-written for the same reason as
318/// [`AllBackendsFailed`]: [`ffmpeg_next::Packet`] does not derive
319/// `Debug`.
320#[derive(Clone, thiserror::Error)]
321#[error("HW->SW fallback failed: {source}")]
322pub struct FallbackFailed {
323  /// Underlying error that aborted the fallback transition.
324  source: Box<Error>,
325  /// Packets that the HW path had consumed but had not yet decoded
326  /// at fallback time. The caller can replay them through a
327  /// software decoder of their choice.
328  unconsumed_packets: Vec<Packet>,
329}
330
331impl FallbackFailed {
332  /// Constructs a new [`FallbackFailed`] payload.
333  ///
334  /// Not `const fn`: the `Vec` argument may carry destructors.
335  #[inline]
336  pub fn new(source: Box<Error>, unconsumed_packets: Vec<Packet>) -> Self {
337    Self {
338      source,
339      unconsumed_packets,
340    }
341  }
342  /// Underlying error that aborted the fallback transition.
343  #[inline]
344  pub fn source(&self) -> &Error {
345    &self.source
346  }
347  /// Packets that the HW path had consumed but had not yet decoded
348  /// at fallback time.
349  #[inline]
350  pub fn unconsumed_packets(&self) -> &[Packet] {
351    &self.unconsumed_packets
352  }
353  /// Consume the payload, returning the moved unconsumed packets so
354  /// non-seekable callers can replay them through a software decoder
355  /// without cloning.
356  #[inline]
357  pub fn into_unconsumed_packets(self) -> Vec<Packet> {
358    self.unconsumed_packets
359  }
360  /// Consume the payload, returning the moved source error and
361  /// unconsumed packets.
362  #[inline]
363  pub fn into_parts(self) -> (Box<Error>, Vec<Packet>) {
364    (self.source, self.unconsumed_packets)
365  }
366}
367
368impl std::fmt::Debug for FallbackFailed {
369  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370    f.debug_struct("FallbackFailed")
371      .field("source", &self.source)
372      .field(
373        "unconsumed_packets",
374        &format_args!("[{} packets]", self.unconsumed_packets.len()),
375      )
376      .finish()
377  }
378}
379
380/// Payload for [`Error::HwTransferTooLarge`].
381///
382/// The CPU-side cost of a hardware->CPU download, priced before it
383/// happens.
384#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
385#[error("hw->cpu transfer would allocate {bytes} bytes for one frame, over a ceiling of {limit}")]
386pub struct HwTransferTooLarge {
387  bytes: usize,
388  limit: usize,
389}
390
391impl HwTransferTooLarge {
392  /// Constructs a `HwTransferTooLarge` payload.
393  #[inline]
394  pub const fn new(bytes: usize, limit: usize) -> Self {
395    Self { bytes, limit }
396  }
397  /// Bytes the destination frame would have cost.
398  #[inline]
399  pub const fn bytes(&self) -> usize {
400    self.bytes
401  }
402  /// The ceiling in force.
403  #[inline]
404  pub const fn limit(&self) -> usize {
405    self.limit
406  }
407}
408
409/// Payload for [`Error::HwSurfaceTooLarge`].
410#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
411#[error(
412  "the hardware surface pool would cost {bytes} bytes, over a ceiling of {limit}; \
413   the hardware format was declined before the pool was built"
414)]
415pub struct HwSurfaceTooLarge {
416  bytes: i64,
417  limit: i64,
418}
419
420impl HwSurfaceTooLarge {
421  /// Constructs a `HwSurfaceTooLarge` payload.
422  #[inline]
423  pub const fn new(bytes: i64, limit: i64) -> Self {
424    Self { bytes, limit }
425  }
426  /// What the pool would have cost, priced through the same
427  /// allocator-parity footprint every other judge uses.
428  #[inline]
429  pub const fn bytes(&self) -> i64 {
430    self.bytes
431  }
432  /// The ceiling in force.
433  #[inline]
434  pub const fn limit(&self) -> i64 {
435    self.limit
436  }
437}
438
439/// Which kind of frame a [`FrameBudgetExceeded`] refers to.
440#[derive(Debug, Clone, Copy, PartialEq, Eq)]
441pub enum FrameMedium {
442  /// A picture.
443  Video,
444  /// An audio frame.
445  Audio,
446}
447
448impl core::fmt::Display for FrameMedium {
449  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
450    match self {
451      Self::Video => f.write_str("picture"),
452      Self::Audio => f.write_str("audio frame"),
453    }
454  }
455}
456
457/// Payload for [`Error::FrameBudgetExceeded`].
458///
459/// The allocator judge refused a frame whose real cost — priced through
460/// the allocator-parity footprint, before `avcodec_default_get_buffer2`
461/// ran — exceeds the caller's ceiling.
462///
463/// # Why this has a name
464///
465/// A `get_buffer2` callback can only answer libavcodec with an errno,
466/// and `AVERROR(EINVAL)` is what libavcodec itself reports for corrupt
467/// input. Without a name, a caller could not tell "this file is broken"
468/// from "your budget refused this frame" — and only one of those is
469/// worth retrying with a larger ceiling.
470#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
471#[error("the {medium} would allocate {bytes} bytes, over a ceiling of {limit}")]
472pub struct FrameBudgetExceeded {
473  bytes: u64,
474  limit: u64,
475  medium: FrameMedium,
476}
477
478impl FrameBudgetExceeded {
479  /// Constructs a `FrameBudgetExceeded` payload.
480  #[inline]
481  pub const fn new(bytes: u64, limit: u64, medium: FrameMedium) -> Self {
482    Self {
483      bytes,
484      limit,
485      medium,
486    }
487  }
488  /// What the frame would have cost.
489  #[inline]
490  pub const fn bytes(&self) -> u64 {
491    self.bytes
492  }
493  /// The ceiling in force.
494  #[inline]
495  pub const fn limit(&self) -> u64 {
496    self.limit
497  }
498  /// Whether the frame was a picture or audio.
499  #[inline]
500  pub const fn medium(&self) -> FrameMedium {
501    self.medium
502  }
503}