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}