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