Skip to main content

mediadecode_ffmpeg/
error.rs

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