mediadecode_ffmpeg/video/mod.rs
1//! `mediadecode::VideoStreamDecoder` impl over FFmpeg's hardware and
2//! software decoders.
3//!
4//! [`FfmpegVideoStreamDecoder`] opens on the [`DecodePath`] it is given:
5//!
6//! * **`Auto`** — what `open` does — probes the platform's hardware
7//! backends in order through [`crate::VideoDecoder`]. When none of them
8//! takes the stream before its first picture, it falls back to a
9//! **software** `ffmpeg::decoder::Video` opened from the same
10//! `Parameters`, and that fallback is lossless: the probe keeps every
11//! packet it consumed and hands them back in
12//! [`AllBackendsFailed`](crate::Error::AllBackendsFailed), and they are
13//! replayed into the software decoder — then the packet the probe
14//! refused — before anything else, so a non-seekable input loses
15//! nothing. A probe that exhausts at `open` opens software at once.
16//! * **`AnyHardware`** is the same probe with software taken away: when no
17//! backend takes the stream before its first picture, the session
18//! reports [`AllBackendsFailed`](crate::Error::AllBackendsFailed) with
19//! the packets the probe took (none, when no backend opens at all), and
20//! the caller decides what to replay them into.
21//! * **`Hardware(b)`** is that one backend or nothing, committed at open.
22//! * **`Software`** is libavcodec's own decoder, with no probe.
23//!
24//! # After the first picture, nothing changes the road
25//!
26//! Once a backend has committed — at its first picture, or at open when
27//! it was named — the session stays on it, and nothing is classified.
28//! Every decoder failure ([`VideoDecodeError::Decode`]) is that picture's
29//! own error, reported as it was minted, and nothing of it is remembered,
30//! so the next call reaches libavcodec. Whether a hardware session
31//! recovers is FFmpeg's: after a VideoToolbox restart that fails, every
32//! picture fails the same way until a new parameter set re-arms the
33//! restart, and `flush` rebuilds no hardware session (see
34//! [`crate::VideoDecoder`] for the lines). This wrapper neither waits for
35//! a recovery nor rules one out.
36//!
37//! A [`VideoDecodeError::Convert`] is this wrapper's own failure to
38//! convert a decoded picture into a frame, not the decoder's. One that
39//! failed on an allocation parks the picture, and until a `receive_frame`
40//! delivers it, `send_packet` and `send_eof` answer `Sent::MustDrain`
41//! without reaching libavcodec: back pressure, not a failure.
42//!
43//! **`Auto` does not go on in software at that point.** A software
44//! decoder that starts mid-stream holds no reference pictures, and after
45//! the first picture this session keeps no packets to give it, so it
46//! cannot switch without a gap. Whether to switch, and what gap to
47//! accept, is the caller's: it sees the failures and what it has
48//! delivered, and it decides what to keep. [`DecodePath`] describes the
49//! simplest policy.
50//!
51//! Frames produced by either decoder are converted via
52//! [`crate::convert::av_frame_to_video_frame`] so the consumer sees the
53//! same `mediadecode::VideoFrame<PixelFormat, VideoFrameExtra,
54//! FfmpegBytes>` shape regardless of which backend produced it.
55
56use std::collections::VecDeque;
57
58/// Maximum number of frames the SW fallback replay path will buffer
59/// while draining the new SW decoder during packet/EOF replay.
60/// Replaying many compressed packets through SW can produce hundreds
61/// of decoded frames before the fallback commits; with no cap the
62/// resident memory grows unbounded (e.g. 4K frames at ~12 MB each ×
63/// 100s of frames). 64 frames is enough room to absorb every
64/// realistic codec's reorder/lookahead window without becoming a
65/// resource sink.
66const SW_REPLAY_FRAME_CAP: usize = 64;
67
68use derive_more::{IsVariant, TryUnwrap, Unwrap};
69use ffmpeg_next::{Packet, codec::Parameters, frame};
70use mediadecode::{
71 Received, Sent, Timebase,
72 decoder::{ScaledOutputCapability, VideoStreamDecoder},
73 frame::VideoFrame,
74 packet::VideoPacket,
75};
76
77use crate::{
78 Backend, DecoderLimits, Error, Ffmpeg, Frame, VideoDecoder, boundary,
79 convert::{self, ConvertError},
80 decoder::{build_codec_context, try_clone_parameters},
81 error::FallbackFailed,
82 extras::{VideoFrameExtra, VideoPacketExtra},
83 frame::alloc_av_video_frame,
84};
85
86/// Which decode path a video session takes — the choice
87/// [`CarrierVideoStreamDecoder::open_as`] is given.
88///
89/// # Three words and a pin, and what each permits
90///
91/// - [`Auto`](Self::Auto): before the first picture, the platform's
92/// hardware backends in probe order, the packets taken so far replayed
93/// across them; when none takes the stream, software, the same packets
94/// replayed into it. After it, the decoder that produced it.
95/// - [`AnyHardware`](Self::AnyHardware): the same probe, and never
96/// software — when no backend takes the stream,
97/// [`Error::AllBackendsFailed`] hands back the packets the probe took.
98/// After the first picture, the backend that produced it.
99/// - [`Software`](Self::Software): libavcodec's own decoder, with no
100/// probe, before and after.
101/// - [`Hardware(b)`](Self::Hardware), the pin: backend `b`, committed at
102/// open; nothing else is tried, before or after.
103///
104/// **After the first picture nothing changes the road, on any path, and
105/// nothing is classified.** A decoder failure
106/// ([`VideoDecodeError::Decode`]) is that picture's own error, reported as
107/// it was minted, and nothing of it is remembered, so the next call
108/// reaches libavcodec. Whether a hardware session recovers is FFmpeg's:
109/// after a VideoToolbox restart that fails, every picture fails the same
110/// way until a new parameter set re-arms the restart, and `flush` rebuilds
111/// no hardware session. See [`VideoDecoder`](crate::VideoDecoder) for the
112/// lines.
113///
114/// # When to stop trusting a hardware session
115///
116/// A caller that sees a hardware session's decoder failures persist
117/// rebuilds: it opens a session on [`Software`](Self::Software) from the
118/// same parameters and feeds it forward. When to do so is the caller's
119/// policy, not this crate's, because it is the caller that sees the
120/// failures on all three roads, the packets' key flags and what it has
121/// delivered. What the new session can be fed depends on the road the
122/// failure came from:
123///
124/// - **`send_packet`**: the failure names the packet in hand, and the new
125/// session is given that packet.
126/// - **`receive_frame`**: the failure may concern a packet accepted
127/// earlier. FFmpeg decouples input from output and may hold several
128/// pictures (`libavcodec/avcodec.h` 90–139 in FFmpeg 9.0.1), so no
129/// packet is named: the new session is given the next packet, and the
130/// caller accepts the gap.
131/// - **`send_eof`**: the end has no packet to give a new session. What
132/// the hardware session still held is recoverable only from packets
133/// kept from before, or by a seek.
134///
135/// What a policy counts is the hardware session's decoder failures,
136/// [`VideoDecodeError::Decode`]. A [`VideoDecodeError::Convert`] is this
137/// wrapper's own: a decoded picture it could not convert into a frame (a
138/// frame ceiling, a pixel format or plane layout it cannot carry, an
139/// allocation). It is reported, not counted. One that failed on an
140/// allocation parks the picture: the next `receive_frame` converts it
141/// again, and until one delivers it `send_packet` and `send_eof` answer
142/// [`Sent::MustDrain`] without reaching libavcodec. That is the wrapper's
143/// back pressure, not a failure.
144///
145/// The example in the [crate documentation](crate) is the simplest such
146/// policy:
147///
148/// - It counts the hardware session's decoder failures from all three
149/// roads, and only a delivered picture ends the count.
150/// - At a threshold of its own it opens [`Software`](Self::Software) from
151/// the same parameters and feeds it forward, road by road as above.
152/// Every picture the new session delivers is delivered as it is.
153/// - On software its failures are reported and the stream goes on:
154/// software is where the policy ends.
155///
156/// It keeps no packets, so the pictures from a failure to the next
157/// keyframe are lost: the new session holds no reference pictures, and
158/// libavcodec drops or conceals what comes before one. A caller that
159/// cannot afford that gap keeps packets and replays them with a picture
160/// identity of its own; that bookkeeping is the caller's design, and the
161/// example does not show it.
162///
163/// The words differ in what they **permit**, not only where they start.
164/// That is the difference the consumers of this door need. A determinism
165/// comparison decodes *one stream* both ways and compares the pixels; a
166/// run that silently swapped paths halfway would compare nothing and say
167/// it had. An operator turning hardware off for a lane over a driver that
168/// produces wrong pixels needs it to stay off. And a node that sends
169/// hardware and software sessions to separate pools needs a hardware
170/// session that never quietly becomes a software one — which is what
171/// [`AnyHardware`](Self::AnyHardware) is for.
172///
173/// # Observability
174///
175/// [`is_hardware`](CarrierVideoStreamDecoder::is_hardware) and
176/// [`is_software`](CarrierVideoStreamDecoder::is_software) read where a
177/// session **is**, which stays a live reading — under
178/// [`Auto`](Self::Auto) it can change once, during the probe, and under
179/// the others it answers what was chosen because nothing can move it.
180///
181/// This type deliberately grows **no** `is_*` predicates of its own,
182/// where most vocabularies in this crate do. They would spell the
183/// decoder's two questions a second time with a different meaning —
184/// `path.is_software()` is *what was asked for* and
185/// `decoder.is_software()` is *where it ended up*, and under
186/// [`Auto`](Self::Auto) those genuinely differ. A caller that needs to
187/// branch on the choice it made already holds the value and can
188/// `match` it.
189#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
190pub enum DecodePath {
191 /// Probe the platform's hardware backends in order and fall back to
192 /// software when none takes the stream before its first picture, the
193 /// packets the probe consumed replayed into it so nothing is lost.
194 /// After the first picture the session stays on the decoder that
195 /// produced it.
196 ///
197 /// What [`CarrierVideoStreamDecoder::open`] does.
198 Auto,
199 /// **Any of the platform's hardware backends, and never software.**
200 ///
201 /// The same probe as [`Auto`](Self::Auto): the backends in the
202 /// platform's probe order, the packets taken before the first picture
203 /// replayed across them as each one is tried. Where `Auto` would then
204 /// open software, this arm refuses — at
205 /// [`open_as`](CarrierVideoStreamDecoder::open_as) when no backend
206 /// opens (on a platform with no hardware backend at all, at once), and
207 /// on the road that met it when the probe exhausts later — with
208 /// [`Error::AllBackendsFailed`] carrying every backend's attempt and the
209 /// packets the probe took, so the caller can replay them into a
210 /// software session of its own. After the first picture it is the
211 /// backend that produced it, as on every path.
212 ///
213 /// [`is_hardware`](CarrierVideoStreamDecoder::is_hardware) answers
214 /// `true` for the session's whole life.
215 AnyHardware,
216 /// **This hardware backend, or nothing.** No other backend is probed
217 /// and software is never opened.
218 ///
219 /// A backend that cannot be opened for the stream fails the
220 /// [`open_as`](CarrierVideoStreamDecoder::open_as) call. The backend is
221 /// committed from open, so every failure after it is that picture's
222 /// own error, as on every path.
223 Hardware(Backend),
224 /// **Software, with no probe at all.**
225 ///
226 /// Opens `libavcodec`'s own decoder for the stream directly. There is
227 /// no hardware in this session to fail, so there is nothing for it to
228 /// fall back from — the state [`Auto`](Self::Auto) reaches when its
229 /// probe exhausts, entered on purpose.
230 Software,
231}
232
233/// `mediadecode::VideoStreamDecoder` impl over FFmpeg's hardware and
234/// software decoders, on the [`DecodePath`] it was opened on — under
235/// [`DecodePath::Auto`], hardware with a lossless fallback to software
236/// before the first picture.
237pub struct CarrierVideoStreamDecoder<C: crate::FfmpegCarrier> {
238 state: DecodeState,
239 /// The path this session was opened on — see [`DecodePath`].
240 ///
241 /// Read for exactly one question, [`Self::may_open_software`]: whether
242 /// a probe's exhaustion is this session's cue to open software or its
243 /// cue to report. Kept as the whole choice rather than reduced to that
244 /// bit so a session can say what it *is*, not only what it allows.
245 path: DecodePath,
246 /// Codec parameters retained so we can open a software
247 /// `ffmpeg::decoder::Video` if the HW probe exhausts.
248 parameters: Parameters,
249 /// HW-side scratch frame (filled by [`VideoDecoder::receive_frame`]).
250 hw_scratch: Frame,
251 /// SW-side scratch frame (filled by `ffmpeg::decoder::Video::receive_frame`).
252 sw_scratch: frame::Video,
253 /// Frames produced while draining the SW decoder during fallback
254 /// replay (see [`Self::fall_back_to_sw`]). The trait's
255 /// `receive_frame` delivers from this queue before pulling new
256 /// frames from the SW decoder. Empty in steady-state operation.
257 sw_replay_frames: VecDeque<frame::Video>,
258 /// Resource ceilings for the frames this decoder exports, and for the
259 /// `AVCodecContext`s it opens — HW candidates, the SW fallback, and
260 /// any decoder a later probe advance builds all get the same number.
261 limits: DecoderLimits,
262 /// `true` once `send_eof` has been called on the active decoder.
263 /// Used to propagate EOF to the SW decoder when fallback fires
264 /// during the drain phase — without this, codecs that hold tail
265 /// frames at EOF would hang waiting for an EOF they already saw on
266 /// the HW path.
267 eof_sent: bool,
268 /// Source-stream time base, used to label produced frames.
269 time_base: Timebase,
270 /// The lane this decoder captures into. A marker: the carrier
271 /// appears in the frames it produces, not in its own state.
272 /// `true` when the scratch frame holds a decoded frame whose
273 /// conversion has **not committed** — see
274 /// [`CarrierAudioStreamDecoder::scratch_pending`](crate::audio::CarrierAudioStreamDecoder)
275 /// for the reasoning, which is the same on both roads.
276 ///
277 /// **This decoder has two scratches and can change which one is
278 /// current, so the seat is enforced rather than merely recorded.**
279 /// While it is set, `send_packet` and `send_eof` answer
280 /// [`Sent::MustDrain`]: both are the roads that commit a
281 /// hardware-to-software fallback, and a fallback under a parked frame
282 /// would leave the retry reading the *other* scratch — delivering a
283 /// stale frame, or refusing permanently and stranding a decoded one.
284 /// Refusing makes the retry's state the state that parked it **by
285 /// construction**, which is a stronger guarantee than remembering
286 /// which road produced it.
287 ///
288 /// **The discipline is unchanged; only its spelling moved.** It was
289 /// `VideoDecodeError::FramePending`, and the escape was already
290 /// documented as "call `receive_frame`, or `flush` to abandon it" —
291 /// which is to say it was back pressure wearing an error's clothes.
292 /// Now it says so, and a caller can act on it without inspecting a
293 /// backend-specific error type. The subtitle decoder keeps the same
294 /// seat one road over, spelled the same way.
295 scratch_pending: bool,
296 _carrier: core::marker::PhantomData<C>,
297}
298
299/// Hardware-decode seam behind [`DecodeState::Hw`]. In production this is
300/// the real [`VideoDecoder`]; tests substitute a fake to drive the
301/// probe-era fallback and the committed road without a live GPU. Mirrors
302/// the subset of `VideoDecoder`'s surface the wrapper drives on the HW
303/// path.
304pub(crate) trait HwInner: Send {
305 /// See [`VideoDecoder::send_packet`].
306 fn send_packet(&mut self, packet: &Packet) -> Result<Sent, Error>;
307 /// See [`VideoDecoder::receive_frame`].
308 fn receive_frame(&mut self, frame: &mut Frame) -> Result<Received, Error>;
309 /// See [`VideoDecoder::send_eof`].
310 fn send_eof(&mut self) -> Result<Sent, Error>;
311 /// See [`VideoDecoder::flush`]. Returns `Result` for a uniform seam even
312 /// though the inherent method is infallible.
313 fn flush(&mut self) -> Result<(), Error>;
314 /// Downcast to the concrete [`VideoDecoder`] when this seam is the real
315 /// HW decoder, so [`FfmpegVideoStreamDecoder::hardware_inner`] can keep
316 /// exposing it. Returns `None` for a test fake.
317 fn as_video_decoder(&self) -> Option<&VideoDecoder>;
318
319 /// See [`VideoDecoder::scaled_output_capability`].
320 ///
321 /// Defaulted to the refusal so a test fake — which has no
322 /// VideoToolbox road behind it, and therefore no stage — answers
323 /// honestly without having to say so.
324 fn scaled_output_capability(&self) -> ScaledOutputCapability {
325 ScaledOutputCapability::Unsupported
326 }
327
328 /// See [`VideoDecoder::request_scaled_output`]. Defaulted to the
329 /// refusal, for the same reason as above.
330 fn request_scaled_output(&mut self, size: (u32, u32)) -> ScaledOutputCapability {
331 let _ = size;
332 ScaledOutputCapability::Unsupported
333 }
334
335 /// See [`VideoDecoder::cancel_scaled_output`]. Defaulted to nothing,
336 /// because a seat that never accepts a request has none to withdraw.
337 fn cancel_scaled_output(&mut self) {}
338}
339
340impl HwInner for VideoDecoder {
341 #[inline]
342 fn send_packet(&mut self, packet: &Packet) -> Result<Sent, Error> {
343 VideoDecoder::send_packet(self, packet)
344 }
345 #[inline]
346 fn receive_frame(&mut self, frame: &mut Frame) -> Result<Received, Error> {
347 VideoDecoder::receive_frame(self, frame)
348 }
349 #[inline]
350 fn send_eof(&mut self) -> Result<Sent, Error> {
351 VideoDecoder::send_eof(self)
352 }
353 #[inline]
354 fn flush(&mut self) -> Result<(), Error> {
355 VideoDecoder::flush(self);
356 Ok(())
357 }
358 #[inline]
359 fn as_video_decoder(&self) -> Option<&VideoDecoder> {
360 Some(self)
361 }
362 #[inline]
363 fn scaled_output_capability(&self) -> ScaledOutputCapability {
364 VideoDecoder::scaled_output_capability(self)
365 }
366 #[inline]
367 fn request_scaled_output(&mut self, size: (u32, u32)) -> ScaledOutputCapability {
368 VideoDecoder::request_scaled_output(self, size)
369 }
370 #[inline]
371 fn cancel_scaled_output(&mut self) {
372 VideoDecoder::cancel_scaled_output(self);
373 }
374}
375
376/// Internal: which backend is currently driving the decode.
377enum DecodeState {
378 /// Hardware-backed decoder. Under [`DecodePath::Auto`] it may
379 /// transition to `Sw` on its probe's `AllBackendsFailed`, before the
380 /// first picture, and at no other time. Boxed behind [`HwInner`] so
381 /// tests can inject a fake HW decoder.
382 Hw(Box<dyn HwInner>),
383 /// Software decoder. Terminal state.
384 Sw(SwDecoder),
385}
386
387/// A software decoder and the callback state its codec context points
388/// at.
389///
390/// The state carries the allocator judge's byte budget and the
391/// `get_format` declination; it has to outlive the `AVCodecContext`
392/// that references it, which is why it is a field here rather than a
393/// value dropped at the end of `open_sw_decoder`.
394///
395/// `Deref` so that every call site keeps talking to the decoder and
396/// only the construction changed — this pairing is a lifetime fact, not
397/// a new abstraction.
398pub(crate) struct SwDecoder {
399 decoder: ffmpeg_next::decoder::Video,
400 /// Declared **after** the decoder: fields drop in declaration order,
401 /// so the codec context is freed before the state it points at.
402 _callback_state: Box<crate::ffi::CallbackState>,
403}
404
405impl SwDecoder {
406 /// The callback state this decoder's codec context points at.
407 ///
408 /// Handed out as a raw pointer so an error closure can consult it
409 /// while the decoder itself is mutably borrowed — every software send
410 /// / receive / EOF failure on this road goes through
411 /// [`crate::decoder::software_exit`] with it, so a frame the
412 /// allocator judge refused surfaces named instead of as the `EINVAL`
413 /// libavcodec also uses for corrupt input.
414 ///
415 /// `Deref` alone was not enough: it exposes the decoder and hides the
416 /// state, so every call site kept wrapping raw and the budget refusal
417 /// had no way out on the whole software road — including the replay
418 /// helper, which drops the state when it finishes.
419 pub(crate) fn state(&self) -> *const crate::ffi::CallbackState {
420 &*self._callback_state
421 }
422}
423
424impl core::ops::Deref for SwDecoder {
425 type Target = ffmpeg_next::decoder::Video;
426 fn deref(&self) -> &Self::Target {
427 &self.decoder
428 }
429}
430
431impl core::ops::DerefMut for SwDecoder {
432 fn deref_mut(&mut self) -> &mut Self::Target {
433 &mut self.decoder
434 }
435}
436
437impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierVideoStreamDecoder<C> {
438 /// Opens a decoder for the given codec parameters with the default
439 /// HW backend probe order. If the HW probe can't open any backend,
440 /// falls back to a software `ffmpeg::decoder::Video` immediately —
441 /// `open` only returns `Err` when both paths fail.
442 ///
443 /// A probe that exhausts later, before the first picture, triggers the
444 /// same software fallback with its rescued packets replayed. After the
445 /// first picture nothing changes the road — see [`DecodePath`].
446 ///
447 /// `limits` bounds what one decoded frame may cost. It is taken here
448 /// rather than through a builder because half of it —
449 /// [`DecoderLimits::max_pixels`] — is written into every
450 /// `AVCodecContext` this decoder opens, and a context's ceiling
451 /// cannot be moved after `avcodec_open2`. That includes the contexts
452 /// opened later, by a probe-era fallback or a probe advance: the
453 /// limits are retained for exactly that reason.
454 pub(crate) fn open_impl(
455 parameters: Parameters,
456 time_base: Timebase,
457 limits: DecoderLimits,
458 ) -> Result<Self, Error> {
459 Self::open_as_impl(parameters, time_base, limits, DecodePath::Auto)
460 }
461
462 /// [`Self::open_impl`], with the decode path chosen rather than
463 /// probed. `DecodePath::Auto` is the constructor above, verbatim.
464 pub(crate) fn open_as_impl(
465 parameters: Parameters,
466 time_base: Timebase,
467 limits: DecoderLimits,
468 path: DecodePath,
469 ) -> Result<Self, Error> {
470 Self::open_as_in(
471 parameters,
472 time_base,
473 limits,
474 path,
475 crate::backend::probe_order(),
476 )
477 }
478
479 /// [`Self::open_as_impl`] with the probe order named — the platform's
480 /// own for every public constructor, and an empty one for the lane
481 /// that stands on a platform with no hardware backend at all.
482 pub(crate) fn open_as_in(
483 parameters: Parameters,
484 time_base: Timebase,
485 limits: DecoderLimits,
486 path: DecodePath,
487 order: &[Backend],
488 ) -> Result<Self, Error> {
489 // ffmpeg-next's `Parameters` carries an optional `owner: Rc<dyn Any>`
490 // (when constructed from `stream.parameters()` it points back at
491 // the demuxer's `AVStream`). Upstream marks the type `Send`
492 // anyway, which is unsound the moment a non-`None` owner is in
493 // play — moving such a value across threads moves the `Rc`. We
494 // sidestep this by always storing a deep-cloned `Parameters`
495 // (`avcodec_parameters_copy` produces an owner-free copy), so
496 // the `FfmpegVideoStreamDecoder`'s `Send` reachability never
497 // depends on the caller's owner discipline.
498 //
499 // Use `try_clone_parameters` instead of `Parameters::clone` —
500 // ffmpeg-next's `clone` calls `Parameters::new()` which can
501 // return a `Parameters` whose inner pointer is null on OOM
502 // (`avcodec_parameters_alloc` returns null without indication);
503 // the subsequent `avcodec_parameters_copy` against that null
504 // destination is C UB. Our checked helper surfaces the OOM as
505 // an error instead.
506 let owned_parameters = try_clone_parameters(¶meters, limits.max_codec_parameter_bytes())?;
507 let hw_scratch = Frame::empty()?;
508 let sw_scratch = alloc_av_video_frame()?;
509 let state = match path {
510 DecodePath::Auto => match VideoDecoder::open_probing(
511 try_clone_parameters(&owned_parameters, limits.max_codec_parameter_bytes())?,
512 limits,
513 Some(time_base),
514 order,
515 ) {
516 Ok(hw) => DecodeState::Hw(Box::new(hw)),
517 Err(Error::AllBackendsFailed(_)) => {
518 // Open-time HW exhaustion: no rescued packets (open didn't
519 // see any). Just open SW directly from our owned copy.
520 let sw = open_sw_decoder(&owned_parameters, limits, Some(time_base))?;
521 DecodeState::Sw(sw)
522 }
523 Err(other) => return Err(other),
524 },
525 // **The same probe, and no software behind it.** An exhaustion at
526 // open is the answer — `AllBackendsFailed` with every backend's
527 // attempt and no packet, since none was sent — where `Auto` would
528 // have read it as its cue to open software.
529 DecodePath::AnyHardware => DecodeState::Hw(Box::new(VideoDecoder::open_probing(
530 try_clone_parameters(&owned_parameters, limits.max_codec_parameter_bytes())?,
531 limits,
532 Some(time_base),
533 order,
534 )?)),
535 // **The named backend, and no probe order at all.** Nothing is
536 // tried before it and nothing after it, which is what makes the
537 // arm a pin: an open that fails is the answer, where `Auto` would
538 // have read the same failure as a reason to look elsewhere.
539 DecodePath::Hardware(backend) => {
540 DecodeState::Hw(Box::new(VideoDecoder::open_with_limits_timed(
541 try_clone_parameters(&owned_parameters, limits.max_codec_parameter_bytes())?,
542 backend,
543 limits,
544 time_base,
545 )?))
546 }
547 // The software decoder, opened on purpose rather than reached by
548 // degrading. `DecodeState::Sw` is terminal, so this session has
549 // nothing to keep it on its path but the shape of the state
550 // machine itself.
551 DecodePath::Software => {
552 DecodeState::Sw(open_sw_decoder(&owned_parameters, limits, Some(time_base))?)
553 }
554 };
555 Ok(Self {
556 state,
557 path,
558 parameters: owned_parameters,
559 hw_scratch,
560 sw_scratch,
561 sw_replay_frames: VecDeque::new(),
562 eof_sent: false,
563 time_base,
564 limits,
565 scratch_pending: false,
566 _carrier: core::marker::PhantomData,
567 })
568 }
569
570 /// Returns `true` when this decoder has fallen back to the software
571 /// path. `false` while still on the HW probe (the initial state).
572 #[cfg_attr(not(tarpaulin), inline(always))]
573 pub(crate) const fn is_software_impl(&self) -> bool {
574 matches!(self.state, DecodeState::Sw(_))
575 }
576
577 /// Returns `true` while the HW probe is still active.
578 #[cfg_attr(not(tarpaulin), inline(always))]
579 pub(crate) const fn is_hardware_impl(&self) -> bool {
580 matches!(self.state, DecodeState::Hw(_))
581 }
582
583 /// Whether this session can currently honor a
584 /// [`Self::request_scaled_output_impl`] request. See
585 /// [`ScaledOutputCapability`] for the determinism trade a caller
586 /// takes on by requesting one.
587 ///
588 /// **[`ScaledOutputCapability::Supported`] on exactly one road: a
589 /// live VideoToolbox session on an Apple target.** There, a
590 /// `VTPixelTransferSession` sits between the decoded hardware frame
591 /// and `av_hwframe_transfer_data` and resizes the `CVPixelBuffer` on
592 /// the GPU, so the fitted picture is what crosses to the CPU — see
593 /// [`crate::vtscale`] for the design, and
594 /// [mediadecode#55](https://github.com/findit-studio/mediadecode/issues/55)
595 /// for the ruling that chose it. Everything else answers
596 /// `Unsupported`, and each refusal has its own reason rather than a
597 /// shared shrug:
598 ///
599 /// - **A session that fell back to software during its probe.** The
600 /// stage is the hardware road's; this answer follows the session, so
601 /// it flips to `Unsupported` the moment the fallback commits, and a
602 /// caller that asks again learns it.
603 /// - **The other hardware backends.** [`Backend::Vaapi`],
604 /// [`Backend::Cuda`] and [`Backend::D3d11va`] are wired in source
605 /// (`Backend::av_hwdevice_type`, `probe_order`) but cannot be
606 /// compiled, run or verified on a non-Linux, non-Windows host, and
607 /// each has a native scaling seam of its own that this crate has
608 /// not built: NVDEC/CUVID in-decode scaling
609 /// ([#56](https://github.com/findit-studio/mediadecode/issues/56)),
610 /// VAAPI VPP
611 /// ([#57](https://github.com/findit-studio/mediadecode/issues/57)),
612 /// the D3D11 Video Processor
613 /// ([#58](https://github.com/findit-studio/mediadecode/issues/58)).
614 /// Filed rather than fabricated.
615 /// - **Software.** See [`Self::request_scaled_output_impl`] for the
616 /// software road's own, separate refusal.
617 ///
618 /// What the VideoToolbox road did **not** get is decode-time
619 /// scaling, and the distinction is worth keeping: inter prediction
620 /// needs full-resolution reference frames, so every road decodes full
621 /// size internally. What this seam saves is the GPU→CPU crossing and
622 /// the CPU frame at the end of it — roughly thirtyfold on a 4K stream
623 /// fitted to a 512-class box. A caller-owned `VTDecompressionSession`
624 /// would save the same crossing and no more, which is why it stays
625 /// #55's standing future enhancement rather than this release's work.
626 ///
627 /// A pure query: calling it requests nothing and changes nothing
628 /// about what [`Self::receive_frame`] delivers.
629 #[cfg_attr(not(tarpaulin), inline(always))]
630 pub(crate) fn scaled_output_capability_impl(&self) -> ScaledOutputCapability {
631 match &self.state {
632 DecodeState::Hw(hw) => hw.scaled_output_capability(),
633 DecodeState::Sw(_) => ScaledOutputCapability::Unsupported,
634 }
635 }
636
637 /// Requests that this session emit pictures at `size` from the next
638 /// frame on. See [`Self::scaled_output_capability_impl`] for which
639 /// road can honor it at all.
640 ///
641 /// # A parked frame refuses
642 ///
643 /// While [`Self::scratch_pending`] holds a decoded picture whose
644 /// conversion did not commit, this refuses. That frame is already
645 /// decided — the retry delivers it from the scratch without
646 /// consulting the stage — so accepting a new size would promise an
647 /// extent the very next frame cannot have. Drain it and ask again;
648 /// the same escape every other seat guarded by that flag offers.
649 ///
650 /// The refusal carries the same meaning as every other: the session
651 /// returns to full coded size, so any request already standing is
652 /// withdrawn. The parked picture keeps the extent it was decoded at.
653 ///
654 /// # When a mid-stream request takes effect
655 ///
656 /// On the **next** picture [`Self::receive_frame`] produces. The
657 /// stage is consulted per frame, on the way out of the hardware
658 /// decoder and before the GPU→CPU download, so a request never
659 /// reaches back to a picture already decoded and never waits longer
660 /// than the one being decoded now.
661 ///
662 /// # The two refusals this seat mints itself
663 ///
664 /// Neither is an error, and each **returns the session to full coded
665 /// size**, dropping any request already standing — what the trait
666 /// says this answer means, and the only reading a caller can act on
667 /// without risking a second resample of an already-fitted picture:
668 ///
669 /// - **A zero extent.** A zero-extent picture is not a smaller
670 /// picture.
671 /// - **An upscale.** The stage exists to move fewer bytes across the
672 /// GPU→CPU bus; enlarging moves more, and inventing detail the
673 /// decoder did not produce is the caller's business rather than a
674 /// decode session's. An *equal* size is not an upscale: it is
675 /// accepted, and the stage simply has nothing to do.
676 ///
677 /// # The software road's refusal has its own, different shape
678 ///
679 /// Worth naming rather than folding into "no backend does this yet":
680 /// FFmpeg's software decoders have no *general* decode-time scaling
681 /// seam. The one option that comes close — `AVCodecContext.lowres`
682 /// (the CLI's `-lowres`) — falls short on three separate counts, any
683 /// one of which would disqualify it as this seam's software answer:
684 ///
685 /// 1. **Narrow codec coverage.** `lowres` is wired only into the
686 /// legacy MPEG-family decoders (MPEG-1/2/4 part 2, H.263) that
687 /// still carry the low-resolution IDCT machinery it depends on.
688 /// HEVC, AV1 and VP9 — the codecs a modern HDR pipeline actually
689 /// decodes — implement no `lowres` support at all.
690 /// 2. **The one codec that is wired is broken.** `lowres` on H.264
691 /// (also nominally covered) has been non-functional for years —
692 /// the decoder does not honor it correctly — so even the "old
693 /// family" half of the promise does not hold across the board.
694 /// 3. **It is not a resize, it is reduced reconstruction.** Where it
695 /// does work, `lowres` decodes at a coarser IDCT precision
696 /// (`1<<lowres`), skipping reconstruction detail rather than
697 /// decoding in full and scaling the result — later inter frames
698 /// drift from a reference the decoder itself degraded, which is a
699 /// different (and worse) contract than "the same picture, smaller".
700 ///
701 /// So the software road's answer is not "unimplemented" the way the
702 /// other hardware backends' is — it is "full-size decode, then the
703 /// fused conform walk downstream", by design, on every codec this
704 /// crate decodes in software.
705 #[cfg_attr(not(tarpaulin), inline(always))]
706 pub(crate) fn request_scaled_output_impl(&mut self, size: (u32, u32)) -> ScaledOutputCapability {
707 // **A parked frame is already decided, so it may not be
708 // re-promised.** [`Self::scratch_pending`] means a picture came out
709 // of the decoder and its conversion did not commit; the retry
710 // delivers *that* frame from the scratch without going back through
711 // the stage, so it will arrive at whatever extent it already has.
712 // Accepting a new size here would answer `Supported` and then hand
713 // the caller a frame the new request never touched — the exact
714 // silent mismatch [`Self::scaled_output_capability_impl`]'s promise
715 // exists to rule out. Refusing changes nothing, which is the
716 // contract for a refusal, and the caller's escape is the one this
717 // seat already documents everywhere else: drain the frame, then ask
718 // again.
719 if self.scratch_pending {
720 tracing::debug!(
721 requested_width = size.0,
722 requested_height = size.1,
723 "mediadecode-ffmpeg: scaled-output request refused while a decoded frame is parked; \
724 the session returns to full size — drain it and ask again"
725 );
726 // **A refusal means the same thing here as anywhere else.**
727 // Returning `Unsupported` while an earlier request stayed armed
728 // would leave the caller resampling pictures this session went on
729 // fitting — the very double-scale the word exists to prevent. So
730 // the standing request goes, and with it what was built for it.
731 // The parked picture keeps the extent it was decoded at, which is
732 // the same rule an *acceptance* has always carried.
733 if let DecodeState::Hw(hw) = &mut self.state {
734 hw.cancel_scaled_output();
735 }
736 return ScaledOutputCapability::Unsupported;
737 }
738 match &mut self.state {
739 DecodeState::Hw(hw) => hw.request_scaled_output(size),
740 DecodeState::Sw(_) => ScaledOutputCapability::Unsupported,
741 }
742 }
743
744 /// Borrow the inner [`VideoDecoder`] when this decoder is still on the
745 /// real HW path. Returns `None` after the SW fallback has fired (or, in
746 /// tests, when the HW seam is a fake rather than a real decoder).
747 #[cfg_attr(not(tarpaulin), inline(always))]
748 pub(crate) fn hardware_inner_impl(&self) -> Option<&VideoDecoder> {
749 match &self.state {
750 DecodeState::Hw(hw) => hw.as_video_decoder(),
751 DecodeState::Sw(_) => None,
752 }
753 }
754
755 /// Returns the time base associated with the source stream.
756 #[cfg_attr(not(tarpaulin), inline(always))]
757 pub(crate) const fn time_base_impl(&self) -> Timebase {
758 self.time_base
759 }
760
761 /// Whether this session may open a software decoder in answer to its
762 /// probe's exhaustion.
763 ///
764 /// **The one place a path that excludes software is enforced**,
765 /// consulted by all three roads that can meet a probe-era
766 /// [`Error::AllBackendsFailed`] — the two send arms and the receive
767 /// arm. It is one predicate rather than three conditions because the
768 /// promise is one: a session opened on [`DecodePath::AnyHardware`] or
769 /// [`DecodePath::Hardware`] ends on hardware or ends in an error, and
770 /// a road that forgot to ask would break that promise silently, which
771 /// is the failure mode a caller cannot see.
772 ///
773 /// It is a question for the probe era only. After the first picture no
774 /// path opens software: a failure there is that picture's own error, on
775 /// every path.
776 ///
777 /// [`DecodePath::Software`] answers `true` and it costs nothing:
778 /// `DecodeState::Sw` is terminal, so no hardware exhaustion can
779 /// reach a road that asks. Answering for it by state rather than by
780 /// path would make the predicate say something it does not mean.
781 #[cfg_attr(not(tarpaulin), inline(always))]
782 const fn may_open_software(&self) -> bool {
783 matches!(self.path, DecodePath::Auto | DecodePath::Software)
784 }
785
786 /// Internal: **probe-era** transition from HW to SW. Replays the rescued
787 /// packets (the inner decoder's buffered history, already accepted by the HW
788 /// probe but not yet decoded) through the new SW decoder so the stream resumes
789 /// seamlessly. No frame was delivered on the HW path yet, so replaying the
790 /// history is lossless.
791 ///
792 /// It is the only transition from hardware to software, and only
793 /// [`DecodePath::Auto`] takes it: after the first picture nothing
794 /// changes the road.
795 ///
796 /// **Transactional**: drained replay frames accumulate in a local
797 /// queue; we only commit them to `self.sw_replay_frames` and switch
798 /// `self.state` to `Sw` after the replay (and EOF re-forwarding, if
799 /// needed) succeed. On failure, the SW decoder, the local frame
800 /// queue, and (where reachable) any consumed packets are dropped —
801 /// `self` is left in its prior state.
802 ///
803 /// **EOF-aware**: when EOF was already accepted on the HW path
804 /// (`self.eof_sent`), the new SW decoder also receives `send_eof()`
805 /// after replay. Without this, codecs that delay tail frames hang
806 /// forever in the drain phase.
807 ///
808 /// **EAGAIN-aware**: if SW's `send_packet` returns EAGAIN during
809 /// replay, drain produced frames into the local queue and retry.
810 ///
811 /// `eof_pending` is passed as a **local** argument rather than read from
812 /// `self.eof_sent`: callers must not mutate `self.eof_sent` before this
813 /// transaction commits (see [`VideoStreamDecoder::send_eof`]), so the
814 /// in-transaction SW EOF re-forward keys off the local flag and `self`'s
815 /// EOF state is updated only after a clean commit.
816 fn fall_back_to_sw(
817 &mut self,
818 unconsumed_packets: std::vec::Vec<ffmpeg_next::Packet>,
819 eof_pending: bool,
820 ) -> Result<(), Error> {
821 tracing::info!(
822 packets_replayed = unconsumed_packets.len(),
823 eof_pending,
824 "mediadecode-ffmpeg: HW probe exhausted, falling back to software decode",
825 );
826 // Wrap the internal worker so any failure path returns the
827 // rescued packets to the caller via `Error::FallbackFailed`.
828 // Without this, non-seekable streams (live feeds, pipes) would
829 // lose every compressed byte the HW path had consumed when a
830 // fallback transition fails partway.
831 match self.fall_back_to_sw_inner(&unconsumed_packets, eof_pending) {
832 Ok(()) => Ok(()),
833 Err(source) => Err(Error::FallbackFailed(FallbackFailed::new(
834 Box::new(source),
835 unconsumed_packets,
836 ))),
837 }
838 }
839
840 /// Worker for [`Self::fall_back_to_sw`]. Returns the rescued packets
841 /// untouched on the borrowed slice; the wrapper takes ownership of
842 /// them and surfaces them in `FallbackFailed` if this returns Err.
843 fn fall_back_to_sw_inner(
844 &mut self,
845 unconsumed_packets: &[ffmpeg_next::Packet],
846 eof_pending: bool,
847 ) -> Result<(), Error> {
848 let mut sw = open_sw_decoder(&self.parameters, self.limits, Some(self.time_base))?;
849 // Bound before the decoder is mutably borrowed, so the error
850 // closures below can still consult it.
851 let sw_state = sw.state();
852 let mut local_replay: VecDeque<frame::Video> = VecDeque::new();
853 // Helper: drain SW into the local replay queue, capped at
854 // `SW_REPLAY_FRAME_CAP`.
855 //
856 // Error discipline: stop the drain **only** on the transient
857 // backpressure signals EAGAIN / EOF (the decoder has no more output for
858 // now). Every other `ffmpeg_next::Error` — e.g. `InvalidData` from a
859 // corrupt replayed packet — is a real decode failure and is propagated,
860 // so a non-recoverable error surfaces as `FallbackFailed` (carrying the
861 // replay packets) instead of being silently swallowed and the fallback
862 // committed over corruption.
863 fn drain_into(
864 sw: &mut ffmpeg_next::decoder::Video,
865 state: *const crate::ffi::CallbackState,
866 local_replay: &mut VecDeque<frame::Video>,
867 ) -> std::result::Result<(), Error> {
868 loop {
869 let mut tmp = alloc_av_video_frame()?;
870 match sw.receive_frame(&mut tmp) {
871 Ok(()) => {
872 if local_replay.len() >= SW_REPLAY_FRAME_CAP {
873 tracing::error!(
874 cap = SW_REPLAY_FRAME_CAP,
875 "mediadecode-ffmpeg: SW fallback replay produced more frames than the \
876 replay cap allows; aborting fallback (no frames dropped — they're \
877 still in the SW decoder's internal queue and will be released when \
878 it drops)",
879 );
880 return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
881 errno: libc::ENOMEM,
882 }));
883 }
884 local_replay.push_back(tmp);
885 }
886 // EAGAIN / EOF: no more output for now — stop draining, success.
887 Err(ffmpeg_next::Error::Other { errno }) if errno == ffmpeg_next::error::EAGAIN => {
888 break;
889 }
890 Err(ffmpeg_next::Error::Eof) => break,
891 // Any other error is a genuine decode failure on a replayed
892 // packet — surface it so it is not masked as a clean fallback.
893 Err(other) => return Err(crate::decoder::software_exit(state, other)),
894 }
895 }
896 Ok(())
897 }
898
899 for pkt in unconsumed_packets {
900 let mut attempts: u32 = 0;
901 loop {
902 match sw.send_packet(pkt) {
903 Ok(()) => break,
904 Err(ffmpeg_next::Error::Other { errno }) if errno == ffmpeg_next::error::EAGAIN => {
905 drain_into(&mut sw, sw_state, &mut local_replay)?;
906 attempts += 1;
907 if attempts > 16 {
908 return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
909 errno: ffmpeg_next::error::EAGAIN,
910 }));
911 }
912 }
913 Err(other) => return Err(crate::decoder::software_exit(sw_state, other)),
914 }
915 }
916 }
917 // Re-forward EOF if the HW path already saw it. SW EOF can also
918 // return EAGAIN until prior output is drained — mirror the
919 // packet-replay loop.
920 if eof_pending {
921 let mut attempts: u32 = 0;
922 loop {
923 match sw.send_eof() {
924 Ok(()) => break,
925 Err(ffmpeg_next::Error::Other { errno }) if errno == ffmpeg_next::error::EAGAIN => {
926 drain_into(&mut sw, sw_state, &mut local_replay)?;
927 attempts += 1;
928 if attempts > 16 {
929 return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
930 errno: ffmpeg_next::error::EAGAIN,
931 }));
932 }
933 }
934 Err(other) => return Err(crate::decoder::software_exit(sw_state, other)),
935 }
936 }
937 }
938 // Final drain BEFORE commit — the transactional commit boundary. The
939 // EAGAIN-triggered drains above only fire when SW exerts backpressure mid
940 // replay; a SW decoder that ACCEPTS every replayed packet (and the EOF)
941 // without one then surfaces a non-transient error — `InvalidData` from a
942 // corrupt replayed packet, or any other decode failure — only on the *next*
943 // `receive_frame`. Without this drain that error would land after the
944 // commit (frames appended, `state` flipped to `Sw`, rescued packets
945 // dropped) and reach the caller as a plain decode failure, not
946 // `FallbackFailed` — breaking probe-era recovery on non-seekable input.
947 // Draining to EAGAIN/EOF here forces any such error to surface now, so it is
948 // wrapped as `FallbackFailed` (retaining the rescued packets) and the
949 // decoder stays on HW — nothing is committed.
950 drain_into(&mut sw, sw_state, &mut local_replay)?;
951 // Commit: only after replay, any EOF forwarding, AND the final drain
952 // succeeded do we move the new SW decoder and queue into `self`.
953 self.sw_replay_frames.append(&mut local_replay);
954 self.state = DecodeState::Sw(sw);
955 Ok(())
956 }
957
958 /// The one place a delivered frame is committed.
959 ///
960 /// Every road that hands a frame to the caller passes through here —
961 /// the hardware scratch, the software scratch, both replay-queue
962 /// entries, and the retry of a parked frame — so the bookkeeping a
963 /// delivery owes cannot be attached to some of them and forgotten on
964 /// others.
965 fn commit_delivery(
966 &mut self,
967 frame: VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, C::Buffer>,
968 dst: &mut VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, C::Buffer>,
969 ) {
970 // The scratch is free once a carrier exists for what it held.
971 self.scratch_pending = false;
972 *dst = frame;
973 }
974
975 /// Where this session is. See
976 /// [`SessionPhase`](crate::decoder::SessionPhase).
977 ///
978 /// The wrapper never sees a probe — that lives inside the hardware
979 /// seam, which derives its own — so only the committed pair is
980 /// reachable from here.
981 const fn phase(&self) -> crate::decoder::SessionPhase {
982 if self.eof_sent {
983 crate::decoder::SessionPhase::Draining
984 } else {
985 crate::decoder::SessionPhase::Streaming
986 }
987 }
988
989 /// Internal: convert the active scratch frame into a
990 /// `mediadecode::VideoFrame` and write into `dst`.
991 fn deliver_frame(
992 &mut self,
993 dst: &mut VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, C::Buffer>,
994 ) -> Result<Received, VideoDecodeError> {
995 let av_frame = match &mut self.state {
996 DecodeState::Hw(_) => unsafe { self.hw_scratch.as_inner_mut().as_ptr() },
997 DecodeState::Sw(_) => unsafe { self.sw_scratch.as_ptr() },
998 };
999 // SAFETY: the scratch frame is live — either just filled by the
1000 // inner decoder's `receive_frame`, or left holding a frame whose
1001 // conversion did not commit. Convert takes what it needs out of it,
1002 // so the scratch can be reused once this has committed.
1003 let converted = unsafe {
1004 convert::av_frame_to_video_frame_as::<C>(av_frame, self.time_base, self.limits.frame())
1005 };
1006 match converted {
1007 Ok(new_frame) => {
1008 self.commit_delivery(new_frame, dst);
1009 Ok(Received::Frame)
1010 }
1011 Err(e) => {
1012 // Park only what another attempt could survive.
1013 self.scratch_pending = e.parks_in_decode();
1014 Err(VideoDecodeError::Convert(e))
1015 }
1016 }
1017 }
1018}
1019
1020#[cfg(test)]
1021impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierVideoStreamDecoder<C> {
1022 /// Build a decoder around an injected HW seam, bypassing the real probe.
1023 /// Lets tests drive the probe-era fallback and the committed road with a
1024 /// [`HwInner`] fake instead of a live GPU. The SW fallback still opens
1025 /// the **real** `ffmpeg::decoder::Video` from `parameters`, so a fallback
1026 /// in these tests genuinely decodes.
1027 pub(crate) fn from_hw_inner_for_test(
1028 hw: Box<dyn HwInner>,
1029 parameters: Parameters,
1030 time_base: Timebase,
1031 ) -> Result<Self, Error> {
1032 Self::from_hw_inner_for_test_as(hw, parameters, time_base, DecodePath::Auto)
1033 }
1034
1035 /// [`Self::from_hw_inner_for_test`], with the session's
1036 /// [`DecodePath`] named.
1037 ///
1038 /// The seam a **pinned** session's mid-stream behaviour is driven
1039 /// through: a pin's promise is about what happens when the hardware
1040 /// fails after opening, and the only way to reach that on a machine
1041 /// whose GPU works is to inject a seam that fails on demand. See
1042 /// `a_hardware_pin_reports_its_probes_exhaustion_instead_of_falling_back`.
1043 pub(crate) fn from_hw_inner_for_test_as(
1044 hw: Box<dyn HwInner>,
1045 parameters: Parameters,
1046 time_base: Timebase,
1047 path: DecodePath,
1048 ) -> Result<Self, Error> {
1049 let limits = DecoderLimits::default();
1050 let owned_parameters = try_clone_parameters(¶meters, limits.max_codec_parameter_bytes())?;
1051 Ok(Self {
1052 state: DecodeState::Hw(hw),
1053 path,
1054 parameters: owned_parameters,
1055 hw_scratch: Frame::empty()?,
1056 sw_scratch: alloc_av_video_frame()?,
1057 sw_replay_frames: VecDeque::new(),
1058 eof_sent: false,
1059 time_base,
1060 limits,
1061 scratch_pending: false,
1062 _carrier: core::marker::PhantomData,
1063 })
1064 }
1065
1066 /// Whether `send_eof` has been committed on the active decoder. Lets the
1067 /// rollback tests assert that a failed EOF fallback restores (never
1068 /// half-mutates) `eof_sent`.
1069 pub(crate) const fn eof_sent_for_test(&self) -> bool {
1070 self.eof_sent
1071 }
1072
1073 /// Whether the probe-era replay queue is empty — so a lane can tell a
1074 /// frame parked in the scratch from one still waiting in the queue.
1075 pub(crate) fn sw_replay_frames_is_empty_for_test(&self) -> bool {
1076 self.sw_replay_frames.is_empty()
1077 }
1078}
1079
1080impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierVideoStreamDecoder<C> {
1081 /// The fault a submission after end-of-stream earns on this face.
1082 ///
1083 /// **Censused from the empty-seat road rather than invented.** With
1084 /// the seat free, a post-EOF `send_packet` or a repeated `send_eof`
1085 /// reaches libavcodec, which answers `AVERROR_EOF`, and all four
1086 /// roads through this wrapper — hardware and software, packet and
1087 /// EOF — surface it as exactly this value. The gates below short
1088 /// out to the same one so a parked seat cannot change *which* answer
1089 /// a caller gets, only how quickly. `the_post_eof_fault_is_the_one_the_substrate_gives`
1090 /// pins the two against each other.
1091 ///
1092 /// Deliberately **not** a new `VideoDecodeError` arm. The subtitle
1093 /// seam had to mint `AfterEof` because `avcodec_decode_subtitle2` has
1094 /// no state machine to refuse for it; this face already has an answer
1095 /// for the condition, and a second spelling for one fault on one
1096 /// surface is the disease this release is curing.
1097 fn after_eof() -> VideoDecodeError {
1098 VideoDecodeError::Decode(Error::Ffmpeg(ffmpeg_next::Error::Eof))
1099 }
1100
1101 pub(crate) fn send_packet_impl(
1102 &mut self,
1103 packet: &VideoPacket<VideoPacketExtra, C::Buffer>,
1104 ) -> Result<Sent, VideoDecodeError> {
1105 // **The end of the stream outranks the parked seat, and the order
1106 // is the whole point.**
1107 //
1108 // `Sent::MustDrain` is a promise: drain the output and this same
1109 // offer becomes acceptable. Past end-of-stream that promise is
1110 // false — draining empties the seat and the retry still faults,
1111 // until `flush`. Checking the seat first made the wrapper answer
1112 // `MustDrain` for a submission nothing could ever accept, which is
1113 // the same fault-under-back-pressure inversion the subtitle seam
1114 // carried: a caller obeying the contract loops, drains, re-offers,
1115 // and is refused anyway.
1116 //
1117 // It is reachable: `send_eof` is accepted and sets `eof_sent`, a
1118 // delayed tail frame comes out of the decoder, its carrier
1119 // allocation fails parkably, and the seat is taken on a session
1120 // that is already over.
1121 if !self.phase().accepts_input() {
1122 return Err(Self::after_eof());
1123 }
1124 // **Nothing is sent while a frame is parked.** Both send roads can
1125 // commit a hardware-to-software fallback, and a fallback under a
1126 // parked frame would leave the retry reading the other scratch. See
1127 // [`Self::scratch_pending`]. Nothing was consumed, so this is back
1128 // pressure and the packet is still the caller's to re-offer — which
1129 // is true precisely because the stream is not over, checked above.
1130 if self.scratch_pending {
1131 return Ok(Sent::MustDrain);
1132 }
1133 let phase = self.phase();
1134 // Scoped submission: the rebuilt `AVPacket` never leaves this call,
1135 // which is what lets the view lane share its buffer with libavcodec
1136 // rather than copy into it. See `boundary::with_ffmpeg_video_packet`.
1137 let limits = self.limits.packet_limits();
1138 // **One route, on every road.** The submission may share its
1139 // carrier's buffer — wherever `boundary::share_or_copy` can prove
1140 // the padding — in the probe window as after it, hardware or
1141 // software. libavcodec may keep a reference past the call and does
1142 // not write through it (`libavcodec/avcodec.h` 2333–2335 in FFmpeg
1143 // 9.0.1). The one thing that keeps what it is sent *and hands it
1144 // back* is the hardware probe, whose rescue history goes to the
1145 // caller as owned, mutable `Packet`s
1146 // (`AllBackendsFailed::into_unconsumed_packets`); it records copies
1147 // of its own (`decoder::try_clone_packet`), so nothing it hands back
1148 // addresses a carrier.
1149 let route = crate::carrier::BodyRoute::Submission;
1150 boundary::with_ffmpeg_video_packet::<C, _>(packet, limits, route, |av_pkt| {
1151 match &mut self.state {
1152 DecodeState::Hw(hw) => match hw.send_packet(av_pkt) {
1153 // The seam already classified libavcodec's back pressure, so
1154 // both states travel on unchanged.
1155 Ok(status) => Ok(status),
1156 Err(Error::AllBackendsFailed(p)) => {
1157 // **A pinned hardware session reports rather than falls back.**
1158 // See [`Self::may_open_software`]: this is the exhaustion
1159 // `DecodePath::Auto` reads as its cue to open software, and
1160 // the pin's whole content is that it is not that cue here.
1161 // Reported with the payload intact, so the caller keeps the
1162 // backend, its error, and any rescued packets.
1163 if !self.may_open_software() {
1164 return Err(VideoDecodeError::Decode(Error::AllBackendsFailed(p)));
1165 }
1166 // The probe exhausted before the first picture: replay the
1167 // inner decoder's buffered history (lossless — no frame was
1168 // delivered yet), then forward the still-unconsumed current
1169 // packet to SW.
1170 let rescued = p.into_unconsumed_packets();
1171 // `eof_pending` is the committed EOF state — never pre-mutated here.
1172 let eof_pending = self.eof_sent;
1173 self
1174 .fall_back_to_sw(rescued, eof_pending)
1175 .map_err(VideoDecodeError::Decode)?;
1176 // Forward the new (still-unconsumed) current packet to the
1177 // freshly-opened SW decoder — the HW decoder REFUSED it, so it was not
1178 // in the replay set. A failure here surfaces (it is not silently
1179 // dropped), and back pressure from the fresh decoder is reported as
1180 // such rather than mistaken for one: the fallback committed either
1181 // way, and the caller re-offers the packet.
1182 if let DecodeState::Sw(sw) = &mut self.state {
1183 let st = sw.state();
1184 if let Err(e) = sw.send_packet(av_pkt) {
1185 return crate::decoder::software_send(st, e, phase)
1186 .map_err(VideoDecodeError::Decode);
1187 }
1188 }
1189 Ok(Sent::Accepted)
1190 }
1191 Err(other) => Err(VideoDecodeError::Decode(other)),
1192 },
1193 DecodeState::Sw(sw) => {
1194 let st = sw.state();
1195 match sw.send_packet(av_pkt) {
1196 Ok(()) => Ok(Sent::Accepted),
1197 // Funnel, then gate: a refusal the allocator judge latched
1198 // comes back named, and back pressure is `MustDrain`.
1199 Err(e) => crate::decoder::software_send(st, e, phase).map_err(VideoDecodeError::Decode),
1200 }
1201 }
1202 }
1203 })
1204 .map_err(|e| VideoDecodeError::Decode(Error::PacketBuild(e)))?
1205 }
1206
1207 pub(crate) fn receive_frame_impl(
1208 &mut self,
1209 dst: &mut VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, C::Buffer>,
1210 ) -> Result<Received, VideoDecodeError> {
1211 // Deliver any frames produced during SW fallback replay before
1212 // pulling new ones from the SW decoder. This is the queue
1213 // populated by `fall_back_to_sw` when SW returned EAGAIN during
1214 // packet replay.
1215 // **Peeked, not popped.** A replayed frame is the rescue history's
1216 // only copy: popping it before the conversion committed lost it to
1217 // any allocation failure, which is the one thing this queue exists
1218 // to prevent. It leaves the queue when a carrier exists for it.
1219 if let Some(replayed) = self.sw_replay_frames.front() {
1220 // SAFETY: `replayed` is a live AVFrame owned by this queue;
1221 // convert takes what it needs out of it.
1222 let converted = unsafe {
1223 convert::av_frame_to_video_frame_as::<C>(
1224 replayed.as_ptr(),
1225 self.time_base,
1226 self.limits.frame(),
1227 )
1228 };
1229 let new_frame = match converted {
1230 Ok(new_frame) => new_frame,
1231 Err(e) if e.parks_in_decode() => return Err(VideoDecodeError::Convert(e)),
1232 // A frame nothing can carry is dropped rather than re-offered
1233 // forever — the same rule the scratch seat follows.
1234 Err(e) => {
1235 self.sw_replay_frames.pop_front();
1236 return Err(VideoDecodeError::Convert(e));
1237 }
1238 };
1239 self.sw_replay_frames.pop_front();
1240 self.commit_delivery(new_frame, dst);
1241 return Ok(Received::Frame);
1242 }
1243 // A frame whose conversion did not commit is converted again before
1244 // the decoder is asked for another — see [`Self::scratch_pending`].
1245 // The scratch still holds it, and `deliver_frame` reads whichever
1246 // scratch the current state uses.
1247 if self.scratch_pending {
1248 return self.deliver_frame(dst);
1249 }
1250 let phase = self.phase();
1251 loop {
1252 match &mut self.state {
1253 DecodeState::Hw(hw) => match hw.receive_frame(&mut self.hw_scratch) {
1254 Ok(Received::Frame) => {
1255 // The frame is out of the decoder's queue from here; the
1256 // seat is what keeps it if the conversion cannot commit.
1257 self.scratch_pending = true;
1258 return self.deliver_frame(dst);
1259 }
1260 // The hardware seam already classified the two flow signals.
1261 Ok(status) => return Ok(status),
1262 Err(Error::AllBackendsFailed(p)) => {
1263 // The same gate, on the receive road — see
1264 // [`Self::may_open_software`] and the identical gate on the
1265 // two send roads.
1266 if !self.may_open_software() {
1267 return Err(VideoDecodeError::Decode(Error::AllBackendsFailed(p)));
1268 }
1269 // The probe exhausted at frame time, before the first picture:
1270 // replay the buffered history (lossless). There is no current
1271 // packet here.
1272 let rescued = p.into_unconsumed_packets();
1273 // `eof_pending` is the committed EOF state — never pre-mutated here.
1274 let eof_pending = self.eof_sent;
1275 self
1276 .fall_back_to_sw(rescued, eof_pending)
1277 .map_err(VideoDecodeError::Decode)?;
1278 // If the replay produced any drained frames, return one
1279 // immediately — preserves stream order vs. whatever the
1280 // SW decoder will produce next.
1281 // **Peeked, not popped** — the second delivery path onto
1282 // this queue, and it owes the same discipline as the first
1283 // (see the head of `receive_frame_impl`). The replay queue
1284 // is the rescue history's only copy of these frames, so a
1285 // conversion that cannot commit must leave the head where
1286 // it is rather than advance past it.
1287 if let Some(replayed) = self.sw_replay_frames.front() {
1288 // SAFETY: `replayed` is a live AVFrame owned by this
1289 // queue; convert takes what it needs out of it.
1290 let converted = unsafe {
1291 convert::av_frame_to_video_frame_as::<C>(
1292 replayed.as_ptr(),
1293 self.time_base,
1294 self.limits.frame(),
1295 )
1296 };
1297 let new_frame = match converted {
1298 Ok(new_frame) => new_frame,
1299 Err(e) if e.parks_in_decode() => return Err(VideoDecodeError::Convert(e)),
1300 // A frame nothing can carry is dropped rather than
1301 // re-offered forever.
1302 Err(e) => {
1303 self.sw_replay_frames.pop_front();
1304 return Err(VideoDecodeError::Convert(e));
1305 }
1306 };
1307 self.sw_replay_frames.pop_front();
1308 self.commit_delivery(new_frame, dst);
1309 return Ok(Received::Frame);
1310 }
1311 // Fall through to the loop; next iteration takes the Sw arm.
1312 }
1313 Err(other) => return Err(VideoDecodeError::Decode(other)),
1314 },
1315 DecodeState::Sw(sw) => {
1316 // Convert inline (rather than via `deliver_frame`, which borrows all
1317 // of `self`) so only the disjoint fields `sw_scratch` / `time_base`
1318 // are touched alongside the `self.state` borrow `sw` holds.
1319 let st = sw.state();
1320 match sw.receive_frame(&mut self.sw_scratch) {
1321 Ok(()) => {
1322 // The frame is out of the decoder's queue from here; the
1323 // seat is what keeps it if the conversion cannot commit.
1324 self.scratch_pending = true;
1325 // SAFETY: the scratch frame is live (just filled by
1326 // `receive_frame`); convert takes what it needs out of
1327 // it, so the scratch can be reused once this commits.
1328 let converted = unsafe {
1329 convert::av_frame_to_video_frame_as::<C>(
1330 self.sw_scratch.as_ptr(),
1331 self.time_base,
1332 self.limits.frame(),
1333 )
1334 };
1335 let new_frame = match converted {
1336 Ok(new_frame) => new_frame,
1337 Err(e) => {
1338 self.scratch_pending = e.parks_in_decode();
1339 return Err(VideoDecodeError::Convert(e));
1340 }
1341 };
1342 self.commit_delivery(new_frame, dst);
1343 return Ok(Received::Frame);
1344 }
1345 // Funnel first — so a recorded budget refusal is named
1346 // rather than laundered — and read as a status second:
1347 // `EAGAIN` is `NeedsInput`, `Eof` is `Ended`, and the errno
1348 // stops inside this crate either way.
1349 Err(e) => {
1350 return crate::decoder::software_receive(st, e, phase)
1351 .map_err(VideoDecodeError::Decode);
1352 }
1353 }
1354 }
1355 }
1356 }
1357 }
1358
1359 pub(crate) fn send_eof_impl(&mut self) -> Result<Sent, VideoDecodeError> {
1360 // The same two gates in the same order, for the same reason: a
1361 // repeated end-of-stream past a committed one is refused however
1362 // much is drained, so answering back pressure would be a promise
1363 // this face cannot keep. See [`Self::after_eof`].
1364 if !self.phase().accepts_input() {
1365 return Err(Self::after_eof());
1366 }
1367 // As `send_packet`: EOF can commit a fallback too. Nothing was
1368 // recorded, so drain and signal again.
1369 if self.scratch_pending {
1370 return Ok(Sent::MustDrain);
1371 }
1372 let phase = self.phase();
1373 let outcome = match &mut self.state {
1374 DecodeState::Hw(hw) => match hw.send_eof() {
1375 // The seam classified libavcodec's back pressure already.
1376 Ok(status) => Ok(status),
1377 Err(Error::AllBackendsFailed(p)) => {
1378 // The same gate, on the EOF road — see
1379 // [`Self::may_open_software`]. Returned rather than folded into
1380 // `outcome`: the commit below fires only on `Ok(Sent::Accepted)`,
1381 // so the two roads agree.
1382 if !self.may_open_software() {
1383 return Err(VideoDecodeError::Decode(Error::AllBackendsFailed(p)));
1384 }
1385 // EOF is pending for this transaction, so the SW decoder must also
1386 // receive `send_eof` (codecs that delay tail frames hang otherwise).
1387 // We pass that intent locally rather than pre-setting `self.eof_sent`:
1388 // a fallback that fails returns `FallbackFailed` and stays on HW, and a
1389 // half-mutated `self.eof_sent = true` would then make a *later*
1390 // fallback inject an EOF into SW even though this `send_eof` errored.
1391 // `self.eof_sent` is committed only after the whole operation succeeds
1392 // (the `outcome` check below), keeping the fallback all-or-nothing.
1393 //
1394 // Replay the buffered history (lossless), re-forwarding EOF inside
1395 // the transaction.
1396 let rescued = p.into_unconsumed_packets();
1397 self
1398 .fall_back_to_sw(rescued, true)
1399 .map(|()| Sent::Accepted)
1400 .map_err(VideoDecodeError::Decode)
1401 }
1402 Err(other) => Err(VideoDecodeError::Decode(other)),
1403 },
1404 DecodeState::Sw(sw) => {
1405 let st = sw.state();
1406 match sw.send_eof() {
1407 Ok(()) => Ok(Sent::Accepted),
1408 Err(e) => crate::decoder::software_send(st, e, phase).map_err(VideoDecodeError::Decode),
1409 }
1410 }
1411 };
1412 // Commit EOF state only when the EOF was actually **taken** — a failed
1413 // fallback left `self.eof_sent` untouched (restored-by-construction: we
1414 // never mutated it), so HW stays EOF-not-yet-sent and a retry behaves
1415 // correctly.
1416 //
1417 // **`is_ok()` is not the test any more, and that is not a stylistic
1418 // change.** `Ok(Sent::MustDrain)` means the decoder did not take the
1419 // end-of-stream; recording `eof_sent` there would make a later fallback
1420 // inject an EOF into the software decoder for a signal that was never
1421 // accepted — the exact half-mutation the local `eof_pending` argument
1422 // exists to prevent on the failure road.
1423 if matches!(outcome, Ok(Sent::Accepted)) {
1424 self.eof_sent = true;
1425 }
1426 outcome
1427 }
1428
1429 pub(crate) fn flush_impl(&mut self) -> Result<(), VideoDecodeError> {
1430 // Drop any frames buffered during SW fallback replay before
1431 // flushing the inner decoder — otherwise a seek/reset would
1432 // surface stale pre-flush frames on the next `receive_frame`.
1433 self.sw_replay_frames.clear();
1434 // And a parked frame belongs to the position being abandoned.
1435 self.scratch_pending = false;
1436 // Flush ends the drain phase; the decoder accepts new packets
1437 // after this, so reset EOF tracking.
1438 self.eof_sent = false;
1439 match &mut self.state {
1440 // The HW seam's `flush` returns `Result` for a uniform trait; the
1441 // real `VideoDecoder::flush` is infallible (always `Ok`).
1442 DecodeState::Hw(hw) => hw.flush().map_err(VideoDecodeError::Decode)?,
1443 DecodeState::Sw(sw) => sw.flush(),
1444 }
1445 Ok(())
1446 }
1447}
1448
1449macro_rules! video_lane_face {
1450 ($($lane:ty),+ $(,)?) => { $(
1451 impl CarrierVideoStreamDecoder<$lane> {
1452 /// Opens a video decoder for `parameters`, probing hardware
1453 /// backends in order and falling back to software.
1454 ///
1455 /// [`open_as`](Self::open_as)`(.., DecodePath::Auto)`.
1456 pub fn open(
1457 parameters: Parameters,
1458 time_base: Timebase,
1459 limits: DecoderLimits,
1460 ) -> Result<Self, Error> {
1461 Self::open_impl(parameters, time_base, limits)
1462 }
1463
1464 /// Opens a video decoder on a **named decode path**.
1465 ///
1466 /// [`DecodePath::Auto`] is [`open`](Self::open) exactly;
1467 /// [`DecodePath::AnyHardware`] keeps the session to the platform's
1468 /// hardware backends and the other two arms pin it to one backend or
1469 /// to software, each for its whole life. See [`DecodePath`] for what
1470 /// each permits and what it costs.
1471 ///
1472 /// Everything else about the session is unchanged — the same
1473 /// [`VideoStreamDecoder`] face, the same frames, the same
1474 /// [`is_hardware`](Self::is_hardware) / [`is_software`](Self::is_software)
1475 /// readings. The choice is *which decoder is behind them*, which
1476 /// is what a determinism comparison and a deployment policy each
1477 /// need and neither could reach.
1478 ///
1479 /// # Errors
1480 ///
1481 /// [`DecodePath::AnyHardware`] fails here with
1482 /// [`Error::AllBackendsFailed`] when no hardware backend opens for
1483 /// the stream — at once on a platform with none — carrying every
1484 /// backend's attempt and no packet, since none was sent.
1485 /// [`DecodePath::Hardware`] fails here when the named backend
1486 /// cannot be opened for the stream. [`DecodePath::Auto`] would have
1487 /// gone on to software in both cases. [`DecodePath::Software`] fails
1488 /// only where libavcodec has no decoder for the stream, or the
1489 /// context cannot be built.
1490 ///
1491 /// # Examples
1492 ///
1493 /// ```no_run
1494 /// use mediadecode_ffmpeg::{DecodePath, DecoderLimits, FfmpegVideoStreamDecoder};
1495 /// # fn f(parameters: ffmpeg_next::codec::Parameters, time_base: mediadecode::Timebase)
1496 /// # -> Result<(), Box<dyn std::error::Error>> {
1497 /// // The same stream, decoded without a GPU anywhere in the story.
1498 /// let decoder = FfmpegVideoStreamDecoder::open_as(
1499 /// parameters,
1500 /// time_base,
1501 /// DecoderLimits::default(),
1502 /// DecodePath::Software,
1503 /// )?;
1504 /// assert!(decoder.is_software());
1505 /// # Ok(())
1506 /// # }
1507 /// ```
1508 pub fn open_as(
1509 parameters: Parameters,
1510 time_base: Timebase,
1511 limits: DecoderLimits,
1512 path: DecodePath,
1513 ) -> Result<Self, Error> {
1514 Self::open_as_impl(parameters, time_base, limits, path)
1515 }
1516
1517 /// Whether this decoder is currently running on software.
1518 pub const fn is_software(&self) -> bool {
1519 self.is_software_impl()
1520 }
1521
1522 /// Whether this decoder is currently running on hardware.
1523 pub const fn is_hardware(&self) -> bool {
1524 self.is_hardware_impl()
1525 }
1526
1527 /// Whether this session can currently emit pictures at a
1528 /// caller-requested output size. See
1529 /// [`ScaledOutputCapability`] and
1530 /// [`Self::request_scaled_output`].
1531 ///
1532 /// `Supported` on a live VideoToolbox session on an Apple
1533 /// target, `Unsupported` everywhere else — including on a
1534 /// session that fell back to software during its probe, which is
1535 /// why this reads the session's live state rather than a fact
1536 /// recorded once at open.
1537 pub fn scaled_output_capability(&self) -> ScaledOutputCapability {
1538 self.scaled_output_capability_impl()
1539 }
1540
1541 /// Requests that this session emit pictures at `size` (width,
1542 /// height) from the next frame on, and reports whether the
1543 /// request was recorded. See
1544 /// [`VideoStreamDecoder::request_scaled_output`] for the full
1545 /// contract (never an error) and
1546 /// [`Self::scaled_output_capability`]'s documentation for which
1547 /// road can honor one, what a mid-stream request means, and the
1548 /// zero / upscale refusals this seat mints itself.
1549 pub fn request_scaled_output(&mut self, size: (u32, u32)) -> ScaledOutputCapability {
1550 self.request_scaled_output_impl(size)
1551 }
1552
1553 /// The hardware wrapper, when one is in use.
1554 pub fn hardware_inner(&self) -> Option<&VideoDecoder> {
1555 self.hardware_inner_impl()
1556 }
1557
1558 /// The stream timebase every produced timestamp is stamped with.
1559 pub const fn time_base(&self) -> Timebase {
1560 self.time_base_impl()
1561 }
1562 }
1563
1564 impl VideoStreamDecoder for CarrierVideoStreamDecoder<$lane> {
1565 type Adapter = Ffmpeg;
1566 type Buffer = <$lane as crate::FfmpegCarrier>::Buffer;
1567 type Error = VideoDecodeError;
1568
1569 fn send_packet(
1570 &mut self,
1571 packet: &VideoPacket<VideoPacketExtra, Self::Buffer>,
1572 ) -> Result<Sent, Self::Error> {
1573 self.send_packet_impl(packet)
1574 }
1575
1576 fn receive_frame(
1577 &mut self,
1578 dst: &mut VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, Self::Buffer>,
1579 ) -> Result<Received, Self::Error> {
1580 self.receive_frame_impl(dst)
1581 }
1582
1583 fn send_eof(&mut self) -> Result<Sent, Self::Error> {
1584 self.send_eof_impl()
1585 }
1586
1587 fn flush(&mut self) -> Result<(), Self::Error> {
1588 self.flush_impl()
1589 }
1590
1591 fn scaled_output_capability(&self) -> ScaledOutputCapability {
1592 self.scaled_output_capability_impl()
1593 }
1594
1595 fn request_scaled_output(&mut self, size: (u32, u32)) -> ScaledOutputCapability {
1596 self.request_scaled_output_impl(size)
1597 }
1598 }
1599 )+ };
1600}
1601
1602video_lane_face!(crate::View, crate::Owned);
1603
1604fn open_sw_decoder(
1605 parameters: &Parameters,
1606 limits: DecoderLimits,
1607 pkt_timebase: Option<Timebase>,
1608) -> Result<SwDecoder, Error> {
1609 // Use the checked codec-context builder — ffmpeg-next's
1610 // `Context::from_parameters` calls `Context::new()` which doesn't
1611 // null-check `avcodec_alloc_context3`'s return value before
1612 // running `avcodec_parameters_to_context` against it. Under
1613 // memory pressure that's C-level UB; `build_codec_context`
1614 // surfaces the OOM as an error instead.
1615 let (ctx, callback_state) = build_codec_context(parameters, limits, pkt_timebase)?;
1616 // Opened without forming a bindgen enum from FFmpeg memory: the codec
1617 // is resolved off a raw `codec_id`, and the medium is proved off a raw
1618 // `codec_type`. See `crate::decoder::ensure_codec_type`.
1619 let codec = crate::decoder::find_decoder(parameters)?;
1620 let opened = ctx.decoder().open_as(codec).map_err(Error::Ffmpeg)?;
1621 crate::decoder::ensure_video_codec_type(&opened)?;
1622 Ok(SwDecoder {
1623 decoder: ffmpeg_next::decoder::Video(opened),
1624 _callback_state: callback_state,
1625 })
1626}
1627
1628/// Error type for [`FfmpegVideoStreamDecoder`] — **faults and the
1629/// send-side refusal**.
1630///
1631/// Every arm here is something that went wrong or something the push
1632/// face declined. The drain's *needs input* and *ended* are
1633/// [`Received`] states out of `receive_frame`; they used to arrive as
1634/// `Decode(Ffmpeg(Other { errno: EAGAIN }))` and `Decode(Ffmpeg(Eof))`,
1635/// which is to say they had no name at this tier at all.
1636///
1637/// **Open fault taxonomy, so it is `#[non_exhaustive]`.** New ways to
1638/// fail are discovered — a backend, a ceiling, a corruption a codec
1639/// learns to report — and a consumer that meets one it has never heard
1640/// of should take its generic-fault path. That is exactly what the
1641/// wildcard arm this attribute forces is for. The two status
1642/// vocabularies opposite it,
1643/// [`Sent`](mediadecode::Sent) and [`Received`](mediadecode::Received),
1644/// are exhaustive for the mirror-image reason: their arms are the
1645/// substrate's fixed state set, and there the wildcard would be dead
1646/// weight hiding a state a consumer forgot.
1647#[derive(thiserror::Error, Debug, IsVariant, Unwrap, TryUnwrap)]
1648#[unwrap(ref, ref_mut)]
1649#[try_unwrap(ref, ref_mut)]
1650#[non_exhaustive]
1651pub enum VideoDecodeError {
1652 /// The wrapped decoder (HW or SW) reported an error.
1653 #[error(transparent)]
1654 Decode(#[from] Error),
1655 /// Frame conversion from FFmpeg's native types to mediadecode's
1656 /// types failed.
1657 #[error(transparent)]
1658 Convert(#[from] ConvertError),
1659}
1660
1661#[cfg(test)]
1662mod tests;