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}