Skip to main content

mediadecode_ffmpeg/video/
mod.rs

1//! `mediadecode::VideoStreamDecoder` impl with HW + SW fallback.
2//!
3//! [`FfmpegVideoStreamDecoder`] starts on the hardware path: an inner
4//! [`crate::VideoDecoder`] that auto-probes VideoToolbox / VAAPI /
5//! NVDEC / D3D11VA. When every HW backend fails — at `open` time
6//! (no backend opens) or mid-stream ([`crate::Error::AllBackendsFailed`]
7//! from `send_packet` / `receive_frame` / `send_eof`) — we transparently
8//! fall back to a **software** `ffmpeg::decoder::Video` opened from the
9//! same `Parameters`.
10//!
11//! Two HW-exhaustion shapes feed the same fallback, distinguished by an
12//! **explicit origin** the `AllBackendsFailed` carries
13//! ([`crate::error::FallbackOrigin`]) — *not* by whether its rescued
14//! `unconsumed_packets` is empty (both shapes can be empty: a probe-era
15//! failure on the first packet has no prior history, exactly like every
16//! post-commit failure):
17//!
18//! * **Probe-era** (pre-first-frame, [`crate::error::FallbackOrigin::Probe`]):
19//!   the inner decoder buffered every packet it consumed and surfaces them in
20//!   `unconsumed_packets`. We **replay exactly those** through the SW decoder
21//!   (lossless — no frame was delivered yet), then route the still-unconsumed
22//!   current packet (the one the inner decoder failed on / refused) to SW
23//!   ourselves. This is the original pre-runtime-fallback behaviour and is
24//!   unchanged.
25//! * **Post-commit** (after the first frame, the inner probe is gone,
26//!   [`crate::error::FallbackOrigin::PostCommit`]): a runtime HW-decode failure
27//!   — e.g. VideoToolbox choking on H.264 High 4:2:2 10-bit — is reclassified
28//!   to `AllBackendsFailed` by the inner decoder with an **empty**
29//!   `unconsumed_packets` (the probe buffer no longer exists). Here we
30//!   **degrade and continue** rather than reconstruct: open the SW decoder with
31//!   an empty replay set and let it **resync at the next keyframe**. Fed forward
32//!   packets from the failure point, the SW decoder naturally produces nothing
33//!   until that keyframe, then decodes normally from there. The bounded span
34//!   from the failure point to the next keyframe is dropped — an accepted,
35//!   **loudly logged** gap (a single `tracing::warn!`), not a silent one. The
36//!   indexing pipeline this serves prefers a small logged gap over the
37//!   error-prone mid-stream-reconstruction state machine a lossless replay
38//!   would require (see findit-studio/mediadecode#12). The *bounded*-ness is
39//!   **enforced, not assumed**: a post-commit fallback enters a degraded-resync
40//!   mode that holds until a **keyframe-anchored** resync — the SW decoder
41//!   delivering a frame *after* a keyframe was fed to it across the gap. (Gating
42//!   on a keyframe, not on *any* frame, matters because a lenient codec will
43//!   decode a lone P-frame from the dropped span into a concealed frame; that
44//!   must not count as a resync, or the one-GOP bound isn't truly enforced.) If
45//!   EOF is reached while the mode is still pending — no keyframe ever arrived
46//!   across the gap and the whole tail was lost — `receive_frame` escalates with
47//!   a distinct [`VideoDecodeError::PostCommitNeverResynced`] (and a
48//!   `tracing::error!`) rather than surfacing a clean end-of-stream that would
49//!   swallow the tail silently. So the gap is either bounded-and-logged (a real
50//!   keyframe resync happened) or reported-at-EOF (it never did) — never
51//!   silent-and-unbounded.
52//!
53//!   The post-commit path retains and reconstructs **zero** frames: it opens SW
54//!   cold, forwards only the failure arm's current packet (or EOF), and lets SW
55//!   resync naturally. It never populates the replay-frame queue, so the
56//!   replay/conversion machinery the probe-era path uses cannot touch it.
57//!
58//! The probe-era replay happens before the new packet (or the next
59//! `receive_frame` poll) is processed, so a probe-era HW exhaustion on a
60//! non-seekable input loses no compressed data. The post-commit path
61//! intentionally accepts the next-keyframe gap.
62//!
63//! After the transition the decoder stays on SW for the rest of its
64//! life — there's no probe-back-to-HW logic; once we've decided the
65//! stream isn't HW-decodable, that decision is sticky.
66//!
67//! Frames produced by either path are converted via
68//! [`crate::convert::av_frame_to_video_frame`] so the consumer sees
69//! the same `mediadecode::VideoFrame<PixelFormat, VideoFrameExtra,
70//! FfmpegBytes>` shape regardless of which backend produced it.
71
72use std::collections::VecDeque;
73
74/// Maximum number of frames the SW fallback replay path will buffer
75/// while draining the new SW decoder during packet/EOF replay.
76/// Replaying many compressed packets through SW can produce hundreds
77/// of decoded frames before the fallback commits; with no cap the
78/// resident memory grows unbounded (e.g. 4K frames at ~12 MB each ×
79/// 100s of frames). 64 frames is enough room to absorb every
80/// realistic codec's reorder/lookahead window without becoming a
81/// resource sink.
82const SW_REPLAY_FRAME_CAP: usize = 64;
83
84use derive_more::{IsVariant, TryUnwrap, Unwrap};
85use ffmpeg_next::{Packet, codec::Parameters, frame};
86use mediadecode::{Timebase, decoder::VideoStreamDecoder, frame::VideoFrame, packet::VideoPacket};
87
88use crate::{
89  DecoderLimits, Error, Ffmpeg, Frame, VideoDecoder, boundary,
90  convert::{self, ConvertError},
91  decoder::{build_codec_context, try_clone_parameters},
92  error::FallbackFailed,
93  extras::{VideoFrameExtra, VideoPacketExtra},
94  frame::alloc_av_video_frame,
95};
96
97/// `mediadecode::VideoStreamDecoder` impl with transparent HW → SW
98/// fallback.
99pub struct CarrierVideoStreamDecoder<C: crate::FfmpegCarrier> {
100  state: DecodeState,
101  /// Codec parameters retained so we can open a software
102  /// `ffmpeg::decoder::Video` if the HW probe exhausts.
103  parameters: Parameters,
104  /// HW-side scratch frame (filled by [`VideoDecoder::receive_frame`]).
105  hw_scratch: Frame,
106  /// SW-side scratch frame (filled by `ffmpeg::decoder::Video::receive_frame`).
107  sw_scratch: frame::Video,
108  /// Frames produced while draining the SW decoder during fallback
109  /// replay (see [`Self::fall_back_to_sw`]). The trait's
110  /// `receive_frame` delivers from this queue before pulling new
111  /// frames from the SW decoder. Empty in steady-state operation.
112  sw_replay_frames: VecDeque<frame::Video>,
113  /// Resource ceilings for the frames this decoder exports, and for the
114  /// `AVCodecContext`s it opens — HW candidates, the SW fallback, and
115  /// any decoder a later probe advance builds all get the same number.
116  limits: DecoderLimits,
117  /// `true` once `send_eof` has been called on the active decoder.
118  /// Used to propagate EOF to the SW decoder when fallback fires
119  /// during the drain phase — without this, codecs that hold tail
120  /// frames at EOF would hang waiting for an EOF they already saw on
121  /// the HW path.
122  eof_sent: bool,
123  /// `true` between a **post-commit** fallback firing and a *keyframe-anchored*
124  /// resync (the SW decoder delivering a frame **after** a keyframe was fed to
125  /// it across the gap). A post-commit fallback opens SW cold and drops the
126  /// bounded span up to the next keyframe; the promise is that the span is
127  /// *bounded* — SW resyncs at that keyframe. This flag makes the promise
128  /// enforced rather than assumed: while it is set we have no proof SW ever
129  /// recovered from a real keyframe. It is cleared only when SW delivers a frame
130  /// *and* [`Self::degraded_keyframe_seen`] is set (a lone concealed P-frame a
131  /// lenient codec emits from the gap does **not** clear it); if EOF is reached
132  /// while it is still set the loss is escalated (a distinct loud error) rather
133  /// than silently swallowing the whole tail. Probe-era fallbacks never set it —
134  /// they replay losslessly and produce frames immediately.
135  degraded_resync_pending: bool,
136  /// `true` once a **keyframe** packet has been successfully fed to the SW
137  /// decoder while [`Self::degraded_resync_pending`] is set — i.e. a real resync
138  /// anchor crossed the gap. The pending flag clears only on a delivered SW
139  /// frame *after* this is set, so a concealed non-keyframe frame (a lenient
140  /// codec decoding a lone P-frame from the dropped span) cannot masquerade as a
141  /// resync and prematurely clear the guard. Set alongside `enter`/cleared with
142  /// the pending flag.
143  degraded_keyframe_seen: bool,
144  /// Packets fed to the SW decoder since the post-commit fallback fired while
145  /// [`Self::degraded_resync_pending`] is set — i.e. across the unresolved
146  /// resync gap. Reported in the escalation message so the lost span is
147  /// quantified ("N packets, no keyframe found"). Reset whenever the flag
148  /// clears or on `flush`.
149  degraded_packets_since_fallback: u64,
150  /// Source-stream time base, used to label produced frames.
151  time_base: Timebase,
152  /// The lane this decoder captures into. A marker: the carrier
153  /// appears in the frames it produces, not in its own state.
154  /// `true` when the scratch frame holds a decoded frame whose
155  /// conversion has **not committed** — see
156  /// [`CarrierAudioStreamDecoder::scratch_pending`](crate::audio::CarrierAudioStreamDecoder)
157  /// for the reasoning, which is the same on both roads.
158  ///
159  /// **This decoder has two scratches and can change which one is
160  /// current, so the seat is enforced rather than merely recorded.**
161  /// While it is set, `send_packet` and `send_eof` refuse by name: both
162  /// are the roads that commit a hardware-to-software fallback, and a
163  /// fallback under a parked frame would leave the retry reading the
164  /// *other* scratch — delivering a stale frame, or refusing
165  /// permanently and stranding a decoded one. Refusing makes the
166  /// retry's state the state that parked it **by construction**, which
167  /// is a stronger guarantee than remembering which road produced it.
168  ///
169  /// It is the discipline the subtitle decoder already keeps one seat
170  /// over (`SubtitleDecodeError::FramePending`), and the escape is the
171  /// same: `receive_frame` to re-attempt, `flush` to abandon.
172  scratch_pending: bool,
173  _carrier: core::marker::PhantomData<C>,
174}
175
176/// Hardware-decode seam behind [`DecodeState::Hw`]. In production this is
177/// the real [`VideoDecoder`]; tests substitute a fake to drive the
178/// post-commit fallback path without a live GPU. Mirrors the subset of
179/// `VideoDecoder`'s surface the wrapper drives on the HW path.
180pub(crate) trait HwInner: Send {
181  /// See [`VideoDecoder::send_packet`].
182  fn send_packet(&mut self, packet: &Packet) -> Result<(), Error>;
183  /// See [`VideoDecoder::receive_frame`].
184  fn receive_frame(&mut self, frame: &mut Frame) -> Result<(), Error>;
185  /// See [`VideoDecoder::send_eof`].
186  fn send_eof(&mut self) -> Result<(), Error>;
187  /// See [`VideoDecoder::flush`]. Returns `Result` for a uniform seam even
188  /// though the inherent method is infallible.
189  fn flush(&mut self) -> Result<(), Error>;
190  /// Downcast to the concrete [`VideoDecoder`] when this seam is the real
191  /// HW decoder, so [`FfmpegVideoStreamDecoder::hardware_inner`] can keep
192  /// exposing it. Returns `None` for a test fake.
193  fn as_video_decoder(&self) -> Option<&VideoDecoder>;
194
195  /// Whether a packet submitted **now** would be recorded for replay.
196  ///
197  /// The probe keeps a rescue history so that a decoder which exhausts
198  /// every backend can hand the caller everything FFmpeg consumed since
199  /// open. It records by `av_packet_ref`, and
200  /// [`AllBackendsFailed::into_unconsumed_packets`] hands those
201  /// recordings out as owned, **mutable** `Packet`s — which is why the
202  /// view lane must not share its carrier's storage into a submission
203  /// that could be recorded. See
204  /// [`CarrierVideoStreamDecoder::send_packet_impl`].
205  fn records_submissions(&self) -> bool;
206}
207
208impl HwInner for VideoDecoder {
209  #[inline]
210  fn records_submissions(&self) -> bool {
211    self.is_probing()
212  }
213
214  #[inline]
215  fn send_packet(&mut self, packet: &Packet) -> Result<(), Error> {
216    VideoDecoder::send_packet(self, packet)
217  }
218  #[inline]
219  fn receive_frame(&mut self, frame: &mut Frame) -> Result<(), Error> {
220    VideoDecoder::receive_frame(self, frame)
221  }
222  #[inline]
223  fn send_eof(&mut self) -> Result<(), Error> {
224    VideoDecoder::send_eof(self)
225  }
226  #[inline]
227  fn flush(&mut self) -> Result<(), Error> {
228    VideoDecoder::flush(self);
229    Ok(())
230  }
231  #[inline]
232  fn as_video_decoder(&self) -> Option<&VideoDecoder> {
233    Some(self)
234  }
235}
236
237/// Internal: which backend is currently driving the decode.
238enum DecodeState {
239  /// Hardware-backed decoder (auto-probe). May transition to `Sw` on
240  /// `AllBackendsFailed`. Boxed behind [`HwInner`] so tests can inject a
241  /// fake HW decoder.
242  Hw(Box<dyn HwInner>),
243  /// Software decoder. Terminal state.
244  Sw(SwDecoder),
245}
246
247/// A software decoder and the callback state its codec context points
248/// at.
249///
250/// The state carries the allocator judge's byte budget and the
251/// `get_format` declination; it has to outlive the `AVCodecContext`
252/// that references it, which is why it is a field here rather than a
253/// value dropped at the end of `open_sw_decoder`.
254///
255/// `Deref` so that every call site keeps talking to the decoder and
256/// only the construction changed — this pairing is a lifetime fact, not
257/// a new abstraction.
258pub(crate) struct SwDecoder {
259  decoder: ffmpeg_next::decoder::Video,
260  /// Declared **after** the decoder: fields drop in declaration order,
261  /// so the codec context is freed before the state it points at.
262  _callback_state: Box<crate::ffi::CallbackState>,
263}
264
265impl SwDecoder {
266  /// The callback state this decoder's codec context points at.
267  ///
268  /// Handed out as a raw pointer so an error closure can consult it
269  /// while the decoder itself is mutably borrowed — every software send
270  /// / receive / EOF failure on this road goes through
271  /// [`crate::decoder::software_exit`] with it, so a frame the
272  /// allocator judge refused surfaces named instead of as the `EINVAL`
273  /// libavcodec also uses for corrupt input.
274  ///
275  /// `Deref` alone was not enough: it exposes the decoder and hides the
276  /// state, so every call site kept wrapping raw and the budget refusal
277  /// had no way out on the whole software road — including the replay
278  /// and cold-fallback helpers, which drop the state when they finish.
279  pub(crate) fn state(&self) -> *const crate::ffi::CallbackState {
280    &*self._callback_state
281  }
282}
283
284impl core::ops::Deref for SwDecoder {
285  type Target = ffmpeg_next::decoder::Video;
286  fn deref(&self) -> &Self::Target {
287    &self.decoder
288  }
289}
290
291impl core::ops::DerefMut for SwDecoder {
292  fn deref_mut(&mut self) -> &mut Self::Target {
293    &mut self.decoder
294  }
295}
296
297/// What the cold SW decoder is fed on a **post-commit** degrade transition,
298/// named by the failure arm so the three shapes stay mutually exclusive (a
299/// current packet and EOF are never forwarded together). The post-commit path
300/// retains no replay frames, so this is the *only* thing handed to the new SW
301/// decoder at fallback time. See [`FfmpegVideoStreamDecoder::degrade_to_sw`].
302enum PostCommitInput<'a> {
303  /// `send_packet` arm: forward this current packet — the one the HW decoder
304  /// refused (so it was never in any replay set). If it is a keyframe it is the
305  /// resync anchor.
306  Packet(&'a Packet),
307  /// `receive_frame` arm: a frame-time failure has no current packet to forward.
308  FrameTime,
309  /// `send_eof` arm: EOF was pending on the HW path; re-forward it to the cold
310  /// SW so tail-delaying codecs don't hang.
311  Eof,
312}
313
314impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierVideoStreamDecoder<C> {
315  /// Opens a decoder for the given codec parameters with the default
316  /// HW backend probe order. If the HW probe can't open any backend,
317  /// falls back to a software `ffmpeg::decoder::Video` immediately —
318  /// `open` only returns `Err` when both paths fail.
319  ///
320  /// Subsequent mid-stream `AllBackendsFailed` from the HW path
321  /// triggers the same SW fallback (with rescued packets replayed).
322  ///
323  /// `limits` bounds what one decoded frame may cost. It is taken here
324  /// rather than through a builder because half of it —
325  /// [`DecoderLimits::max_pixels`] — is written into every
326  /// `AVCodecContext` this decoder opens, and a context's ceiling
327  /// cannot be moved after `avcodec_open2`. That includes the contexts
328  /// opened later, by a mid-stream fallback or a probe advance: the
329  /// limits are retained for exactly that reason.
330  pub(crate) fn open_impl(
331    parameters: Parameters,
332    time_base: Timebase,
333    limits: DecoderLimits,
334  ) -> Result<Self, Error> {
335    // ffmpeg-next's `Parameters` carries an optional `owner: Rc<dyn Any>`
336    // (when constructed from `stream.parameters()` it points back at
337    // the demuxer's `AVStream`). Upstream marks the type `Send`
338    // anyway, which is unsound the moment a non-`None` owner is in
339    // play — moving such a value across threads moves the `Rc`. We
340    // sidestep this by always storing a deep-cloned `Parameters`
341    // (`avcodec_parameters_copy` produces an owner-free copy), so
342    // the `FfmpegVideoStreamDecoder`'s `Send` reachability never
343    // depends on the caller's owner discipline.
344    //
345    // Use `try_clone_parameters` instead of `Parameters::clone` —
346    // ffmpeg-next's `clone` calls `Parameters::new()` which can
347    // return a `Parameters` whose inner pointer is null on OOM
348    // (`avcodec_parameters_alloc` returns null without indication);
349    // the subsequent `avcodec_parameters_copy` against that null
350    // destination is C UB. Our checked helper surfaces the OOM as
351    // an error instead.
352    let owned_parameters = try_clone_parameters(&parameters, limits.max_codec_parameter_bytes())?;
353    let hw_scratch = Frame::empty()?;
354    let sw_scratch = alloc_av_video_frame()?;
355    let state = match VideoDecoder::open_with_frame_limits(
356      try_clone_parameters(&owned_parameters, limits.max_codec_parameter_bytes())?,
357      limits,
358    ) {
359      Ok(hw) => DecodeState::Hw(Box::new(hw)),
360      Err(Error::AllBackendsFailed(_)) => {
361        // Open-time HW exhaustion: no rescued packets (open didn't
362        // see any). Just open SW directly from our owned copy.
363        let sw = open_sw_decoder(&owned_parameters, limits)?;
364        DecodeState::Sw(sw)
365      }
366      Err(other) => return Err(other),
367    };
368    Ok(Self {
369      state,
370      parameters: owned_parameters,
371      hw_scratch,
372      sw_scratch,
373      sw_replay_frames: VecDeque::new(),
374      eof_sent: false,
375      degraded_resync_pending: false,
376      degraded_keyframe_seen: false,
377      degraded_packets_since_fallback: 0,
378      time_base,
379      limits,
380      scratch_pending: false,
381      _carrier: core::marker::PhantomData,
382    })
383  }
384
385  /// Returns `true` when this decoder has fallen back to the software
386  /// path. `false` while still on the HW probe (the initial state).
387  #[cfg_attr(not(tarpaulin), inline(always))]
388  pub(crate) const fn is_software_impl(&self) -> bool {
389    matches!(self.state, DecodeState::Sw(_))
390  }
391
392  /// Returns `true` while the HW probe is still active.
393  #[cfg_attr(not(tarpaulin), inline(always))]
394  pub(crate) const fn is_hardware_impl(&self) -> bool {
395    matches!(self.state, DecodeState::Hw(_))
396  }
397
398  /// Borrow the inner [`VideoDecoder`] when this decoder is still on the
399  /// real HW path. Returns `None` after the SW fallback has fired (or, in
400  /// tests, when the HW seam is a fake rather than a real decoder).
401  #[cfg_attr(not(tarpaulin), inline(always))]
402  pub(crate) fn hardware_inner_impl(&self) -> Option<&VideoDecoder> {
403    match &self.state {
404      DecodeState::Hw(hw) => hw.as_video_decoder(),
405      DecodeState::Sw(_) => None,
406    }
407  }
408
409  /// Returns the time base associated with the source stream.
410  #[cfg_attr(not(tarpaulin), inline(always))]
411  pub(crate) const fn time_base_impl(&self) -> Timebase {
412    self.time_base
413  }
414
415  /// Internal: **probe-era** transition from HW to SW. Replays the rescued
416  /// packets (the inner decoder's buffered history, already accepted by the HW
417  /// probe but not yet decoded) through the new SW decoder so the stream resumes
418  /// seamlessly. No frame was delivered on the HW path yet, so replaying the
419  /// history is lossless.
420  ///
421  /// Only the probe-era branches drive this. The **post-commit** path does
422  /// *not* — it retains and reconstructs zero frames, opening SW cold via
423  /// [`Self::degrade_to_sw`] and resyncing at the next keyframe instead of
424  /// replaying. (That is why this method's replay/drain machinery — and the
425  /// finding that the in-transaction drain doesn't cover later frame
426  /// *conversion* — cannot affect the post-commit path: it never produces a
427  /// post-commit replay frame to convert.)
428  ///
429  /// **Transactional**: drained replay frames accumulate in a local
430  /// queue; we only commit them to `self.sw_replay_frames` and switch
431  /// `self.state` to `Sw` after the replay (and EOF re-forwarding, if
432  /// needed) succeed. On failure, the SW decoder, the local frame
433  /// queue, and (where reachable) any consumed packets are dropped —
434  /// `self` is left in its prior state.
435  ///
436  /// **EOF-aware**: when EOF was already accepted on the HW path
437  /// (`self.eof_sent`), the new SW decoder also receives `send_eof()`
438  /// after replay. Without this, codecs that delay tail frames hang
439  /// forever in the drain phase.
440  ///
441  /// **EAGAIN-aware**: if SW's `send_packet` returns EAGAIN during
442  /// replay, drain produced frames into the local queue and retry.
443  ///
444  /// `eof_pending` is passed as a **local** argument rather than read from
445  /// `self.eof_sent`: callers must not mutate `self.eof_sent` before this
446  /// transaction commits (see [`VideoStreamDecoder::send_eof`]), so the
447  /// in-transaction SW EOF re-forward keys off the local flag and `self`'s
448  /// EOF state is updated only after a clean commit.
449  fn fall_back_to_sw(
450    &mut self,
451    unconsumed_packets: std::vec::Vec<ffmpeg_next::Packet>,
452    eof_pending: bool,
453  ) -> Result<(), Error> {
454    tracing::info!(
455      packets_replayed = unconsumed_packets.len(),
456      eof_pending,
457      "mediadecode-ffmpeg: HW probe exhausted, falling back to software decode",
458    );
459    // Wrap the internal worker so any failure path returns the
460    // rescued packets to the caller via `Error::FallbackFailed`.
461    // Without this, non-seekable streams (live feeds, pipes) would
462    // lose every compressed byte the HW path had consumed when a
463    // fallback transition fails partway.
464    match self.fall_back_to_sw_inner(&unconsumed_packets, eof_pending) {
465      Ok(()) => Ok(()),
466      Err(source) => Err(Error::FallbackFailed(FallbackFailed::new(
467        Box::new(source),
468        unconsumed_packets,
469      ))),
470    }
471  }
472
473  /// Worker for [`Self::fall_back_to_sw`]. Returns the rescued packets
474  /// untouched on the borrowed slice; the wrapper takes ownership of
475  /// them and surfaces them in `FallbackFailed` if this returns Err.
476  fn fall_back_to_sw_inner(
477    &mut self,
478    unconsumed_packets: &[ffmpeg_next::Packet],
479    eof_pending: bool,
480  ) -> Result<(), Error> {
481    let mut sw = open_sw_decoder(&self.parameters, self.limits)?;
482    // Bound before the decoder is mutably borrowed, so the error
483    // closures below can still consult it.
484    let sw_state = sw.state();
485    let mut local_replay: VecDeque<frame::Video> = VecDeque::new();
486    // Helper: drain SW into the local replay queue, capped at
487    // `SW_REPLAY_FRAME_CAP`.
488    //
489    // Error discipline: stop the drain **only** on the transient
490    // backpressure signals EAGAIN / EOF (the decoder has no more output for
491    // now). Every other `ffmpeg_next::Error` — e.g. `InvalidData` from a
492    // corrupt replayed packet — is a real decode failure and is propagated,
493    // so a non-recoverable error surfaces as `FallbackFailed` (carrying the
494    // replay packets) instead of being silently swallowed and the fallback
495    // committed over corruption.
496    fn drain_into(
497      sw: &mut ffmpeg_next::decoder::Video,
498      state: *const crate::ffi::CallbackState,
499      local_replay: &mut VecDeque<frame::Video>,
500    ) -> std::result::Result<(), Error> {
501      loop {
502        let mut tmp = alloc_av_video_frame()?;
503        match sw.receive_frame(&mut tmp) {
504          Ok(()) => {
505            if local_replay.len() >= SW_REPLAY_FRAME_CAP {
506              tracing::error!(
507                cap = SW_REPLAY_FRAME_CAP,
508                "mediadecode-ffmpeg: SW fallback replay produced more frames than the \
509                 replay cap allows; aborting fallback (no frames dropped — they're \
510                 still in the SW decoder's internal queue and will be released when \
511                 it drops)",
512              );
513              return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
514                errno: libc::ENOMEM,
515              }));
516            }
517            local_replay.push_back(tmp);
518          }
519          // EAGAIN / EOF: no more output for now — stop draining, success.
520          Err(ffmpeg_next::Error::Other { errno }) if errno == ffmpeg_next::error::EAGAIN => {
521            break;
522          }
523          Err(ffmpeg_next::Error::Eof) => break,
524          // Any other error is a genuine decode failure on a replayed
525          // packet — surface it so it is not masked as a clean fallback.
526          Err(other) => return Err(crate::decoder::software_exit(state, other)),
527        }
528      }
529      Ok(())
530    }
531
532    for pkt in unconsumed_packets {
533      let mut attempts: u32 = 0;
534      loop {
535        match sw.send_packet(pkt) {
536          Ok(()) => break,
537          Err(ffmpeg_next::Error::Other { errno }) if errno == ffmpeg_next::error::EAGAIN => {
538            drain_into(&mut sw, sw_state, &mut local_replay)?;
539            attempts += 1;
540            if attempts > 16 {
541              return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
542                errno: ffmpeg_next::error::EAGAIN,
543              }));
544            }
545          }
546          Err(other) => return Err(crate::decoder::software_exit(sw_state, other)),
547        }
548      }
549    }
550    // Re-forward EOF if the HW path already saw it. SW EOF can also
551    // return EAGAIN until prior output is drained — mirror the
552    // packet-replay loop.
553    if eof_pending {
554      let mut attempts: u32 = 0;
555      loop {
556        match sw.send_eof() {
557          Ok(()) => break,
558          Err(ffmpeg_next::Error::Other { errno }) if errno == ffmpeg_next::error::EAGAIN => {
559            drain_into(&mut sw, sw_state, &mut local_replay)?;
560            attempts += 1;
561            if attempts > 16 {
562              return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
563                errno: ffmpeg_next::error::EAGAIN,
564              }));
565            }
566          }
567          Err(other) => return Err(crate::decoder::software_exit(sw_state, other)),
568        }
569      }
570    }
571    // Final drain BEFORE commit — the transactional commit boundary. The
572    // EAGAIN-triggered drains above only fire when SW exerts backpressure mid
573    // replay; a SW decoder that ACCEPTS every replayed packet (and the EOF)
574    // without one then surfaces a non-transient error — `InvalidData` from a
575    // corrupt replayed packet, or any other decode failure — only on the *next*
576    // `receive_frame`. Without this drain that error would land after the
577    // commit (frames appended, `state` flipped to `Sw`, rescued packets
578    // dropped) and reach the caller as a plain decode failure, not
579    // `FallbackFailed` — breaking probe-era recovery on non-seekable input.
580    // Draining to EAGAIN/EOF here forces any such error to surface now, so it is
581    // wrapped as `FallbackFailed` (retaining the rescued packets) and the
582    // decoder stays on HW — nothing is committed. (Only the probe-era path
583    // reaches this; the post-commit path degrades via `degrade_to_sw` and never
584    // replays, so it has no drained frames to commit or convert.)
585    drain_into(&mut sw, sw_state, &mut local_replay)?;
586    // Commit: only after replay, any EOF forwarding, AND the final drain
587    // succeeded do we move the new SW decoder and queue into `self`.
588    self.sw_replay_frames.append(&mut local_replay);
589    self.state = DecodeState::Sw(sw);
590    Ok(())
591  }
592
593  /// **Post-commit** degrade-and-continue transition: open the SW decoder
594  /// **cold** and forward only the failure-arm's input, retaining and
595  /// reconstructing **zero** frames. This is the whole post-commit path: open
596  /// SW, forward the current packet (or EOF), degrade-track — nothing is drained
597  /// into `sw_replay_frames`, so there is no replayed frame to convert later and
598  /// no terminal-drain transaction to reason about. SW naturally produces no
599  /// frame until the next keyframe arrives across the gap, then decodes normally;
600  /// the failure-point→next-keyframe span is the accepted, logged drop.
601  ///
602  /// **Transactional (SW-open only)**: `self.state` flips to `Sw` *only after*
603  /// `open_sw_decoder` and the input forward succeed. On any failure the new SW
604  /// decoder is dropped and the decoder is left on its prior HW state, the error
605  /// surfaced as [`Error::FallbackFailed`] (with an empty rescue set — a
606  /// post-commit failure never carries unconsumed packets). With no replay-frame
607  /// retention there is nothing else to roll back.
608  ///
609  /// On a clean commit it enters degraded-resync mode (see
610  /// [`Self::enter_degraded_resync`]); if the forwarded current packet is itself
611  /// a keyframe, the resync anchor is recorded immediately
612  /// ([`Self::note_degraded_keyframe`]).
613  fn degrade_to_sw(&mut self, input: PostCommitInput<'_>) -> Result<(), Error> {
614    match self.degrade_to_sw_inner(input) {
615      Ok(()) => Ok(()),
616      // **A budget refusal is not a fallback failure.** It travels
617      // unwrapped, and the spelling was chosen rather than inherited:
618      //
619      // * `FallbackFailed` means the fallback *machinery* could not
620      //   complete, and its contract is to hand back the unconsumed
621      //   packets so a caller can re-drive them. On this road that set
622      //   is empty by construction — the probe buffer is gone and no
623      //   replay frames are retained — so the envelope carries no
624      //   recovery affordance at all, only a label.
625      // * And the label is the wrong one. Re-driving is the natural
626      //   response to a fallback failure, and re-driving a budget
627      //   refusal under the same limits refuses identically. Naming it
628      //   a fallback failure invites an action that cannot succeed,
629      //   while `FrameBudgetExceeded` names the one that can: raise
630      //   the ceiling, or accept the refusal.
631      //
632      // So it keeps the same spelling here as on every other road. One
633      // fact, one name.
634      Err(budget @ Error::FrameBudgetExceeded(_)) => Err(budget),
635      // Everything else really is the machinery failing, and keeps the
636      // envelope — empty rescue set and all, which is what a
637      // post-commit failure has to hand back.
638      Err(source) => Err(Error::FallbackFailed(FallbackFailed::new(
639        Box::new(source),
640        std::vec::Vec::new(),
641      ))),
642    }
643  }
644
645  /// Worker for [`Self::degrade_to_sw`]. Opens SW cold, forwards the arm's input,
646  /// and on success commits + enters degraded-resync mode. Returns `Err` (and
647  /// commits nothing) if SW cannot open or the forward fails.
648  fn degrade_to_sw_inner(&mut self, input: PostCommitInput<'_>) -> Result<(), Error> {
649    let mut sw = open_sw_decoder(&self.parameters, self.limits)?;
650    // Captured before the decoder is borrowed for the forward, and
651    // before it can be dropped on the error road: this temporary
652    // decoder owns the callback state, so a `judge_buffer` refusal
653    // recorded during either forward below dies with it unless the
654    // reason is collected here. That was the last software road still
655    // wrapping libavcodec's `EINVAL` raw.
656    let state = sw.state();
657    let mut forwarded_keyframe = false;
658    let mut forwarded_packet = false;
659    match input {
660      PostCommitInput::Packet(pkt) => {
661        // The HW decoder REFUSED this packet, so it was never decoded; forward
662        // it to the cold SW. A failure here surfaces (it is not silently
663        // dropped) and rolls back to HW.
664        sw.send_packet(pkt)
665          .map_err(|e| crate::decoder::software_exit(state, e))?;
666        forwarded_keyframe = pkt.is_key();
667        forwarded_packet = true;
668      }
669      // Frame-time failure: there is no current packet to forward.
670      PostCommitInput::FrameTime => {}
671      PostCommitInput::Eof => {
672        // EOF was pending on the HW path; the cold SW must also see it so codecs
673        // that delay tail frames don't hang. A cold decoder (no packets sent)
674        // has no buffered output, so this cannot return EAGAIN.
675        sw.send_eof()
676          .map_err(|e| crate::decoder::software_exit(state, e))?;
677      }
678    }
679    // Commit: only after a clean open + forward.
680    self.state = DecodeState::Sw(sw);
681    self.enter_degraded_resync();
682    if forwarded_keyframe {
683      // The refused current packet was itself the resync anchor.
684      self.note_degraded_keyframe(true);
685    }
686    if forwarded_packet {
687      self.count_degraded_packet();
688    }
689    Ok(())
690  }
691
692  /// Enter post-commit degraded mode after a post-commit fallback commits: the
693  /// SW decoder opened cold and the span up to the next keyframe is being
694  /// dropped. We hold this mode until SW proves a *keyframe-anchored* resync
695  /// (a delivered frame after a keyframe was fed — see
696  /// [`Self::note_degraded_keyframe`] / [`Self::resync_on_frame`]) and the EOF
697  /// escalation in [`VideoStreamDecoder::receive_frame`]. Called only on the
698  /// post-commit path, only after a clean commit. Resets the keyframe-seen anchor
699  /// and the gap counter.
700  #[inline]
701  fn enter_degraded_resync(&mut self) {
702    self.degraded_resync_pending = true;
703    self.degraded_keyframe_seen = false;
704    self.degraded_packets_since_fallback = 0;
705  }
706
707  /// Record that a packet fed to the SW decoder across an unresolved post-commit
708  /// gap was a **keyframe** — the resync anchor. Only a frame delivered *after*
709  /// this clears the pending flag, so a lenient codec's concealed P-frame can't
710  /// masquerade as a resync. A no-op outside degraded mode, or for a
711  /// non-keyframe.
712  #[inline]
713  fn note_degraded_keyframe(&mut self, is_key: bool) {
714    if self.degraded_resync_pending && is_key {
715      self.degraded_keyframe_seen = true;
716    }
717  }
718
719  /// Count one packet fed to the SW decoder while a post-commit resync is still
720  /// unproven, so the EOF escalation can quantify the lost tail. A no-op once
721  /// SW has resynced (the flag is clear).
722  #[inline]
723  fn count_degraded_packet(&mut self) {
724    if self.degraded_resync_pending {
725      self.degraded_packets_since_fallback = self.degraded_packets_since_fallback.saturating_add(1);
726    }
727  }
728
729  /// A SW frame was delivered. Clear post-commit degraded mode **only if** a
730  /// keyframe was fed across the gap ([`Self::degraded_keyframe_seen`]) — that is
731  /// a real keyframe-anchored resync, so the dropped span is now the promised
732  /// *bounded* gap. A frame delivered with no keyframe yet (a concealed P-frame
733  /// from the dropped span) leaves the guard set, so the one-GOP bound stays
734  /// enforced and the EOF escalation still fires if no keyframe ever arrives.
735  /// Idempotent; a no-op outside degraded mode (steady state, probe-era replay).
736  #[inline]
737  fn resync_on_frame(&mut self) {
738    if self.degraded_resync_pending && self.degraded_keyframe_seen {
739      self.clear_degraded_resync();
740    }
741  }
742
743  /// Unconditionally reset post-commit degraded-mode state. Used where the gap
744  /// is moot regardless of resync proof: a `flush` (seek/reset re-anchors the
745  /// stream) and the cleanup after an EOF escalation has already fired (so a
746  /// follow-up poll sees plain EOF, not a repeated escalation). The
747  /// frame-delivery path uses the keyframe-gated [`Self::resync_on_frame`]
748  /// instead.
749  #[inline]
750  fn clear_degraded_resync(&mut self) {
751    self.degraded_resync_pending = false;
752    self.degraded_keyframe_seen = false;
753    self.degraded_packets_since_fallback = 0;
754  }
755
756  /// The one place a delivered frame is committed.
757  ///
758  /// Every road that hands a frame to the caller passes through here —
759  /// the hardware scratch, the software scratch, both replay-queue
760  /// entries, and the retry of a parked frame — so the bookkeeping a
761  /// delivery owes cannot be attached to some of them and forgotten on
762  /// others. It was: a parked software frame delivered on the retry
763  /// road skipped [`Self::resync_on_frame`], so the last recovered
764  /// frame of a degraded stream could leave the resync guard standing
765  /// and turn a clean EOF into a false
766  /// [`PostCommitNeverResynced`].
767  fn commit_delivery(
768    &mut self,
769    frame: VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, C::Buffer>,
770    dst: &mut VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, C::Buffer>,
771  ) {
772    // The seat is free once a carrier exists for what it held.
773    self.scratch_pending = false;
774    // A delivered frame is what clears a keyframe-anchored resync. A
775    // no-op on every road that never entered degraded mode, which is
776    // why it can be unconditional here.
777    self.resync_on_frame();
778    *dst = frame;
779  }
780
781  /// Internal: convert the active scratch frame into a
782  /// `mediadecode::VideoFrame` and write into `dst`.
783  fn deliver_frame(
784    &mut self,
785    dst: &mut VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, C::Buffer>,
786  ) -> Result<(), VideoDecodeError> {
787    let av_frame = match &mut self.state {
788      DecodeState::Hw(_) => unsafe { self.hw_scratch.as_inner_mut().as_ptr() },
789      DecodeState::Sw(_) => unsafe { self.sw_scratch.as_ptr() },
790    };
791    // SAFETY: the scratch frame is live — either just filled by the
792    // inner decoder's `receive_frame`, or left holding a frame whose
793    // conversion did not commit. Convert takes what it needs out of it,
794    // so the scratch can be reused once this has committed.
795    let converted = unsafe {
796      convert::av_frame_to_video_frame_as::<C>(av_frame, self.time_base, self.limits.frame())
797    };
798    match converted {
799      Ok(new_frame) => {
800        self.commit_delivery(new_frame, dst);
801        Ok(())
802      }
803      Err(e) => {
804        // Park only what another attempt could survive.
805        self.scratch_pending = e.parks_in_decode();
806        Err(VideoDecodeError::Convert(e))
807      }
808    }
809  }
810}
811
812#[cfg(test)]
813impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierVideoStreamDecoder<C> {
814  /// Build a decoder around an injected HW seam, bypassing the real probe.
815  /// Lets tests drive the post-commit fallback path with a [`HwInner`] fake
816  /// instead of a live GPU. The SW fallback still opens the **real**
817  /// `ffmpeg::decoder::Video` from `parameters`, so a fallback in these tests
818  /// genuinely decodes.
819  pub(crate) fn from_hw_inner_for_test(
820    hw: Box<dyn HwInner>,
821    parameters: Parameters,
822    time_base: Timebase,
823  ) -> Result<Self, Error> {
824    let limits = DecoderLimits::default();
825    let owned_parameters = try_clone_parameters(&parameters, limits.max_codec_parameter_bytes())?;
826    Ok(Self {
827      state: DecodeState::Hw(hw),
828      parameters: owned_parameters,
829      hw_scratch: Frame::empty()?,
830      sw_scratch: alloc_av_video_frame()?,
831      sw_replay_frames: VecDeque::new(),
832      eof_sent: false,
833      degraded_resync_pending: false,
834      degraded_keyframe_seen: false,
835      degraded_packets_since_fallback: 0,
836      time_base,
837      limits,
838      scratch_pending: false,
839      _carrier: core::marker::PhantomData,
840    })
841  }
842
843  /// Whether `send_eof` has been committed on the active decoder. Lets the
844  /// rollback tests assert that a failed EOF fallback restores (never
845  /// half-mutates) `eof_sent`.
846  pub(crate) const fn eof_sent_for_test(&self) -> bool {
847    self.eof_sent
848  }
849
850  /// Whether a post-commit fallback is awaiting a keyframe-anchored resync.
851  /// Lets the escalation tests observe the degraded-resync state machine.
852  pub(crate) const fn degraded_resync_pending_for_test(&self) -> bool {
853    self.degraded_resync_pending
854  }
855
856  /// Whether a keyframe has been fed to the SW decoder across the unresolved
857  /// post-commit gap (the resync anchor). Lets the keyframe-gating test confirm
858  /// a concealed P-frame does not set it (so the resync clear stays blocked).
859  pub(crate) const fn degraded_keyframe_seen_for_test(&self) -> bool {
860    self.degraded_keyframe_seen
861  }
862
863  /// Whether the post-commit path retained any replay frames — must always be
864  /// empty for a post-commit fallback (it retains zero). Lets the finding-1
865  /// dissolution test assert no replay frame was ever queued.
866  pub(crate) fn sw_replay_frames_is_empty_for_test(&self) -> bool {
867    self.sw_replay_frames.is_empty()
868  }
869
870  /// Packets fed to SW across an unresolved post-commit resync gap. Lets the
871  /// counter test confirm packets crossing the gap from the `send_packet` arm
872  /// are tallied (and cleared on resync).
873  pub(crate) const fn degraded_packets_since_fallback_for_test(&self) -> u64 {
874    self.degraded_packets_since_fallback
875  }
876}
877
878impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierVideoStreamDecoder<C> {
879  pub(crate) fn send_packet_impl(
880    &mut self,
881    packet: &VideoPacket<VideoPacketExtra, C::Buffer>,
882  ) -> Result<(), VideoDecodeError> {
883    // **Nothing is sent while a frame is parked.** Both send roads can
884    // commit a hardware-to-software fallback, and a fallback under a
885    // parked frame would leave the retry reading the other scratch. See
886    // [`Self::scratch_pending`].
887    if self.scratch_pending {
888      return Err(VideoDecodeError::FramePending);
889    }
890    // Scoped submission: the rebuilt `AVPacket` never leaves this call,
891    // which is what lets the view lane share its buffer with libavcodec
892    // rather than copy into it. See `boundary::with_ffmpeg_video_packet`.
893    let limits = self.limits.packet_limits();
894    // **The route depends on what this decoder does with what it is
895    // sent.** While the hardware probe is open it `av_packet_ref`s
896    // every accepted packet into a rescue history, and
897    // `AllBackendsFailed::into_unconsumed_packets` hands those out as
898    // owned, mutable `Packet`s — so a shared body would escape this
899    // call as a live mutable alias of a carrier the caller may still be
900    // reading. Inside that window the body is copied; once the probe
901    // has committed, nothing is recorded and the send is zero-copy
902    // again. The software road never records.
903    let route = match &self.state {
904      DecodeState::Hw(hw) if hw.records_submissions() => crate::carrier::BodyRoute::Copy,
905      _ => crate::carrier::BodyRoute::Submission,
906    };
907    boundary::with_ffmpeg_video_packet::<C, _>(packet, limits, route, |av_pkt| {
908      match &mut self.state {
909        DecodeState::Hw(hw) => match hw.send_packet(av_pkt) {
910          Ok(()) => Ok(()),
911          Err(Error::AllBackendsFailed(p)) => {
912            // Route on the EXPLICIT origin, never on whether `rescued` is empty (a
913            // probe-era first-packet cap trip is *also* empty).
914            if p.origin().is_post_commit() {
915              // Post-commit: DEGRADE AND CONTINUE. No lossless mid-stream
916              // reconstruction — the SW decoder opens cold, retains zero replay
917              // frames, and resyncs at the next keyframe. The current packet (the
918              // one HW REFUSED) is forwarded to that cold SW: if it is the resync
919              // keyframe SW decodes from it, otherwise SW drops it until a keyframe
920              // arrives. The bounded span from here to that keyframe is dropped — a
921              // loudly logged gap (see the `warn!`), not a silent one.
922              tracing::warn!(
923                backend = ?p.attempts().last().map(|(b, _)| *b),
924                pts = ?av_pkt.pts(),
925                "mediadecode-ffmpeg: HW decode failed post-commit; falling back to \
926                 software, resyncing at next keyframe — a bounded span of frames \
927                 may be dropped at this boundary",
928              );
929              // Transactional SW-open + current-packet forward; degrade-tracking
930              // (incl. keyframe-anchor recording) happens inside on a clean commit.
931              // A failure surfaces `FallbackFailed` and stays on HW.
932              return self
933                .degrade_to_sw(PostCommitInput::Packet(av_pkt))
934                .map_err(VideoDecodeError::Decode);
935            }
936            // Probe-era: replay the inner decoder's buffered history (lossless —
937            // no frame was delivered yet), then forward the still-unconsumed
938            // current packet to SW.
939            let rescued = p.into_unconsumed_packets();
940            // `eof_pending` is the committed EOF state — never pre-mutated here.
941            let eof_pending = self.eof_sent;
942            self
943              .fall_back_to_sw(rescued, eof_pending)
944              .map_err(VideoDecodeError::Decode)?;
945            // Forward the new (still-unconsumed) current packet to the
946            // freshly-opened SW decoder — the HW decoder REFUSED it, so it was not
947            // in the replay set. A failure here surfaces (it is not silently
948            // dropped).
949            if let DecodeState::Sw(sw) = &mut self.state {
950              let st = sw.state();
951              sw.send_packet(av_pkt)
952                .map_err(|e| VideoDecodeError::Decode(crate::decoder::software_exit(st, e)))?;
953            }
954            Ok(())
955          }
956          Err(other) => Err(VideoDecodeError::Decode(other)),
957        },
958        DecodeState::Sw(sw) => {
959          let st = sw.state();
960          sw.send_packet(av_pkt)
961            .map_err(|e| VideoDecodeError::Decode(crate::decoder::software_exit(st, e)))?;
962          // A keyframe fed across an unresolved post-commit gap is the resync
963          // anchor; record it so the next delivered frame can clear the guard.
964          self.note_degraded_keyframe(av_pkt.is_key());
965          // Count packets crossing an unresolved post-commit resync gap so the
966          // escalation at EOF can report how much tail was lost.
967          self.count_degraded_packet();
968          Ok(())
969        }
970      }
971    })
972    .map_err(|e| VideoDecodeError::Decode(Error::PacketBuild(e)))?
973  }
974
975  pub(crate) fn receive_frame_impl(
976    &mut self,
977    dst: &mut VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, C::Buffer>,
978  ) -> Result<(), VideoDecodeError> {
979    // Deliver any frames produced during SW fallback replay before
980    // pulling new ones from the SW decoder. This is the queue
981    // populated by `fall_back_to_sw` when SW returned EAGAIN during
982    // packet replay — a **probe-era** path only (the post-commit path retains
983    // no replay frames), so `resync_on_frame` here is a no-op (probe-era never
984    // enters degraded mode).
985    // **Peeked, not popped.** A replayed frame is the rescue history's
986    // only copy: popping it before the conversion committed lost it to
987    // any allocation failure, which is the one thing this queue exists
988    // to prevent. It leaves the queue when a carrier exists for it.
989    if let Some(replayed) = self.sw_replay_frames.front() {
990      // SAFETY: `replayed` is a live AVFrame owned by this queue;
991      // convert takes what it needs out of it.
992      let converted = unsafe {
993        convert::av_frame_to_video_frame_as::<C>(
994          replayed.as_ptr(),
995          self.time_base,
996          self.limits.frame(),
997        )
998      };
999      let new_frame = match converted {
1000        Ok(new_frame) => new_frame,
1001        Err(e) if e.parks_in_decode() => return Err(VideoDecodeError::Convert(e)),
1002        // A frame nothing can carry is dropped rather than re-offered
1003        // forever — the same rule the scratch seat follows.
1004        Err(e) => {
1005          self.sw_replay_frames.pop_front();
1006          return Err(VideoDecodeError::Convert(e));
1007        }
1008      };
1009      self.sw_replay_frames.pop_front();
1010      self.commit_delivery(new_frame, dst);
1011      return Ok(());
1012    }
1013    // A frame whose conversion did not commit is converted again before
1014    // the decoder is asked for another — see [`Self::scratch_pending`].
1015    // The scratch still holds it, and `deliver_frame` reads whichever
1016    // scratch the current state uses.
1017    if self.scratch_pending {
1018      return self.deliver_frame(dst);
1019    }
1020    loop {
1021      match &mut self.state {
1022        DecodeState::Hw(hw) => match hw.receive_frame(&mut self.hw_scratch) {
1023          Ok(()) => {
1024            // The frame is out of the decoder's queue from here; the
1025            // seat is what keeps it if the conversion cannot commit.
1026            self.scratch_pending = true;
1027            return self.deliver_frame(dst);
1028          }
1029          Err(Error::AllBackendsFailed(p)) => {
1030            // HW exhausted at frame-time. There is no current packet here.
1031            // Route on the explicit origin.
1032            if p.origin().is_post_commit() {
1033              // Post-commit: DEGRADE AND CONTINUE — open SW cold (no current
1034              // packet to forward, no replay frames retained) and resync at the
1035              // next keyframe, dropping the bounded span up to it. Loud single
1036              // `warn!` marks that accepted gap. A clean commit enters degraded
1037              // mode; a SW-open failure surfaces `FallbackFailed` and stays HW.
1038              tracing::warn!(
1039                backend = ?p.attempts().last().map(|(b, _)| *b),
1040                "mediadecode-ffmpeg: HW decode failed post-commit at frame-time; \
1041                 falling back to software, resyncing at next keyframe — a bounded \
1042                 span of frames may be dropped at this boundary",
1043              );
1044              self
1045                .degrade_to_sw(PostCommitInput::FrameTime)
1046                .map_err(VideoDecodeError::Decode)?;
1047              // Nothing to deliver yet — fall through to the loop; the next
1048              // iteration takes the Sw arm and pulls from the cold SW decoder.
1049              continue;
1050            }
1051            // Probe-era: replay the buffered history (lossless).
1052            let rescued = p.into_unconsumed_packets();
1053            // `eof_pending` is the committed EOF state — never pre-mutated here.
1054            let eof_pending = self.eof_sent;
1055            self
1056              .fall_back_to_sw(rescued, eof_pending)
1057              .map_err(VideoDecodeError::Decode)?;
1058            // If the replay produced any drained frames, return one
1059            // immediately — preserves stream order vs. whatever the
1060            // SW decoder will produce next.
1061            // **Peeked, not popped** — the second delivery path onto
1062            // this queue, and it owes the same discipline as the first
1063            // (see the head of `receive_frame_impl`). The replay queue
1064            // is the rescue history's only copy of these frames, so a
1065            // conversion that cannot commit must leave the head where
1066            // it is rather than advance past it.
1067            if let Some(replayed) = self.sw_replay_frames.front() {
1068              // SAFETY: `replayed` is a live AVFrame owned by this
1069              // queue; convert takes what it needs out of it.
1070              let converted = unsafe {
1071                convert::av_frame_to_video_frame_as::<C>(
1072                  replayed.as_ptr(),
1073                  self.time_base,
1074                  self.limits.frame(),
1075                )
1076              };
1077              let new_frame = match converted {
1078                Ok(new_frame) => new_frame,
1079                Err(e) if e.parks_in_decode() => return Err(VideoDecodeError::Convert(e)),
1080                // A frame nothing can carry is dropped rather than
1081                // re-offered forever.
1082                Err(e) => {
1083                  self.sw_replay_frames.pop_front();
1084                  return Err(VideoDecodeError::Convert(e));
1085                }
1086              };
1087              self.sw_replay_frames.pop_front();
1088              self.commit_delivery(new_frame, dst);
1089              return Ok(());
1090            }
1091            // Fall through to the loop; next iteration takes the Sw arm.
1092          }
1093          Err(other) => return Err(VideoDecodeError::Decode(other)),
1094        },
1095        DecodeState::Sw(sw) => {
1096          // Convert inline (rather than via `deliver_frame`, which borrows all
1097          // of `self`) so only the disjoint fields `sw_scratch` / `time_base`
1098          // are touched alongside the `self.state` borrow `sw` holds.
1099          let st = sw.state();
1100          match sw.receive_frame(&mut self.sw_scratch) {
1101            Ok(()) => {
1102              // The frame is out of the decoder's queue from here; the
1103              // seat is what keeps it if the conversion cannot commit.
1104              self.scratch_pending = true;
1105              // SAFETY: the scratch frame is live (just filled by
1106              // `receive_frame`); convert takes what it needs out of
1107              // it, so the scratch can be reused once this commits.
1108              let converted = unsafe {
1109                convert::av_frame_to_video_frame_as::<C>(
1110                  self.sw_scratch.as_ptr(),
1111                  self.time_base,
1112                  self.limits.frame(),
1113                )
1114              };
1115              let new_frame = match converted {
1116                Ok(new_frame) => new_frame,
1117                Err(e) => {
1118                  self.scratch_pending = e.parks_in_decode();
1119                  return Err(VideoDecodeError::Convert(e));
1120                }
1121              };
1122              // SW produced a frame. The commit point clears degraded mode only
1123              // if a keyframe was fed across the gap — a real keyframe-anchored
1124              // resync, so the dropped span is the promised bounded gap. A
1125              // concealed P-frame (no keyframe yet) does not clear it (see
1126              // `resync_on_frame`).
1127              self.commit_delivery(new_frame, dst);
1128              return Ok(());
1129            }
1130            // EOF while a post-commit resync is still unproven: SW never emitted
1131            // a frame between the fallback and end-of-stream, so no keyframe
1132            // arrived across the gap and the ENTIRE tail was lost — not the
1133            // bounded span the degrade-and-continue path promises. Escalate
1134            // loudly with a distinct error instead of surfacing a clean `Eof`
1135            // that would silently swallow the tail. (Resync clears the flag, so
1136            // a normal degraded-then-recovered stream reaches EOF with the flag
1137            // already clear and takes the plain `Eof` path below.)
1138            Err(ffmpeg_next::Error::Eof) if self.degraded_resync_pending => {
1139              let packets_lost = self.degraded_packets_since_fallback;
1140              tracing::error!(
1141                packets_lost,
1142                "mediadecode-ffmpeg: post-commit HW->SW fallback never resynced before EOF — \
1143                 {packets_lost} packets fed to the software decoder produced no frame (no \
1144                 keyframe found across the gap); the stream tail from the fallback point was \
1145                 lost",
1146              );
1147              // Clear so a subsequent `receive_frame` poll (callers often drain
1148              // to EOF) sees plain EOF, not a repeated escalation.
1149              self.clear_degraded_resync();
1150              return Err(VideoDecodeError::PostCommitNeverResynced(
1151                PostCommitNeverResynced::new(packets_lost),
1152              ));
1153            }
1154            Err(e) => {
1155              return Err(VideoDecodeError::Decode(crate::decoder::software_exit(
1156                st, e,
1157              )));
1158            }
1159          }
1160        }
1161      }
1162    }
1163  }
1164
1165  pub(crate) fn send_eof_impl(&mut self) -> Result<(), VideoDecodeError> {
1166    // As `send_packet`: EOF can commit a fallback too, and the escalation
1167    // it may raise reads the resync standing a parked frame has not yet
1168    // had the chance to clear.
1169    if self.scratch_pending {
1170      return Err(VideoDecodeError::FramePending);
1171    }
1172    let outcome = match &mut self.state {
1173      DecodeState::Hw(hw) => match hw.send_eof() {
1174        Ok(()) => Ok(()),
1175        Err(Error::AllBackendsFailed(p)) => {
1176          // EOF is pending for this transaction, so the SW decoder must also
1177          // receive `send_eof` (codecs that delay tail frames hang otherwise).
1178          // We pass that intent locally rather than pre-setting `self.eof_sent`:
1179          // a fallback that fails returns `FallbackFailed` and stays on HW, and a
1180          // half-mutated `self.eof_sent = true` would then make a *later*
1181          // fallback inject an EOF into SW even though this `send_eof` errored.
1182          // `self.eof_sent` is committed only after the whole operation succeeds
1183          // (the `outcome` check below), keeping the fallback all-or-nothing.
1184          if p.origin().is_post_commit() {
1185            // Post-commit: DEGRADE AND CONTINUE — open SW cold, re-forward EOF
1186            // (no current packet, no replay frames). The cold SW produces no
1187            // frame from EOF alone, so the drain-to-EOF in `receive_frame`
1188            // escalates (`PostCommitNeverResynced`) unless a later keyframe-fed
1189            // poll resyncs first. A clean commit enters degraded mode; a SW-open
1190            // failure surfaces `FallbackFailed` and stays HW.
1191            tracing::warn!(
1192              backend = ?p.attempts().last().map(|(b, _)| *b),
1193              "mediadecode-ffmpeg: HW decode failed post-commit at EOF; falling \
1194               back to software — a bounded span of tail frames may be dropped",
1195            );
1196            self
1197              .degrade_to_sw(PostCommitInput::Eof)
1198              .map_err(VideoDecodeError::Decode)
1199          } else {
1200            // Probe-era: replay the buffered history (lossless), re-forwarding
1201            // EOF inside the transaction.
1202            let rescued = p.into_unconsumed_packets();
1203            self
1204              .fall_back_to_sw(rescued, true)
1205              .map_err(VideoDecodeError::Decode)
1206          }
1207        }
1208        Err(other) => Err(VideoDecodeError::Decode(other)),
1209      },
1210      DecodeState::Sw(sw) => {
1211        let st = sw.state();
1212        sw.send_eof()
1213          .map_err(|e| VideoDecodeError::Decode(crate::decoder::software_exit(st, e)))
1214      }
1215    };
1216    // Commit EOF state only on success — a failed fallback left `self.eof_sent`
1217    // untouched (restored-by-construction: we never mutated it), so HW stays
1218    // EOF-not-yet-sent and a retry behaves correctly.
1219    if outcome.is_ok() {
1220      self.eof_sent = true;
1221    }
1222    outcome
1223  }
1224
1225  pub(crate) fn flush_impl(&mut self) -> Result<(), VideoDecodeError> {
1226    // Drop any frames buffered during SW fallback replay before
1227    // flushing the inner decoder — otherwise a seek/reset would
1228    // surface stale pre-flush frames on the next `receive_frame`.
1229    self.sw_replay_frames.clear();
1230    // And a parked frame belongs to the position being abandoned.
1231    self.scratch_pending = false;
1232    // Flush ends the drain phase; the decoder accepts new packets
1233    // after this, so reset EOF tracking.
1234    self.eof_sent = false;
1235    // A flush (seek/reset) re-anchors the stream — any in-flight post-commit
1236    // resync tracking from before the flush is moot. Clear it so the next EOF
1237    // doesn't escalate over a now-irrelevant pre-flush gap.
1238    self.clear_degraded_resync();
1239    match &mut self.state {
1240      // The HW seam's `flush` returns `Result` for a uniform trait; the
1241      // real `VideoDecoder::flush` is infallible (always `Ok`).
1242      DecodeState::Hw(hw) => hw.flush().map_err(VideoDecodeError::Decode)?,
1243      DecodeState::Sw(sw) => sw.flush(),
1244    }
1245    Ok(())
1246  }
1247}
1248
1249macro_rules! video_lane_face {
1250  ($($lane:ty),+ $(,)?) => { $(
1251    impl CarrierVideoStreamDecoder<$lane> {
1252      /// Opens a video decoder for `parameters`, probing hardware
1253      /// backends in order and falling back to software.
1254      pub fn open(
1255        parameters: Parameters,
1256        time_base: Timebase,
1257        limits: DecoderLimits,
1258      ) -> Result<Self, Error> {
1259        Self::open_impl(parameters, time_base, limits)
1260      }
1261
1262      /// Whether this decoder is currently running on software.
1263      pub const fn is_software(&self) -> bool {
1264        self.is_software_impl()
1265      }
1266
1267      /// Whether this decoder is currently running on hardware.
1268      pub const fn is_hardware(&self) -> bool {
1269        self.is_hardware_impl()
1270      }
1271
1272      /// The hardware wrapper, when one is in use.
1273      pub fn hardware_inner(&self) -> Option<&VideoDecoder> {
1274        self.hardware_inner_impl()
1275      }
1276
1277      /// The stream timebase every produced timestamp is stamped with.
1278      pub const fn time_base(&self) -> Timebase {
1279        self.time_base_impl()
1280      }
1281    }
1282
1283    impl VideoStreamDecoder for CarrierVideoStreamDecoder<$lane> {
1284      type Adapter = Ffmpeg;
1285      type Buffer = <$lane as crate::FfmpegCarrier>::Buffer;
1286      type Error = VideoDecodeError;
1287
1288      fn send_packet(
1289        &mut self,
1290        packet: &VideoPacket<VideoPacketExtra, Self::Buffer>,
1291      ) -> Result<(), Self::Error> {
1292        self.send_packet_impl(packet)
1293      }
1294
1295      fn receive_frame(
1296        &mut self,
1297        dst: &mut VideoFrame<mediadecode::PixelFormat, VideoFrameExtra, Self::Buffer>,
1298      ) -> Result<(), Self::Error> {
1299        self.receive_frame_impl(dst)
1300      }
1301
1302      fn send_eof(&mut self) -> Result<(), Self::Error> {
1303        self.send_eof_impl()
1304      }
1305
1306      fn flush(&mut self) -> Result<(), Self::Error> {
1307        self.flush_impl()
1308      }
1309    }
1310  )+ };
1311}
1312
1313video_lane_face!(crate::View, crate::Owned);
1314
1315fn open_sw_decoder(parameters: &Parameters, limits: DecoderLimits) -> Result<SwDecoder, Error> {
1316  // Use the checked codec-context builder — ffmpeg-next's
1317  // `Context::from_parameters` calls `Context::new()` which doesn't
1318  // null-check `avcodec_alloc_context3`'s return value before
1319  // running `avcodec_parameters_to_context` against it. Under
1320  // memory pressure that's C-level UB; `build_codec_context`
1321  // surfaces the OOM as an error instead.
1322  let (ctx, callback_state) = build_codec_context(parameters, limits)?;
1323  // Opened without forming a bindgen enum from FFmpeg memory: the codec
1324  // is resolved off a raw `codec_id`, and the medium is proved off a raw
1325  // `codec_type`. See `crate::decoder::ensure_codec_type`.
1326  let codec = crate::decoder::find_decoder(parameters)?;
1327  let opened = ctx.decoder().open_as(codec).map_err(Error::Ffmpeg)?;
1328  crate::decoder::ensure_video_codec_type(&opened)?;
1329  Ok(SwDecoder {
1330    decoder: ffmpeg_next::decoder::Video(opened),
1331    _callback_state: callback_state,
1332  })
1333}
1334
1335/// Payload for [`VideoDecodeError::PostCommitNeverResynced`].
1336///
1337/// A **post-commit** HW->SW fallback degraded the stream (dropping the
1338/// bounded span up to the next keyframe) but the software decoder
1339/// reached EOF without ever producing a frame — it never resynced, so
1340/// the entire tail from the failure point was lost. The "bounded,
1341/// logged gap" the post-commit path promises did not materialise (no
1342/// keyframe arrived before EOF), so the loss is surfaced loudly here
1343/// instead of being silently swallowed as a clean end-of-stream.
1344#[derive(thiserror::Error, Debug)]
1345#[error(
1346  "post-commit HW->SW fallback never resynced before EOF: {packets_lost} packets fed to the \
1347   software decoder produced no frame (no keyframe found across the gap) — the stream tail \
1348   from the fallback point was lost"
1349)]
1350pub struct PostCommitNeverResynced {
1351  packets_lost: u64,
1352}
1353
1354impl PostCommitNeverResynced {
1355  /// Constructs a `PostCommitNeverResynced` payload.
1356  #[inline]
1357  pub const fn new(packets_lost: u64) -> Self {
1358    Self { packets_lost }
1359  }
1360  /// Packets fed to the software decoder across the unresolved resync
1361  /// gap.
1362  #[inline]
1363  pub const fn packets_lost(&self) -> u64 {
1364    self.packets_lost
1365  }
1366}
1367
1368/// Error type for [`FfmpegVideoStreamDecoder`].
1369#[derive(thiserror::Error, Debug, IsVariant, Unwrap, TryUnwrap)]
1370#[unwrap(ref, ref_mut)]
1371#[try_unwrap(ref, ref_mut)]
1372pub enum VideoDecodeError {
1373  /// The wrapped decoder (HW or SW) reported an error.
1374  #[error(transparent)]
1375  Decode(#[from] Error),
1376  /// Frame conversion from FFmpeg's native types to mediadecode's
1377  /// types failed.
1378  #[error(transparent)]
1379  Convert(#[from] ConvertError),
1380  /// A **post-commit** HW->SW fallback degraded the stream but the
1381  /// software decoder reached EOF without ever producing a frame.
1382  #[error(transparent)]
1383  PostCommitNeverResynced(#[from] PostCommitNeverResynced),
1384  /// A decoded frame is parked, waiting for a conversion that can
1385  /// commit, and nothing may be sent until it has been delivered.
1386  ///
1387  /// The answer is [`VideoStreamDecoder::receive_frame`]: it re-attempts
1388  /// the parked frame. A caller who would rather abandon it calls
1389  /// `flush`, which clears the seat with the rest of the decoder's
1390  /// position.
1391  ///
1392  /// [`VideoStreamDecoder::receive_frame`]:
1393  ///   mediadecode::decoder::VideoStreamDecoder::receive_frame
1394  #[error(
1395    "a decoded frame is parked and must be delivered before another packet is sent \
1396     (call `receive_frame`, or `flush` to abandon it)"
1397  )]
1398  FramePending,
1399}
1400
1401#[cfg(test)]
1402mod tests;