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(¶meters, 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(¶meters, 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;