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, thiserror::Error)]
20pub enum Error {
21  /// An underlying FFmpeg error.
22  #[error("ffmpeg error: {0}")]
23  Ffmpeg(#[from] ffmpeg_next::Error),
24
25  /// `avcodec_find_decoder` returned null for the input codec id. The id
26  /// is reported as the raw integer (`AVCodecID` discriminant) — we do not
27  /// construct the bindgen `AVCodecID` enum from a runtime value, since
28  /// values outside our build's discriminant set would invoke UB.
29  #[error("no decoder for codec id {0}")]
30  NoCodec(u32),
31
32  /// The codec does not advertise a hardware configuration matching the
33  /// requested backend (via `avcodec_get_hw_config`).
34  #[error("codec does not support backend {0:?}")]
35  BackendUnsupportedByCodec(Backend),
36
37  /// `av_hwdevice_ctx_create` failed for the requested backend. See
38  /// [`HwDeviceInitFailed`] for the payload details. `#[from]` gives
39  /// a free `impl From<HwDeviceInitFailed> for Error`, so inner
40  /// helpers that return `Result<_, HwDeviceInitFailed>` can be
41  /// `?`-propagated into `Error` directly.
42  #[error(transparent)]
43  HwDeviceInitFailed(#[from] HwDeviceInitFailed),
44
45  /// Auto-probe exhausted every backend in the platform's order. See
46  /// [`AllBackendsFailed`] for the payload details (in particular the
47  /// `unconsumed_packets` history that callers should replay through
48  /// their own software decoder for non-seekable inputs). `#[from]`
49  /// gives a free `impl From<AllBackendsFailed> for Error`.
50  #[error(transparent)]
51  AllBackendsFailed(#[from] AllBackendsFailed),
52
53  /// Surfaced by [`crate::FfmpegVideoStreamDecoder`] when a HW->SW
54  /// fallback attempt itself fails. See [`FallbackFailed`] for the
55  /// payload details (in particular the rescued `unconsumed_packets`
56  /// the HW path had already consumed from the caller). `#[from]`
57  /// gives a free `impl From<FallbackFailed> for Error`.
58  #[error(transparent)]
59  FallbackFailed(#[from] FallbackFailed),
60}
61
62/// Payload for [`Error::HwDeviceInitFailed`].
63///
64/// `av_hwdevice_ctx_create` failed for the requested backend.
65#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
66#[error("hardware device init failed for {backend:?}: {source}")]
67pub struct HwDeviceInitFailed {
68  /// Backend that failed to initialise.
69  backend: Backend,
70  /// Underlying FFmpeg error.
71  source: ffmpeg_next::Error,
72}
73
74impl HwDeviceInitFailed {
75  /// Constructs a new [`HwDeviceInitFailed`] payload.
76  #[inline]
77  pub const fn new(backend: Backend, source: ffmpeg_next::Error) -> Self {
78    Self { backend, source }
79  }
80  /// Backend that failed to initialise.
81  #[inline]
82  pub const fn backend(&self) -> Backend {
83    self.backend
84  }
85  /// Underlying FFmpeg error.
86  #[inline]
87  pub const fn source(&self) -> &ffmpeg_next::Error {
88    &self.source
89  }
90  /// Consume the payload, returning the backend identifier and the
91  /// moved FFmpeg error so callers can take ownership without
92  /// cloning.
93  #[inline]
94  pub fn into_parts(self) -> (Backend, ffmpeg_next::Error) {
95    (self.backend, self.source)
96  }
97}
98
99/// Payload for [`Error::AllBackendsFailed`].
100///
101/// Auto-probe exhausted every backend in the platform's order. Empty
102/// `attempts` means the platform has no hardware backends listed in
103/// [`crate::Backend`] for the current `target_os` — callers must
104/// fall back to a software decoder of their choice.
105///
106/// `unconsumed_packets` holds the packets the decoder accepted from
107/// the caller before the probe exhausted (refcounted shallow clones
108/// of the packets fed via `send_packet`). For non-seekable inputs
109/// (live streams, pipes, network sources) the caller cannot
110/// re-demux from start, so this crate surfaces the buffered history
111/// here so the caller can feed those packets directly into a
112/// software decoder of their choice. When `AllBackendsFailed` comes
113/// from [`crate::VideoDecoder::open`] (no packets were ever sent),
114/// this vec is empty.
115///
116/// `Debug` is hand-written: [`ffmpeg_next::Packet`] does not derive
117/// `Debug`, so we print `[N packets]` instead of dumping per-packet
118/// bytes, which would be both noisy and useless for triage.
119#[derive(thiserror::Error)]
120#[error("all hardware backends failed; attempts: {attempts:?}")]
121pub struct AllBackendsFailed {
122  /// Per-backend errors collected during probing, in the order tried.
123  attempts: Vec<(Backend, Box<Error>)>,
124  /// Packets the decoder consumed from the caller before exhaustion.
125  /// Replay them through a software decoder for non-seekable inputs.
126  unconsumed_packets: Vec<Packet>,
127}
128
129impl AllBackendsFailed {
130  /// Constructs a new [`AllBackendsFailed`] payload.
131  ///
132  /// Not `const fn`: the `Vec` arguments may carry destructors and
133  /// the const evaluator can't prove their drop safe for arbitrary
134  /// allocator state.
135  #[inline]
136  pub fn new(attempts: Vec<(Backend, Box<Error>)>, unconsumed_packets: Vec<Packet>) -> Self {
137    Self {
138      attempts,
139      unconsumed_packets,
140    }
141  }
142  /// Per-backend errors collected during probing, in the order tried.
143  #[inline]
144  pub fn attempts(&self) -> &[(Backend, Box<Error>)] {
145    &self.attempts
146  }
147  /// Packets the decoder consumed from the caller before exhaustion.
148  /// Replay them through a software decoder for non-seekable inputs.
149  #[inline]
150  pub fn unconsumed_packets(&self) -> &[Packet] {
151    &self.unconsumed_packets
152  }
153  /// Consume the payload, returning the moved unconsumed packets so
154  /// non-seekable callers can replay them through a software decoder
155  /// without cloning.
156  #[inline]
157  pub fn into_unconsumed_packets(self) -> Vec<Packet> {
158    self.unconsumed_packets
159  }
160  /// Consume the payload, returning the moved attempts log and
161  /// unconsumed packets.
162  #[inline]
163  pub fn into_parts(self) -> (Vec<(Backend, Box<Error>)>, Vec<Packet>) {
164    (self.attempts, self.unconsumed_packets)
165  }
166}
167
168impl std::fmt::Debug for AllBackendsFailed {
169  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170    f.debug_struct("AllBackendsFailed")
171      .field("attempts", &self.attempts)
172      // `Packet` is not `Debug`; print just the count so the error is
173      // still useful for triage without dumping per-packet bytes.
174      .field(
175        "unconsumed_packets",
176        &format_args!("[{} packets]", self.unconsumed_packets.len()),
177      )
178      .finish()
179  }
180}
181
182/// Payload for [`Error::FallbackFailed`].
183///
184/// Surfaced by [`crate::FfmpegVideoStreamDecoder`] when a HW->SW
185/// fallback attempt itself fails — e.g. the SW decoder failed to
186/// open, EOF replay returned EAGAIN past the bounded retry, or the
187/// per-frame replay queue exceeded its cap. The HW decoder has
188/// already consumed `unconsumed_packets` from the caller; we
189/// surface them here so non-seekable inputs (pipes, live streams)
190/// can drive their own decoder of last resort.
191///
192/// `Debug` is hand-written for the same reason as
193/// [`AllBackendsFailed`]: [`ffmpeg_next::Packet`] does not derive
194/// `Debug`.
195#[derive(thiserror::Error)]
196#[error("HW->SW fallback failed: {source}")]
197pub struct FallbackFailed {
198  /// Underlying error that aborted the fallback transition.
199  source: Box<Error>,
200  /// Packets that the HW path had consumed but had not yet decoded
201  /// at fallback time. The caller can replay them through a
202  /// software decoder of their choice.
203  unconsumed_packets: Vec<Packet>,
204}
205
206impl FallbackFailed {
207  /// Constructs a new [`FallbackFailed`] payload.
208  ///
209  /// Not `const fn`: the `Vec` argument may carry destructors.
210  #[inline]
211  pub fn new(source: Box<Error>, unconsumed_packets: Vec<Packet>) -> Self {
212    Self {
213      source,
214      unconsumed_packets,
215    }
216  }
217  /// Underlying error that aborted the fallback transition.
218  #[inline]
219  pub fn source(&self) -> &Error {
220    &self.source
221  }
222  /// Packets that the HW path had consumed but had not yet decoded
223  /// at fallback time.
224  #[inline]
225  pub fn unconsumed_packets(&self) -> &[Packet] {
226    &self.unconsumed_packets
227  }
228  /// Consume the payload, returning the moved unconsumed packets so
229  /// non-seekable callers can replay them through a software decoder
230  /// without cloning.
231  #[inline]
232  pub fn into_unconsumed_packets(self) -> Vec<Packet> {
233    self.unconsumed_packets
234  }
235  /// Consume the payload, returning the moved source error and
236  /// unconsumed packets.
237  #[inline]
238  pub fn into_parts(self) -> (Box<Error>, Vec<Packet>) {
239    (self.source, self.unconsumed_packets)
240  }
241}
242
243impl std::fmt::Debug for FallbackFailed {
244  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245    f.debug_struct("FallbackFailed")
246      .field("source", &self.source)
247      .field(
248        "unconsumed_packets",
249        &format_args!("[{} packets]", self.unconsumed_packets.len()),
250      )
251      .finish()
252  }
253}