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  /// `avcodec_find_decoder` returned null for the input codec id. The id
34  /// is reported as the raw integer (`AVCodecID` discriminant) — we do not
35  /// construct the bindgen `AVCodecID` enum from a runtime value, since
36  /// values outside our build's discriminant set would invoke UB.
37  #[error("no decoder for codec id {0}")]
38  NoCodec(u32),
39
40  /// The codec does not advertise a hardware configuration matching the
41  /// requested backend (via `avcodec_get_hw_config`).
42  #[error("codec does not support backend {0:?}")]
43  BackendUnsupportedByCodec(Backend),
44
45  /// `av_hwdevice_ctx_create` failed for the requested backend. See
46  /// [`HwDeviceInitFailed`] for the payload details. `#[from]` gives
47  /// a free `impl From<HwDeviceInitFailed> for Error`, so inner
48  /// helpers that return `Result<_, HwDeviceInitFailed>` can be
49  /// `?`-propagated into `Error` directly.
50  #[error(transparent)]
51  HwDeviceInitFailed(#[from] HwDeviceInitFailed),
52
53  /// Auto-probe exhausted every backend in the platform's order. See
54  /// [`AllBackendsFailed`] for the payload details (in particular the
55  /// `unconsumed_packets` history that callers should replay through
56  /// their own software decoder for non-seekable inputs). `#[from]`
57  /// gives a free `impl From<AllBackendsFailed> for Error`.
58  #[error(transparent)]
59  AllBackendsFailed(#[from] AllBackendsFailed),
60
61  /// Surfaced by [`crate::FfmpegVideoStreamDecoder`] when a HW->SW
62  /// fallback attempt itself fails. See [`FallbackFailed`] for the
63  /// payload details (in particular the rescued `unconsumed_packets`
64  /// the HW path had already consumed from the caller). `#[from]`
65  /// gives a free `impl From<FallbackFailed> for Error`.
66  #[error(transparent)]
67  FallbackFailed(#[from] FallbackFailed),
68}
69
70/// Payload for [`Error::HwDeviceInitFailed`].
71///
72/// `av_hwdevice_ctx_create` failed for the requested backend.
73#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
74#[error("hardware device init failed for {backend:?}: {source}")]
75pub struct HwDeviceInitFailed {
76  /// Backend that failed to initialise.
77  backend: Backend,
78  /// Underlying FFmpeg error.
79  source: ffmpeg_next::Error,
80}
81
82impl HwDeviceInitFailed {
83  /// Constructs a new [`HwDeviceInitFailed`] payload.
84  #[inline]
85  pub const fn new(backend: Backend, source: ffmpeg_next::Error) -> Self {
86    Self { backend, source }
87  }
88  /// Backend that failed to initialise.
89  #[inline]
90  pub const fn backend(&self) -> Backend {
91    self.backend
92  }
93  /// Underlying FFmpeg error.
94  #[inline]
95  pub const fn source(&self) -> &ffmpeg_next::Error {
96    &self.source
97  }
98  /// Consume the payload, returning the backend identifier and the
99  /// moved FFmpeg error so callers can take ownership without
100  /// cloning.
101  #[inline]
102  pub fn into_parts(self) -> (Backend, ffmpeg_next::Error) {
103    (self.backend, self.source)
104  }
105}
106
107/// Where in the decoder's life a [`AllBackendsFailed`] was raised.
108///
109/// The [`crate::FfmpegVideoStreamDecoder`] wrapper routes its software-fallback
110/// replay on **this explicit signal** rather than inferring origin from whether
111/// `unconsumed_packets` is empty. Both origins can carry an empty
112/// `unconsumed_packets` — a probe-era failure on the *first* packet (a
113/// side-data / byte / packet cap trip, or an `av_packet_ref` ENOMEM) has no
114/// prior history to surface, exactly like every post-commit failure — so
115/// emptiness cannot disambiguate them. Conflating the two made the wrapper
116/// treat a probe-era first-packet cap trip as post-commit: it would append a
117/// clone of the borrowed current packet to an empty replay set and skip the
118/// post-fallback `send_packet`, silently dropping that packet if the clone
119/// failed.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant)]
121pub enum FallbackOrigin {
122  /// Raised while the inner decoder's probe was still active (before the first
123  /// frame). `unconsumed_packets` is the probe's buffered history (possibly
124  /// empty when the failure landed on the very first packet). The wrapper
125  /// replays that history and then routes the still-unconsumed current packet
126  /// to the new software decoder itself.
127  Probe,
128  /// Raised after the probe collapsed (the committed backend failed at
129  /// runtime). `unconsumed_packets` is always empty — the probe buffer is gone
130  /// — so the wrapper does not replay: it opens a software decoder cold,
131  /// forwards only the failing call's current packet (or EOF), and resyncs at
132  /// the next keyframe, accepting a bounded, logged gap (degrade-and-continue).
133  PostCommit,
134}
135
136/// Payload for [`Error::AllBackendsFailed`].
137///
138/// Auto-probe exhausted every backend in the platform's order. Empty
139/// `attempts` means the platform has no hardware backends listed in
140/// [`crate::Backend`] for the current `target_os` — callers must
141/// fall back to a software decoder of their choice.
142///
143/// `unconsumed_packets` holds the packets the decoder accepted from
144/// the caller before the probe exhausted (refcounted shallow clones
145/// of the packets fed via `send_packet`). For non-seekable inputs
146/// (live streams, pipes, network sources) the caller cannot
147/// re-demux from start, so this crate surfaces the buffered history
148/// here so the caller can feed those packets directly into a
149/// software decoder of their choice. When `AllBackendsFailed` comes
150/// from [`crate::VideoDecoder::open`] (no packets were ever sent),
151/// this vec is empty.
152///
153/// `origin` records whether the failure happened during the probe or after the
154/// committed backend collapsed at runtime — the explicit signal the wrapper
155/// routes on (see [`FallbackOrigin`]). It is never inferred from
156/// `unconsumed_packets.is_empty()`, which both origins can satisfy.
157///
158/// `Debug` is hand-written: [`ffmpeg_next::Packet`] does not derive
159/// `Debug`, so we print `[N packets]` instead of dumping per-packet
160/// bytes, which would be both noisy and useless for triage.
161#[derive(Clone, thiserror::Error)]
162#[error("all hardware backends failed; attempts: {attempts:?}")]
163pub struct AllBackendsFailed {
164  /// Per-backend errors collected during probing, in the order tried.
165  attempts: Vec<(Backend, Box<Error>)>,
166  /// Packets the decoder consumed from the caller before exhaustion.
167  /// Replay them through a software decoder for non-seekable inputs.
168  unconsumed_packets: Vec<Packet>,
169  /// Whether this was raised during the probe or post-commit. The wrapper's
170  /// fallback replay routes on this, never on `unconsumed_packets` emptiness.
171  origin: FallbackOrigin,
172}
173
174impl AllBackendsFailed {
175  /// Constructs a probe-era [`AllBackendsFailed`] payload — raised while the
176  /// inner decoder's probe is still active. `unconsumed_packets` is the probe's
177  /// buffered history (possibly empty if the failure landed on the first
178  /// packet). See [`FallbackOrigin::Probe`].
179  ///
180  /// Not `const fn`: the `Vec` arguments may carry destructors and
181  /// the const evaluator can't prove their drop safe for arbitrary
182  /// allocator state.
183  #[inline]
184  pub fn new(attempts: Vec<(Backend, Box<Error>)>, unconsumed_packets: Vec<Packet>) -> Self {
185    Self {
186      attempts,
187      unconsumed_packets,
188      origin: FallbackOrigin::Probe,
189    }
190  }
191  /// Constructs a post-commit [`AllBackendsFailed`] payload — raised after the
192  /// probe collapsed, when the committed backend failed at runtime.
193  /// `unconsumed_packets` is always empty (the probe buffer is gone); the
194  /// wrapper's retained GOP window supplies the replay set. See
195  /// [`FallbackOrigin::PostCommit`].
196  #[inline]
197  pub fn new_post_commit(attempts: Vec<(Backend, Box<Error>)>) -> Self {
198    Self {
199      attempts,
200      unconsumed_packets: Vec::new(),
201      origin: FallbackOrigin::PostCommit,
202    }
203  }
204  /// Per-backend errors collected during probing, in the order tried.
205  #[inline]
206  pub fn attempts(&self) -> &[(Backend, Box<Error>)] {
207    &self.attempts
208  }
209  /// Where this failure was raised — the explicit probe-vs-post-commit signal
210  /// the wrapper routes its fallback replay on.
211  #[inline]
212  pub const fn origin(&self) -> FallbackOrigin {
213    self.origin
214  }
215  /// Packets the decoder consumed from the caller before exhaustion.
216  /// Replay them through a software decoder for non-seekable inputs.
217  #[inline]
218  pub fn unconsumed_packets(&self) -> &[Packet] {
219    &self.unconsumed_packets
220  }
221  /// Consume the payload, returning the moved unconsumed packets so
222  /// non-seekable callers can replay them through a software decoder
223  /// without cloning.
224  #[inline]
225  pub fn into_unconsumed_packets(self) -> Vec<Packet> {
226    self.unconsumed_packets
227  }
228  /// Consume the payload, returning the moved attempts log and
229  /// unconsumed packets.
230  #[inline]
231  pub fn into_parts(self) -> (Vec<(Backend, Box<Error>)>, Vec<Packet>) {
232    (self.attempts, self.unconsumed_packets)
233  }
234}
235
236impl std::fmt::Debug for AllBackendsFailed {
237  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238    f.debug_struct("AllBackendsFailed")
239      .field("attempts", &self.attempts)
240      // `Packet` is not `Debug`; print just the count so the error is
241      // still useful for triage without dumping per-packet bytes.
242      .field(
243        "unconsumed_packets",
244        &format_args!("[{} packets]", self.unconsumed_packets.len()),
245      )
246      .field("origin", &self.origin)
247      .finish()
248  }
249}
250
251/// Payload for [`Error::FallbackFailed`].
252///
253/// Surfaced by [`crate::FfmpegVideoStreamDecoder`] when a HW->SW
254/// fallback attempt itself fails — e.g. the SW decoder failed to
255/// open, EOF replay returned EAGAIN past the bounded retry, or the
256/// per-frame replay queue exceeded its cap. The HW decoder has
257/// already consumed `unconsumed_packets` from the caller; we
258/// surface them here so non-seekable inputs (pipes, live streams)
259/// can drive their own decoder of last resort.
260///
261/// `Debug` is hand-written for the same reason as
262/// [`AllBackendsFailed`]: [`ffmpeg_next::Packet`] does not derive
263/// `Debug`.
264#[derive(Clone, thiserror::Error)]
265#[error("HW->SW fallback failed: {source}")]
266pub struct FallbackFailed {
267  /// Underlying error that aborted the fallback transition.
268  source: Box<Error>,
269  /// Packets that the HW path had consumed but had not yet decoded
270  /// at fallback time. The caller can replay them through a
271  /// software decoder of their choice.
272  unconsumed_packets: Vec<Packet>,
273}
274
275impl FallbackFailed {
276  /// Constructs a new [`FallbackFailed`] payload.
277  ///
278  /// Not `const fn`: the `Vec` argument may carry destructors.
279  #[inline]
280  pub fn new(source: Box<Error>, unconsumed_packets: Vec<Packet>) -> Self {
281    Self {
282      source,
283      unconsumed_packets,
284    }
285  }
286  /// Underlying error that aborted the fallback transition.
287  #[inline]
288  pub fn source(&self) -> &Error {
289    &self.source
290  }
291  /// Packets that the HW path had consumed but had not yet decoded
292  /// at fallback time.
293  #[inline]
294  pub fn unconsumed_packets(&self) -> &[Packet] {
295    &self.unconsumed_packets
296  }
297  /// Consume the payload, returning the moved unconsumed packets so
298  /// non-seekable callers can replay them through a software decoder
299  /// without cloning.
300  #[inline]
301  pub fn into_unconsumed_packets(self) -> Vec<Packet> {
302    self.unconsumed_packets
303  }
304  /// Consume the payload, returning the moved source error and
305  /// unconsumed packets.
306  #[inline]
307  pub fn into_parts(self) -> (Box<Error>, Vec<Packet>) {
308    (self.source, self.unconsumed_packets)
309  }
310}
311
312impl std::fmt::Debug for FallbackFailed {
313  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314    f.debug_struct("FallbackFailed")
315      .field("source", &self.source)
316      .field(
317        "unconsumed_packets",
318        &format_args!("[{} packets]", self.unconsumed_packets.len()),
319      )
320      .finish()
321  }
322}