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}