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, 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 the decoder's life a [`AllBackendsFailed`] was raised.
189///
190/// The [`crate::FfmpegVideoStreamDecoder`] wrapper routes its software-fallback
191/// replay on **this explicit signal** rather than inferring origin from whether
192/// `unconsumed_packets` is empty. Both origins can carry an empty
193/// `unconsumed_packets` — a probe-era failure on the *first* packet (a
194/// side-data / byte / packet cap trip, or an `av_packet_ref` ENOMEM) has no
195/// prior history to surface, exactly like every post-commit failure — so
196/// emptiness cannot disambiguate them. Conflating the two made the wrapper
197/// treat a probe-era first-packet cap trip as post-commit: it would append a
198/// clone of the borrowed current packet to an empty replay set and skip the
199/// post-fallback `send_packet`, silently dropping that packet if the clone
200/// failed.
201#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant)]
202pub enum FallbackOrigin {
203  /// Raised while the inner decoder's probe was still active (before the first
204  /// frame). `unconsumed_packets` is the probe's buffered history (possibly
205  /// empty when the failure landed on the very first packet). The wrapper
206  /// replays that history and then routes the still-unconsumed current packet
207  /// to the new software decoder itself.
208  Probe,
209  /// Raised after the probe collapsed (the committed backend failed at
210  /// runtime). `unconsumed_packets` is always empty — the probe buffer is gone
211  /// — so the wrapper does not replay: it opens a software decoder cold,
212  /// forwards only the failing call's current packet (or EOF), and resyncs at
213  /// the next keyframe, accepting a bounded, logged gap (degrade-and-continue).
214  PostCommit,
215}
216
217/// Payload for [`Error::AllBackendsFailed`].
218///
219/// Auto-probe exhausted every backend in the platform's order. Empty
220/// `attempts` means the platform has no hardware backends listed in
221/// [`crate::Backend`] for the current `target_os` — callers must
222/// fall back to a software decoder of their choice.
223///
224/// `unconsumed_packets` holds the packets the decoder accepted from
225/// the caller before the probe exhausted (refcounted shallow clones
226/// of the packets fed via `send_packet`). For non-seekable inputs
227/// (live streams, pipes, network sources) the caller cannot
228/// re-demux from start, so this crate surfaces the buffered history
229/// here so the caller can feed those packets directly into a
230/// software decoder of their choice. When `AllBackendsFailed` comes
231/// from [`crate::VideoDecoder::open`] (no packets were ever sent),
232/// this vec is empty.
233///
234/// `origin` records whether the failure happened during the probe or after the
235/// committed backend collapsed at runtime — the explicit signal the wrapper
236/// routes on (see [`FallbackOrigin`]). It is never inferred from
237/// `unconsumed_packets.is_empty()`, which both origins can satisfy.
238///
239/// `Debug` is hand-written: [`ffmpeg_next::Packet`] does not derive
240/// `Debug`, so we print `[N packets]` instead of dumping per-packet
241/// bytes, which would be both noisy and useless for triage.
242#[derive(thiserror::Error)]
243#[error("all hardware backends failed; attempts: {attempts:?}")]
244pub struct AllBackendsFailed {
245  /// Per-backend errors collected during probing, in the order tried.
246  attempts: Vec<(Backend, Box<Error>)>,
247  /// Packets the decoder consumed from the caller before exhaustion.
248  /// Replay them through a software decoder for non-seekable inputs.
249  unconsumed_packets: Vec<Packet>,
250  /// Whether this was raised during the probe or post-commit. The wrapper's
251  /// fallback replay routes on this, never on `unconsumed_packets` emptiness.
252  origin: FallbackOrigin,
253}
254
255impl AllBackendsFailed {
256  /// Constructs a probe-era [`AllBackendsFailed`] payload — raised while the
257  /// inner decoder's probe is still active. `unconsumed_packets` is the probe's
258  /// buffered history (possibly empty if the failure landed on the first
259  /// packet). See [`FallbackOrigin::Probe`].
260  ///
261  /// Not `const fn`: the `Vec` arguments may carry destructors and
262  /// the const evaluator can't prove their drop safe for arbitrary
263  /// allocator state.
264  #[inline]
265  pub fn new(attempts: Vec<(Backend, Box<Error>)>, unconsumed_packets: Vec<Packet>) -> Self {
266    Self {
267      attempts,
268      unconsumed_packets,
269      origin: FallbackOrigin::Probe,
270    }
271  }
272  /// Constructs a post-commit [`AllBackendsFailed`] payload — raised after the
273  /// probe collapsed, when the committed backend failed at runtime.
274  /// `unconsumed_packets` is always empty (the probe buffer is gone); the
275  /// wrapper's retained GOP window supplies the replay set. See
276  /// [`FallbackOrigin::PostCommit`].
277  #[inline]
278  pub fn new_post_commit(attempts: Vec<(Backend, Box<Error>)>) -> Self {
279    Self {
280      attempts,
281      unconsumed_packets: Vec::new(),
282      origin: FallbackOrigin::PostCommit,
283    }
284  }
285  /// Per-backend errors collected during probing, in the order tried.
286  #[inline]
287  pub fn attempts(&self) -> &[(Backend, Box<Error>)] {
288    &self.attempts
289  }
290  /// Where this failure was raised — the explicit probe-vs-post-commit signal
291  /// the wrapper routes its fallback replay on.
292  #[inline]
293  pub const fn origin(&self) -> FallbackOrigin {
294    self.origin
295  }
296  /// Packets the decoder consumed from the caller before exhaustion.
297  /// Replay them through a software decoder for non-seekable inputs.
298  #[inline]
299  pub fn unconsumed_packets(&self) -> &[Packet] {
300    &self.unconsumed_packets
301  }
302  /// Consume the payload, returning the moved unconsumed packets so
303  /// non-seekable callers can replay them through a software decoder
304  /// without cloning.
305  #[inline]
306  pub fn into_unconsumed_packets(self) -> Vec<Packet> {
307    self.unconsumed_packets
308  }
309  /// Consume the payload, returning the moved attempts log and
310  /// unconsumed packets.
311  #[inline]
312  pub fn into_parts(self) -> (Vec<(Backend, Box<Error>)>, Vec<Packet>) {
313    (self.attempts, self.unconsumed_packets)
314  }
315}
316
317impl std::fmt::Debug for AllBackendsFailed {
318  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319    f.debug_struct("AllBackendsFailed")
320      .field("attempts", &self.attempts)
321      // `Packet` is not `Debug`; print just the count so the error is
322      // still useful for triage without dumping per-packet bytes.
323      .field(
324        "unconsumed_packets",
325        &format_args!("[{} packets]", self.unconsumed_packets.len()),
326      )
327      .field("origin", &self.origin)
328      .finish()
329  }
330}
331
332/// Payload for [`Error::FallbackFailed`].
333///
334/// Surfaced by [`crate::FfmpegVideoStreamDecoder`] when a HW->SW
335/// fallback attempt itself fails — e.g. the SW decoder failed to
336/// open, EOF replay returned EAGAIN past the bounded retry, or the
337/// per-frame replay queue exceeded its cap. The HW decoder has
338/// already consumed `unconsumed_packets` from the caller; we
339/// surface them here so non-seekable inputs (pipes, live streams)
340/// can drive their own decoder of last resort.
341///
342/// `Debug` is hand-written for the same reason as
343/// [`AllBackendsFailed`]: [`ffmpeg_next::Packet`] does not derive
344/// `Debug`.
345#[derive(thiserror::Error)]
346#[error("HW->SW fallback failed: {source}")]
347pub struct FallbackFailed {
348  /// Underlying error that aborted the fallback transition.
349  source: Box<Error>,
350  /// Packets that the HW path had consumed but had not yet decoded
351  /// at fallback time. The caller can replay them through a
352  /// software decoder of their choice.
353  unconsumed_packets: Vec<Packet>,
354}
355
356impl FallbackFailed {
357  /// Constructs a new [`FallbackFailed`] payload.
358  ///
359  /// Not `const fn`: the `Vec` argument may carry destructors.
360  #[inline]
361  pub fn new(source: Box<Error>, unconsumed_packets: Vec<Packet>) -> Self {
362    Self {
363      source,
364      unconsumed_packets,
365    }
366  }
367  /// Underlying error that aborted the fallback transition.
368  #[inline]
369  pub fn source(&self) -> &Error {
370    &self.source
371  }
372  /// Packets that the HW path had consumed but had not yet decoded
373  /// at fallback time.
374  #[inline]
375  pub fn unconsumed_packets(&self) -> &[Packet] {
376    &self.unconsumed_packets
377  }
378  /// Consume the payload, returning the moved unconsumed packets so
379  /// non-seekable callers can replay them through a software decoder
380  /// without cloning.
381  #[inline]
382  pub fn into_unconsumed_packets(self) -> Vec<Packet> {
383    self.unconsumed_packets
384  }
385  /// Consume the payload, returning the moved source error and
386  /// unconsumed packets.
387  #[inline]
388  pub fn into_parts(self) -> (Box<Error>, Vec<Packet>) {
389    (self.source, self.unconsumed_packets)
390  }
391}
392
393impl std::fmt::Debug for FallbackFailed {
394  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395    f.debug_struct("FallbackFailed")
396      .field("source", &self.source)
397      .field(
398        "unconsumed_packets",
399        &format_args!("[{} packets]", self.unconsumed_packets.len()),
400      )
401      .finish()
402  }
403}
404
405/// Payload for [`Error::HwTransferTooLarge`].
406///
407/// The CPU-side cost of a hardware->CPU download, priced before it
408/// happens.
409#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
410#[error("hw->cpu transfer would allocate {bytes} bytes for one frame, over a ceiling of {limit}")]
411pub struct HwTransferTooLarge {
412  bytes: usize,
413  limit: usize,
414}
415
416impl HwTransferTooLarge {
417  /// Constructs a `HwTransferTooLarge` payload.
418  #[inline]
419  pub const fn new(bytes: usize, limit: usize) -> Self {
420    Self { bytes, limit }
421  }
422  /// Bytes the destination frame would have cost.
423  #[inline]
424  pub const fn bytes(&self) -> usize {
425    self.bytes
426  }
427  /// The ceiling in force.
428  #[inline]
429  pub const fn limit(&self) -> usize {
430    self.limit
431  }
432}
433
434/// Payload for [`Error::HwSurfaceTooLarge`].
435#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
436#[error(
437  "the hardware surface pool would cost {bytes} bytes, over a ceiling of {limit}; \
438   the hardware format was declined before the pool was built"
439)]
440pub struct HwSurfaceTooLarge {
441  bytes: i64,
442  limit: i64,
443}
444
445impl HwSurfaceTooLarge {
446  /// Constructs a `HwSurfaceTooLarge` payload.
447  #[inline]
448  pub const fn new(bytes: i64, limit: i64) -> Self {
449    Self { bytes, limit }
450  }
451  /// What the pool would have cost, priced through the same
452  /// allocator-parity footprint every other judge uses.
453  #[inline]
454  pub const fn bytes(&self) -> i64 {
455    self.bytes
456  }
457  /// The ceiling in force.
458  #[inline]
459  pub const fn limit(&self) -> i64 {
460    self.limit
461  }
462}
463
464/// Which kind of frame a [`FrameBudgetExceeded`] refers to.
465#[derive(Debug, Clone, Copy, PartialEq, Eq)]
466pub enum FrameMedium {
467  /// A picture.
468  Video,
469  /// An audio frame.
470  Audio,
471}
472
473impl core::fmt::Display for FrameMedium {
474  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
475    match self {
476      Self::Video => f.write_str("picture"),
477      Self::Audio => f.write_str("audio frame"),
478    }
479  }
480}
481
482/// Payload for [`Error::FrameBudgetExceeded`].
483///
484/// The allocator judge refused a frame whose real cost — priced through
485/// the allocator-parity footprint, before `avcodec_default_get_buffer2`
486/// ran — exceeds the caller's ceiling.
487///
488/// # Why this has a name
489///
490/// A `get_buffer2` callback can only answer libavcodec with an errno,
491/// and `AVERROR(EINVAL)` is what libavcodec itself reports for corrupt
492/// input. Without a name, a caller could not tell "this file is broken"
493/// from "your budget refused this frame" — and only one of those is
494/// worth retrying with a larger ceiling.
495#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
496#[error("the {medium} would allocate {bytes} bytes, over a ceiling of {limit}")]
497pub struct FrameBudgetExceeded {
498  bytes: u64,
499  limit: u64,
500  medium: FrameMedium,
501}
502
503impl FrameBudgetExceeded {
504  /// Constructs a `FrameBudgetExceeded` payload.
505  #[inline]
506  pub const fn new(bytes: u64, limit: u64, medium: FrameMedium) -> Self {
507    Self {
508      bytes,
509      limit,
510      medium,
511    }
512  }
513  /// What the frame would have cost.
514  #[inline]
515  pub const fn bytes(&self) -> u64 {
516    self.bytes
517  }
518  /// The ceiling in force.
519  #[inline]
520  pub const fn limit(&self) -> u64 {
521    self.limit
522  }
523  /// Whether the frame was a picture or audio.
524  #[inline]
525  pub const fn medium(&self) -> FrameMedium {
526    self.medium
527  }
528}