Skip to main content

mediadecode_ffmpeg/decoder/
mod.rs

1use std::{collections::VecDeque, mem::ManuallyDrop, ptr};
2
3use ffmpeg_next::{
4  Codec, Packet, Rational,
5  codec::{
6    self,
7    Context,
8    // Bring the `Mut` / `Ref` traits into scope so `Packet::as_ptr` /
9    // `Packet::as_mut_ptr` resolve. They are aliased to avoid shadowing
10    // any future `Mut`/`Ref` types we might add — `cargo clippy` would
11    // otherwise flag them as "unused" without the alias and the import
12    // can mistakenly look unused. Confirmed in use by all `packet.as_ptr()`
13    // / `packet.as_mut_ptr()` call sites in this module.
14    packet::{Mut as PacketMut, Ref as PacketRef},
15  },
16  ffi::{
17    AVBufferRef, AVCodec, AVFrame, AVHWFramesContext, AVMediaType, av_buffer_ref, av_buffer_unref,
18    av_frame_move_ref, av_frame_unref, av_hwdevice_ctx_create, av_hwframe_transfer_data,
19    av_packet_ref, avcodec_alloc_context3, avcodec_free_context, avcodec_parameters_to_context,
20  },
21  frame,
22};
23
24/// Local FFI shims: FFmpeg entry points re-declared with `c_int` where
25/// the generated bindings use a closed Rust enum.
26///
27/// Constructing `AVCodecID` / `AVPixelFormat` / `AVSampleFormat` from a
28/// runtime integer that is not in this build's discriminant set is UB —
29/// and these are open C enums that FFmpeg extends in ABI-compatible
30/// releases. Declaring the same C symbol with `c_int` sidesteps the
31/// boundary entirely: both Rust declarations resolve to the same symbol
32/// at link time, and the integer never becomes an enum on the Rust
33/// side.
34///
35/// **This is the enum class inside this crate's own code.** The
36/// dependency-API sweep closed every place *ffmpeg-next* formed an enum
37/// out of FFmpeg memory; these three are places the crate's own new
38/// code did the same thing. The census that walks the pixel-format
39/// table to price the worst format is the sharpest instance: it exists
40/// precisely to be correct about formats this build does not name, and
41/// the binding it called returned those very ids as a closed
42/// `AVPixelFormat`. Every id would have become an invalid enum value on
43/// the way *into* the pricing that was supposed to handle it.
44pub(crate) mod c_shims {
45  use libc::c_int;
46
47  use super::AVCodec;
48
49  unsafe extern "C" {
50    /// `AVCodecID` as `c_int`.
51    pub fn avcodec_find_decoder(id: c_int) -> *const AVCodec;
52
53    /// Returns `AVPixelFormat` as `c_int` — the id of a descriptor that
54    /// may well name a format this build's bindings do not.
55    pub fn av_pix_fmt_desc_get_id(desc: *const ffmpeg_next::ffi::AVPixFmtDescriptor) -> c_int;
56
57    /// Takes `AVPixelFormat` as `c_int`, so an id straight out of
58    /// [`av_pix_fmt_desc_get_id`] can be priced without ever being an
59    /// enum.
60    pub fn av_image_get_buffer_size(
61      pix_fmt: c_int,
62      width: c_int,
63      height: c_int,
64      align: c_int,
65    ) -> c_int;
66
67    /// Takes `AVSampleFormat` as `c_int`. Kept for the footprint
68    /// sweep, which walks the format table to decide which cells
69    /// exist; production pricing goes through
70    /// `av_samples_get_buffer_size`, the allocator's own ruler.
71    #[cfg(test)]
72    pub fn av_get_bytes_per_sample(sample_fmt: c_int) -> c_int;
73
74    /// The allocator's own audio ruler, with `AVSampleFormat` as
75    /// `c_int`. `align = 0` asks for the alignment
76    /// `av_frame_get_buffer` itself uses.
77    pub fn av_samples_get_buffer_size(
78      linesize: *mut c_int,
79      nb_channels: c_int,
80      nb_samples: c_int,
81      sample_fmt: c_int,
82      align: c_int,
83    ) -> c_int;
84
85    /// Writes an `AV_PIX_FMT_NONE`-terminated list of destination
86    /// formats a transfer may produce. Declared `*mut *mut c_int` so
87    /// the list is walked as integers — a driver may well offer a
88    /// format this build's bindings do not name.
89    pub fn av_hwframe_transfer_get_formats(
90      hwframe_ctx: *mut ffmpeg_next::ffi::AVBufferRef,
91      dir: c_int,
92      formats: *mut *mut c_int,
93      flags: c_int,
94    ) -> c_int;
95  }
96}
97
98use crate::{
99  backend::{self, Backend},
100  error::{AllBackendsFailed, Error, HwDeviceInitFailed, Result},
101  ffi::{CallbackState, codec_supports_hwaccel, get_hw_format},
102  frame::Frame,
103};
104
105/// Hardware-accelerated video decoder.
106///
107/// Hardware-only — there is no software fallback inside this crate. If
108/// every hardware backend in the platform's probe order fails to open,
109/// `open` returns [`Error::AllBackendsFailed`] and the caller is
110/// responsible for falling back to a software decoder of their choice
111/// (e.g. `ffmpeg::decoder::Video`).
112///
113/// Mirrors `ffmpeg::decoder::Video`'s `send_packet`/`receive_frame` interface.
114/// Decoded frames are returned through [`crate::Frame`], a CPU-side wrapper
115/// whose accessors avoid the `AVPixelFormat`-enum UB that an unvalidated read
116/// of FFmpeg's raw integer pixel formats can trigger.
117///
118/// `open` does a true probe: each backend opens with a strict `get_format`
119/// callback. On the first non-transient error from a backend the decoder is
120/// torn down and the next backend in probe order is tried, with all packets
121/// seen so far replayed through it. The advance is *transactional* — the
122/// candidate backend must successfully build and accept the replayed packets
123/// before any probe state is consumed, so a failing backend in the middle of
124/// the order does not strand the caller without history. Once the first frame
125/// is delivered the probe collapses and subsequent calls go straight to the
126/// active (committed) backend.
127///
128/// The committed backend can still fail at runtime — e.g. VideoToolbox can
129/// decode a clip's first frames and then hit content its kernel can't handle
130/// (H.264 High 4:2:2 10-bit), surfacing `AVERROR_EXTERNAL`. Post-commit a
131/// non-transient, non-EOF error from the committed backend is reclassified to
132/// [`Error::AllBackendsFailed`] (see the `is_hw_decode_failure` predicate), so
133/// the [`crate::FfmpegVideoStreamDecoder`] wrapper still recognises it as a
134/// HW-path exhaustion and falls back to software. The post-commit
135/// `unconsumed_packets` is empty (the probe buffer is gone); the wrapper's
136/// rolling since-last-keyframe buffer supplies the replay set.
137pub struct VideoDecoder {
138  /// Live FFmpeg state for the currently active backend.
139  state: DecoderState,
140  /// Reusable frame buffer used for hw-side decoding before transfer / move.
141  /// Internal use only — never handed to callers.
142  hw_frame: frame::Video,
143  /// Probe state: present until the first frame is received from the active
144  /// backend, then `None`. While `Some`, packets are buffered for replay and
145  /// non-transient errors / decoder failures advance to the next backend.
146  probe: Option<ProbeState>,
147  /// CPU-side frames produced by a candidate decoder during probe replay
148  /// (when its internal queue filled and we had to drain output before the
149  /// next `send_packet`). Already transferred from the candidate's
150  /// `AVHWFramesContext` to a CPU frame, so they remain valid after the
151  /// candidate state is committed. [`Self::receive_frame`] dequeues these
152  /// FIFO before reading from `state.inner`.
153  pending_frames: VecDeque<frame::Video>,
154  /// Per-decoder byte budget for [`Self::pending_frames`] during probe
155  /// replay. Defaults to [`DEFAULT_MAX_PROBE_PENDING_BYTES`]; override via
156  /// [`Self::with_max_probe_pending_bytes`].
157  max_probe_pending_bytes: usize,
158  /// Resource ceilings for the frames this decoder produces. Fixed at
159  /// open, because [`FrameLimits::max_pixels`] is written into every
160  /// `AVCodecContext` this decoder builds — including the ones a probe
161  /// advance builds later — and a context's ceiling cannot be moved
162  /// after `avcodec_open2`.
163  frame_limits: crate::limits::DecoderLimits,
164}
165
166/// Owned FFmpeg state for one open codec context. Has its own `Drop` so we
167/// can swap it out cleanly during a probe advance via `mem::replace`.
168struct DecoderState {
169  /// Wrapped FFmpeg decoder. `ManuallyDrop` so we can sequence its drop
170  /// before freeing the callback state.
171  inner: ManuallyDrop<ffmpeg_next::decoder::Video>,
172  /// Backend driving this state.
173  backend: Backend,
174  /// Owned reference produced by `av_hwdevice_ctx_create`.
175  hw_device_ref: *mut AVBufferRef,
176  /// Owned `Box<CallbackState>` raw pointer; `AVCodecContext::opaque`
177  /// aliases it.
178  callback_state: *mut CallbackState,
179}
180
181/// Maximum number of packets we are willing to buffer for probe replay
182/// before abandoning the fallback safety net. Set high enough to absorb
183/// long B-frame GOPs and codec setup latency, low enough to bound memory
184/// against malicious / pathological streams that never produce a first
185/// frame.
186const MAX_PROBE_PACKETS: usize = 256;
187
188/// Maximum total compressed-byte size of buffered probe packets. Each
189/// `Packet` clone holds a refcounted reference to the demuxer's bitstream
190/// data — even though the clone itself is shallow, the underlying buffers
191/// stay alive until we drop them. 64 MiB is generous for normal video and
192/// gives untrusted media a hard ceiling.
193const MAX_PROBE_PACKET_BYTES: usize = 64 * 1024 * 1024;
194
195/// Hard cap on the number of side-data entries we tolerate per buffered
196/// packet. `av_packet_ref` allocates an `AVPacketSideData` descriptor and
197/// an `AVBufferRef` per entry, so a packet stuffed with many tiny or
198/// zero-sized entries can consume significant memory in descriptor /
199/// allocator overhead even after [`packet_side_data_bytes`] charges
200/// [`SIDE_DATA_ENTRY_OVERHEAD`] bytes per entry. Refusing to clone such
201/// packets short-circuits the descriptor explosion path.
202///
203/// Sized for legitimate streams (typical video packets carry 0-5 side-
204/// data entries; SEI-heavy HEVC/AV1 maybe a dozen) while comfortably
205/// rejecting weaponised input.
206///
207/// Shared with the [`crate::FfmpegVideoStreamDecoder`] rolling GOP buffer,
208/// which charges the same side-data budget so its byte cap is a true upper
209/// bound on retained memory rather than counting bare payloads.
210pub(crate) const MAX_PROBE_PACKET_SIDE_DATA_ENTRIES: usize = 64;
211
212/// Conservative per-side-data-entry overhead estimate used by both
213/// [`packet_side_data_bytes`] and the budget accounting in
214/// [`VideoDecoder::send_packet`]. Counts the `AVPacketSideData`
215/// descriptor (24 bytes per the FFmpeg 9.x bindings), the `AVBufferRef`
216/// FFmpeg allocates per entry, and a margin for malloc bookkeeping
217/// (header bytes, alignment slack). Setting it on the high side keeps
218/// the byte cap a true upper bound on retained memory; under-charging
219/// would let many tiny entries slip past the cap.
220const SIDE_DATA_ENTRY_OVERHEAD: usize = 80;
221
222/// Conservative upper-bound bytes-per-pixel multiplier used to estimate
223/// the size of a CPU frame **before** `av_hwframe_transfer_data`
224/// allocates its pixel buffers. Covers every HW download format this
225/// crate produces (worst case is `P416LE` / `P412LE` at 6 bytes/pixel
226/// for 16-bit 4:4:4 semi-planar) plus a margin for FFmpeg's per-row
227/// stride alignment (typically 32-byte aligned, ~5% extra at HD widths
228/// and below).
229///
230/// Used by [`drain_into_pending`] as a pre-transfer guard: if the
231/// product `width * height * WORST_CASE_BYTES_PER_PIXEL` would already
232/// push `pending_bytes` past `max_probe_pending_bytes`, the candidate
233/// replay refuses the frame *before* allocating. Without this, FFmpeg
234/// would perform the full HW→CPU download (potentially ~100 MiB for
235/// 8K HDR) and we would only reject the frame after RSS had already
236/// spiked. The post-transfer accounting via [`cpu_frame_bytes`] stays in
237/// place as a backstop using the frame's actual stride/format.
238///
239/// Slightly over-charges true 4:2:0 NV12 / P010 frames (which dominate
240/// real workloads) — that's the right side to err on. Callers feeding
241/// 8K+ workloads through the probe path can tune
242/// [`VideoDecoder::with_max_probe_pending_bytes`] upward to compensate.
243const WORST_CASE_BYTES_PER_PIXEL: usize = 8;
244
245/// Maximum number of CPU frames we are willing to queue from a candidate
246/// during probe replay. Each frame is a fully-allocated CPU buffer
247/// (~3 MiB for 1080p NV12, ~24 MiB for 4K P010, ~96 MiB for 8K P010), so
248/// an unbounded queue would OOM on a candidate with a shallow internal
249/// queue against a deep replay history. This cap, together with
250/// [`DEFAULT_MAX_PROBE_PENDING_BYTES`], is enforced as a hard limit during
251/// replay: once either limit is reached, probe buffering fails for the
252/// candidate (returns `ENOMEM` from `drain_into_pending`) instead of
253/// queueing additional drained frames. The probe loop then advances to
254/// the next backend or returns `Error::AllBackendsFailed` if exhausted.
255const MAX_PROBE_PENDING_FRAMES: usize = 16;
256
257/// Default byte budget for probe-replay drained frames. 256 MiB is enough
258/// for 16 frames at 4K P010 (~24 MiB each = 384 MiB worst case under the
259/// count cap), and is the cap that fires first for very high-resolution
260/// content (8K P010: ~96 MiB per frame → only ~2 frames fit).
261///
262/// Override per-decoder with [`VideoDecoder::with_max_probe_pending_bytes`]
263/// when targeting 8K+ workloads or memory-constrained environments.
264///
265/// TODO: when frames significantly exceed typical sizes, consider
266/// memmap-backed pending buffers (write transferred frames to a temp file
267/// or shared-memory segment) so the resident set stays bounded even when
268/// the byte cap is raised. Out of scope for now.
269pub const DEFAULT_MAX_PROBE_PENDING_BYTES: usize = 256 * 1024 * 1024;
270
271/// State carried only during the probe window (before the first successful
272/// frame). Holds enough information to tear down the current decoder and
273/// retry with the next backend.
274struct ProbeState {
275  parameters: codec::Parameters,
276  codec: Codec,
277  /// Backends still to try, in order. Empty means "no more options after
278  /// the active one fails" — `advance_probe` then surfaces
279  /// [`Error::AllBackendsFailed`] so the contract is the same on
280  /// single-backend platforms (e.g. macOS) as on multi-backend ones.
281  remaining_backends: Vec<Backend>,
282  /// Packets sent so far, kept for replay through any candidate backend.
283  /// Preserved across failed candidates — only cleared when the probe
284  /// collapses on a successful first frame, or when the probe is
285  /// abandoned due to the size caps.
286  buffered_packets: Vec<Packet>,
287  /// Cumulative size (in compressed bytes) of `buffered_packets`. Tracked
288  /// incrementally so we don't have to re-sum on every send.
289  buffered_bytes: usize,
290  /// Whether `send_eof` has been called; replayed alongside packets.
291  eof_sent: bool,
292  /// Per-backend errors captured since the probe window opened. Pushed
293  /// whenever a backend's failure triggers `advance_probe` (the active
294  /// backend that just failed) or a candidate's build / replay rejects
295  /// it. Drained into [`Error::AllBackendsFailed`] when the probe
296  /// exhausts every option.
297  attempts: Vec<(Backend, Box<Error>)>,
298}
299
300// SAFETY: All raw pointers are exclusively owned by `DecoderState` and never
301// shared. `ffmpeg::decoder::Video` is itself `Send` (its `Context` carries an
302// `unsafe impl Send`). The decoder is not safe for concurrent use, hence not
303// `Sync`.
304unsafe impl Send for DecoderState {}
305unsafe impl Send for VideoDecoder {}
306
307impl Drop for DecoderState {
308  fn drop(&mut self) {
309    // Order matters:
310    //  1. Drop the codec context first. While it lives, FFmpeg may invoke
311    //     `get_format`, which dereferences `callback_state` via `opaque`.
312    //  2. Free the callback state heap allocation.
313    //  3. Release our hw device reference (FFmpeg released its own when
314    //     the codec context was freed in step 1).
315    unsafe {
316      ManuallyDrop::drop(&mut self.inner);
317      if !self.callback_state.is_null() {
318        drop(Box::from_raw(self.callback_state));
319        self.callback_state = ptr::null_mut();
320      }
321      if !self.hw_device_ref.is_null() {
322        av_buffer_unref(&mut self.hw_device_ref);
323      }
324    }
325  }
326}
327
328impl VideoDecoder {
329  /// Auto-probe hardware backends in the platform's default order.
330  ///
331  /// Each backend opens with a strict `get_format` callback. The first
332  /// backend whose `avcodec_open2` succeeds becomes active; if its first
333  /// frame is unusable (decode error, transfer failure, or a CPU-format
334  /// frame from a HW context) the decoder is torn down and the next backend
335  /// is tried — packets sent so far are replayed through the new decoder
336  /// transparently. The probe advance is transactional: the next backend
337  /// must build *and* accept the replayed history before any probe state is
338  /// consumed, so a misbehaving middle backend cannot strand the caller.
339  ///
340  /// [`Self::backend`] reflects whichever backend ultimately produced the
341  /// first frame.
342  ///
343  /// [`Error::AllBackendsFailed`] surfaces in two places, with the same
344  /// meaning ("no hardware backend can decode this stream — fall back to
345  /// software yourself"):
346  /// - From `open` itself, when no backend even opens.
347  /// - From [`Self::send_packet`] / [`Self::send_eof`] /
348  ///   [`Self::receive_frame`], when the initially-opened backend fails
349  ///   at decode time and every remaining backend in the probe order
350  ///   either also fails or doesn't exist. On single-backend platforms
351  ///   (e.g. macOS, where the order is `[VideoToolbox]`), this is the
352  ///   only place a HW-only failure surfaces.
353  ///
354  /// In both cases, `attempts` carries the per-backend error log. When
355  /// the runtime path fires, `unconsumed_packets` also contains the
356  /// packets the decoder consumed from the caller before the probe
357  /// exhausted (refcounted shallow clones); for non-seekable inputs
358  /// (live streams, pipes) the caller can replay these directly into
359  /// a software decoder of their choice without re-demuxing. From the
360  /// open-time path the vec is empty since no packets have been sent.
361  ///
362  /// On `Ok`, the returned decoder **always** has an active probe
363  /// rescue safety net. If a parameters clone fails under memory
364  /// pressure before the probe state can be set up, `open` returns
365  /// `Err(Error::Ffmpeg(Other { errno: ENOMEM }))` rather than handing
366  /// back a live decoder with no fallback contract. No packets have
367  /// been sent yet, so the caller can retry or fall back to software
368  /// with the original `parameters` directly.
369  pub fn open(parameters: codec::Parameters) -> Result<Self> {
370    Self::open_with_frame_limits(parameters, crate::limits::DecoderLimits::default())
371  }
372
373  /// [`Self::open`], with the frame ceilings named.
374  ///
375  /// Taken at open for the reason [`Self::open_with_limits`] gives:
376  /// [`FrameLimits::max_pixels`] is written into every `AVCodecContext`
377  /// this decoder opens — including the ones a later probe advance
378  /// opens — and a context's ceiling cannot be moved after
379  /// `avcodec_open2`.
380  pub fn open_with_frame_limits(
381    parameters: codec::Parameters,
382    limits: crate::limits::DecoderLimits,
383  ) -> Result<Self> {
384    let codec = find_decoder(&parameters)?;
385    let order = backend::probe_order();
386
387    let mut attempts: Vec<(Backend, Box<Error>)> = Vec::new();
388    for (i, &backend) in order.iter().enumerate() {
389      // Use the checked clone — ffmpeg-next's `Parameters::clone` does
390      // `avcodec_parameters_alloc` without a null check and ignores the
391      // return of `avcodec_parameters_copy`. Under OOM that path silently
392      // produces a Parameters with a null inner pointer.
393      let cloned_for_build =
394        match try_clone_parameters(&parameters, limits.max_codec_parameter_bytes()) {
395          Ok(p) => p,
396          Err(e) => {
397            tracing::warn!(?backend, error = %e, "hwdecode: parameters clone failed");
398            attempts.push((backend, Box::new(e)));
399            continue;
400          }
401        };
402      match Self::build_state(cloned_for_build, codec, backend, limits) {
403        Ok(state) => {
404          tracing::info!(?backend, "hwdecode: opened video decoder (probing)");
405          let remaining = order[(i + 1)..].to_vec();
406          // Deep-copy the caller's `parameters` before storing in ProbeState.
407          // `codec::Parameters` from `stream.parameters()` carries an Rc
408          // owner pointing at the demuxer; moving that Rc to a worker
409          // thread (when VideoDecoder is sent) would race with the demuxer's
410          // Rc on the original thread. The checked clone copies the bytes
411          // into a fresh allocation with `owner: None`, severing the link.
412          //
413          // We always create ProbeState — even when `remaining` is empty
414          // (single-backend platforms like macOS) — so that a first-frame
415          // failure on the only backend surfaces as
416          // `Error::AllBackendsFailed` from `receive_frame` /
417          // `send_packet` rather than as a raw FFmpeg error. That keeps
418          // the API contract the same regardless of how many HW backends
419          // the platform exposes.
420          //
421          // If the clone fails (ENOMEM), fail the **whole open call**
422          // rather than returning a live decoder with `probe: None`.
423          // Returning Ok here would let the caller send packets that the
424          // active backend consumes, and a subsequent backend failure
425          // would then surface as a raw FFmpeg error with no
426          // `unconsumed_packets` — silently breaking the rescue contract
427          // for non-seekable inputs (live streams, pipes). Dropping the
428          // already-built `state` here runs its FFmpeg cleanup, and the
429          // caller can retry / fall back to software with the original
430          // parameters in their hand (no packets were consumed yet).
431          // Seed the probe's attempt log with any backends that failed
432          // to open earlier in this loop (including
433          // `BackendUnsupportedByCodec` and parameters-clone errors).
434          // Without this, a runtime exhaustion on the active backend
435          // would surface an `AllBackendsFailed` containing only the
436          // active backend's runtime failure — losing the original
437          // open-time causes that, on multi-backend platforms (Linux,
438          // Windows), are usually the more diagnostic signal. E.g. a
439          // VAAPI-then-CUDA host where VAAPI fails to open and CUDA
440          // later fails at first-frame must report both failures in
441          // probe order, not just CUDA.
442          let probe = match try_clone_parameters(&parameters, limits.max_codec_parameter_bytes()) {
443            Ok(probe_params) => ProbeState {
444              parameters: probe_params,
445              codec,
446              remaining_backends: remaining,
447              buffered_packets: Vec::new(),
448              buffered_bytes: 0,
449              eof_sent: false,
450              attempts: std::mem::take(&mut attempts),
451            },
452            Err(e) => {
453              tracing::warn!(
454                error = %e,
455                "hwdecode: parameters clone failed for probe state at open; \
456                 failing closed instead of returning a decoder without rescue"
457              );
458              return Err(e);
459            }
460          };
461          return Ok(Self {
462            state,
463            hw_frame: alloc_av_frame().map_err(Error::Ffmpeg)?,
464            probe: Some(probe),
465            pending_frames: VecDeque::new(),
466            max_probe_pending_bytes: DEFAULT_MAX_PROBE_PENDING_BYTES,
467            frame_limits: limits,
468          });
469        }
470        Err(e) => {
471          tracing::warn!(?backend, error = %e, "hwdecode: backend open failed");
472          attempts.push((backend, Box::new(e)));
473        }
474      }
475    }
476    // No packets have been consumed at open time.
477    Err(Error::AllBackendsFailed(AllBackendsFailed::new(
478      attempts,
479      Vec::new(),
480    )))
481  }
482
483  /// Open the decoder with a specific backend. No probe, no fallback.
484  ///
485  /// If `backend` cannot actually decode this stream, the failure surfaces
486  /// from [`Self::receive_frame`] (the strict `get_format` callback returns
487  /// `AV_PIX_FMT_NONE`, the decoder errors out). The caller is responsible
488  /// for retrying with another hardware backend or falling back to a
489  /// software decoder of their choice (e.g. `ffmpeg::decoder::Video`).
490  pub fn open_with(parameters: codec::Parameters, backend: Backend) -> Result<Self> {
491    Self::open_with_limits(parameters, backend, crate::limits::DecoderLimits::default())
492  }
493
494  /// [`Self::open_with`], with the frame ceilings named.
495  ///
496  /// The limits are taken **at open**, not through a `with_*` builder,
497  /// because [`FrameLimits::max_pixels`] is written straight into the
498  /// `AVCodecContext` this call opens — that is the layer that makes
499  /// libavcodec refuse an oversized picture before allocating it, and a
500  /// context's ceiling cannot be moved after `avcodec_open2`. A builder
501  /// would have silently applied to only half the enforcement.
502  pub fn open_with_limits(
503    parameters: codec::Parameters,
504    backend: Backend,
505    limits: crate::limits::DecoderLimits,
506  ) -> Result<Self> {
507    let codec = find_decoder(&parameters)?;
508    let state = Self::build_state(parameters, codec, backend, limits)?;
509    Ok(Self {
510      state,
511      hw_frame: alloc_av_frame().map_err(Error::Ffmpeg)?,
512      probe: None,
513      pending_frames: VecDeque::new(),
514      max_probe_pending_bytes: DEFAULT_MAX_PROBE_PENDING_BYTES,
515      frame_limits: limits,
516    })
517  }
518
519  /// Override the byte budget for probe-replay queued frames. Defaults to
520  /// [`DEFAULT_MAX_PROBE_PENDING_BYTES`]. Use a higher value when targeting
521  /// 8K+ workloads where 16 frames at full size could exceed the default;
522  /// use a lower value in memory-constrained services to bound peak
523  /// allocation more tightly.
524  ///
525  /// Setting after the first frame has been delivered is harmless but has
526  /// no observable effect — the probe has already collapsed and the cap
527  /// only applies during replay drain.
528  ///
529  /// Returns `self` for builder-style chaining:
530  /// ```ignore
531  /// let decoder = VideoDecoder::open(params)?
532  ///     .with_max_probe_pending_bytes(1024 * 1024 * 1024); // 1 GiB
533  /// ```
534  #[must_use]
535  pub fn with_max_probe_pending_bytes(mut self, bytes: usize) -> Self {
536    self.max_probe_pending_bytes = bytes;
537    self
538  }
539
540  /// The backend currently producing frames. While the probe is still in
541  /// progress (no frame received yet) this returns the optimistically
542  /// selected backend; after the first frame, it is the backend that
543  /// actually produced it. Once stable, never changes again.
544  pub fn backend(&self) -> Backend {
545    self.state.backend
546  }
547
548  /// Decoder width in pixels.
549  pub fn width(&self) -> u32 {
550    self.state.inner.width()
551  }
552
553  /// Decoder height in pixels.
554  pub fn height(&self) -> u32 {
555    self.state.inner.height()
556  }
557
558  /// Codec context time base.
559  pub fn time_base(&self) -> Rational {
560    self.state.inner.time_base()
561  }
562
563  /// Frame rate from the codec context, if known.
564  pub fn frame_rate(&self) -> Option<Rational> {
565    self.state.inner.frame_rate()
566  }
567
568  /// Reclassify a post-commit runtime error from the committed HW backend
569  /// into [`Error::AllBackendsFailed`] so the [`crate::FfmpegVideoStreamDecoder`]
570  /// wrapper recognises it as a HW-path exhaustion and falls back to
571  /// software. The single attempt records the committed backend
572  /// (`self.state.backend` is the live backend post-commit) paired with the
573  /// underlying FFmpeg error. `unconsumed_packets` is empty: the probe
574  /// buffer is gone after commit, so the wrapper's rolling
575  /// since-last-keyframe buffer supplies the replay set.
576  fn post_commit_hw_failure(&self, e: ffmpeg_next::Error) -> Error {
577    // Through the funnel: this is the exit a decoder with no probe
578    // takes, and it used to wrap FFmpeg's error raw.
579    let inner = self.hw_exit(Error::Ffmpeg(e));
580    // `new_post_commit` stamps `FallbackOrigin::PostCommit`: the wrapper
581    // routes its replay on that explicit signal, not on the (here-empty)
582    // `unconsumed_packets`, which a probe-era first-packet cap trip also
583    // leaves empty.
584    Error::AllBackendsFailed(AllBackendsFailed::new_post_commit(vec![(
585      self.state.backend,
586      Box::new(inner),
587    )]))
588  }
589
590  /// Whether the probe rescue history is still being recorded.
591  ///
592  /// While this is true, [`Self::send_packet`] `av_packet_ref`s every
593  /// accepted packet into `buffered_packets`, and a later
594  /// [`Error::AllBackendsFailed`] hands those recordings to the caller
595  /// as owned, mutable `Packet`s. A submission built to be dropped
596  /// inside one call therefore does **not** stay inside that call on
597  /// this road — which is what the view lane's send-side sharing
598  /// assumed. The window closes at commit, when the first frame
599  /// arrives and `probe` is taken.
600  #[inline]
601  pub(crate) const fn is_probing(&self) -> bool {
602    self.probe.is_some()
603  }
604
605  /// Submit a packet to the decoder.
606  ///
607  /// On success — and only on success — the packet is buffered for potential
608  /// replay through a fallback backend while the probe is active. EAGAIN
609  /// (decoder needs `receive_frame` to drain output first) propagates as
610  /// normal backpressure; the caller drains then retries.
611  ///
612  /// While the probe is active, a non-transient error (e.g. the active HW
613  /// backend rejecting this stream's geometry on first packet) advances the
614  /// probe to the next candidate and retries the packet there. The caller
615  /// observes only the eventual success or, if the probe is exhausted, the
616  /// final error.
617  ///
618  /// **Atomic probe rescue.** While the probe is active, the rescue
619  /// invariant is that everything FFmpeg has consumed since open is
620  /// reflected in `buffered_packets` (so a future
621  /// [`Error::AllBackendsFailed`] can hand a complete replay history
622  /// back to the caller for software fallback on a non-seekable input).
623  /// If we cannot prove this packet is buffer-able — its side-data
624  /// entry count exceeds [`MAX_PROBE_PACKET_SIDE_DATA_ENTRIES`], its
625  /// bytes would push the probe past [`MAX_PROBE_PACKETS`] or
626  /// [`MAX_PROBE_PACKET_BYTES`], or [`av_packet_ref`] fails ENOMEM —
627  /// `send_packet` returns [`Error::AllBackendsFailed`] **without
628  /// invoking** `state.inner.send_packet` on this packet. The caller's
629  /// packet stays in their hand and `unconsumed_packets` carries the
630  /// pre-existing buffered history, so they can replay
631  /// `unconsumed_packets` plus the current packet through their
632  /// software decoder of choice. The post-probe path (after the first
633  /// frame, when `self.probe` is `None`) skips this pre-flight
634  /// entirely.
635  pub fn send_packet(&mut self, packet: &Packet) -> Result<()> {
636    loop {
637      // Pre-flight while probe is active: prove we can record this
638      // packet for replay BEFORE the active decoder consumes it.
639      // `staged_clone` carries the refcounted clone and the new
640      // `buffered_bytes` value through the send below; we only commit
641      // them to the probe state if FFmpeg accepts the packet.
642      let staged_clone: Option<(Packet, usize)> = if let Some(probe) = self.probe.as_ref() {
643        // Step 1: side-data entry count cap. Read just `side_data_elems`
644        // (no array walk yet) so a corrupt or weaponised value cannot
645        // drive an unbounded loop from the safe entry point.
646        let side_count = packet_side_data_count(packet);
647        if side_count > MAX_PROBE_PACKET_SIDE_DATA_ENTRIES {
648          let probe = self.probe.take().expect("probe present");
649          tracing::warn!(
650            side_data_entries = side_count,
651            max_side_data_entries = MAX_PROBE_PACKET_SIDE_DATA_ENTRIES,
652            trigger = "side_data_entry_cap",
653            "hwdecode: probe rescue exhausted before consuming packet; \
654             returning AllBackendsFailed without invoking decoder"
655          );
656          return Err(Error::AllBackendsFailed(AllBackendsFailed::new(
657            probe.attempts,
658            probe.buffered_packets,
659          )));
660        }
661        // Step 2: byte / packet count cap. `packet_side_data_bytes`
662        // clamps its walk to MAX_PROBE_PACKET_SIDE_DATA_ENTRIES as
663        // defense-in-depth even though the count check above already
664        // bounded the array length.
665        let pkt_size = packet.size().saturating_add(packet_side_data_bytes(
666          packet,
667          MAX_PROBE_PACKET_SIDE_DATA_ENTRIES,
668        ));
669        let new_count = probe.buffered_packets.len() + 1;
670        let new_bytes = probe.buffered_bytes.saturating_add(pkt_size);
671        if new_count > MAX_PROBE_PACKETS || new_bytes > MAX_PROBE_PACKET_BYTES {
672          let probe = self.probe.take().expect("probe present");
673          tracing::warn!(
674            packets = new_count,
675            bytes = new_bytes,
676            side_data_entries = side_count,
677            max_packets = MAX_PROBE_PACKETS,
678            max_bytes = MAX_PROBE_PACKET_BYTES,
679            trigger = "byte_or_packet_cap",
680            "hwdecode: probe rescue exhausted before consuming packet; \
681             returning AllBackendsFailed without invoking decoder"
682          );
683          return Err(Error::AllBackendsFailed(AllBackendsFailed::new(
684            probe.attempts,
685            probe.buffered_packets,
686          )));
687        }
688        // Step 3: pre-clone before consuming. `av_packet_ref` is a
689        // refcounted shallow clone (no payload deep-copy) but can still
690        // ENOMEM on heavy side-data; if it does we bail rather than
691        // consuming a packet we can't track.
692        match try_clone_packet(packet) {
693          Ok(c) => Some((c, new_bytes)),
694          Err(e) => {
695            let probe = self.probe.take().expect("probe present");
696            tracing::warn!(
697              error = %e,
698              "hwdecode: packet clone failed before consuming; \
699               returning AllBackendsFailed without invoking decoder"
700            );
701            return Err(Error::AllBackendsFailed(AllBackendsFailed::new(
702              probe.attempts,
703              probe.buffered_packets,
704            )));
705          }
706        }
707      } else {
708        None
709      };
710
711      match self.state.inner.send_packet(packet) {
712        Ok(()) => {
713          if let Some((cloned, new_bytes)) = staged_clone {
714            // Probe is still Some here: the only paths that take it are
715            // the bailouts above (which return) and `advance_probe`'s
716            // exhaustion (which would have propagated via `?`). Commit
717            // the clone now that FFmpeg has accepted the packet.
718            if let Some(probe) = self.probe.as_mut() {
719              probe.buffered_packets.push(cloned);
720              probe.buffered_bytes = new_bytes;
721            }
722          }
723          return Ok(());
724        }
725        Err(e) if is_transient(&e) => {
726          // EAGAIN / EOF backpressure — pass through unchanged. The
727          // staged clone drops; the caller will retry after draining
728          // and we'll re-clone at the top of the loop.
729          return Err(Error::Ffmpeg(e));
730        }
731        Err(e) => {
732          if self.probe.is_some() {
733            // advance_probe consumes the error into `attempts` and
734            // either installs a candidate (Ok — loop top re-clones for
735            // the new candidate) or surfaces AllBackendsFailed (Err —
736            // `?` propagates). Either way the staged clone we just
737            // built drops without entering history; the next iteration
738            // clones afresh against the new active state.
739            self.advance_probe(Error::Ffmpeg(e))?;
740            continue;
741          }
742          // Post-commit (probe gone): the committed HW backend just failed
743          // at runtime. A HW-only decoder's non-transient, non-EOF error
744          // means the backend can't decode this content — reclassify to
745          // AllBackendsFailed so the wrapper falls back to software.
746          if is_hw_decode_failure(&e) {
747            return Err(self.post_commit_hw_failure(e));
748          }
749          return Err(Error::Ffmpeg(e));
750        }
751      }
752    }
753  }
754
755  /// Signal end-of-stream to the decoder.
756  ///
757  /// Recorded for replay only if the underlying `send_eof` succeeds. While
758  /// the probe is active, non-transient errors trigger probe advance and
759  /// retry, matching `send_packet`'s behaviour.
760  pub fn send_eof(&mut self) -> Result<()> {
761    loop {
762      match self.state.inner.send_eof() {
763        Ok(()) => {
764          if let Some(probe) = self.probe.as_mut() {
765            probe.eof_sent = true;
766          }
767          return Ok(());
768        }
769        Err(e) if is_transient(&e) => return Err(Error::Ffmpeg(e)),
770        Err(e) => {
771          if self.probe.is_some() {
772            self.advance_probe(Error::Ffmpeg(e))?;
773            continue;
774          }
775          // Post-commit: committed HW backend failed draining at EOF.
776          // Reclassify a HW-decode failure so the wrapper falls back to SW
777          // (the SW decoder will re-receive the buffered GOP + EOF).
778          if is_hw_decode_failure(&e) {
779            return Err(self.post_commit_hw_failure(e));
780          }
781          return Err(Error::Ffmpeg(e));
782        }
783      }
784    }
785  }
786
787  /// Receive a CPU-side decoded frame.
788  ///
789  /// The frame is downloaded with `av_hwframe_transfer_data` and metadata
790  /// is copied via `av_frame_copy_props`. The caller's frame is always
791  /// unref'd first, so reuse across resolution changes or different
792  /// decoders is safe.
793  ///
794  /// While the probe window is open, *any* non-transient failure (decode
795  /// error, transfer error, copy_props error, or a CPU-format frame from a
796  /// HW-opened context) tears down the current decoder and advances to the
797  /// next hardware backend in probe order, replaying buffered packets
798  /// through it. Frames the candidate produced during replay (drained when
799  /// `send_packet` returned EAGAIN) are queued and delivered FIFO via this
800  /// method, so the caller never loses initial frames after a fallback.
801  ///
802  /// This crate is hardware-only: there is no software fallback inside the
803  /// decoder. When every backend in the probe order has been exhausted —
804  /// including the case of a single-backend platform whose only backend
805  /// failed — this returns [`Error::AllBackendsFailed`] with the per-
806  /// backend attempt log so the caller can branch into a software
807  /// decoder of their choice.
808  ///
809  /// Returns the same transient signals as `ffmpeg::decoder::Video`:
810  /// `Error::Ffmpeg(Other { errno: EAGAIN })` when no frame is ready and
811  /// more packets must be sent, and `Error::Ffmpeg(Eof)` once fully drained.
812  pub fn receive_frame(&mut self, frame: &mut Frame) -> Result<()> {
813    // Pre-drain frames queued during probe replay. They are already CPU-side
814    // (transferred at drain time, when the candidate's HW context was alive)
815    // so we just move them into the caller's slot.
816    if self.try_pop_pending(frame) {
817      return Ok(());
818    }
819
820    loop {
821      let res = self.state.inner.receive_frame(&mut self.hw_frame);
822      match res {
823        Err(e) => {
824          // EAGAIN is normal backpressure — pass through unconditionally.
825          if is_eagain(&e) {
826            return Err(Error::Ffmpeg(e));
827          }
828          // EOF (and every other non-transient error): if we are still
829          // probing, treat it as candidate failure — a backend that drains
830          // to EOF without ever producing a frame should not silently
831          // present as "stream over" to the caller. Advance and retry; if
832          // every backend has been exhausted, advance_probe surfaces
833          // AllBackendsFailed and `?` propagates it.
834          if self.probe.is_some() {
835            self.advance_probe(Error::Ffmpeg(e))?;
836            // Probe advance may have populated `pending_frames`; deliver
837            // one of those before reading more from the new candidate.
838            if self.try_pop_pending(frame) {
839              return Ok(());
840            }
841            continue;
842          }
843          // Probe collapsed already. A non-transient, non-EOF error from the
844          // committed HW backend is a runtime HW-decode failure — reclassify
845          // to AllBackendsFailed so the wrapper falls back to software.
846          // `is_hw_decode_failure` excludes EOF, so a genuine end-of-stream
847          // still propagates as `Error::Ffmpeg(Eof)` (never trapped in an
848          // infinite fallback-retry loop).
849          if is_hw_decode_failure(&e) {
850            return Err(self.post_commit_hw_failure(e));
851          }
852          // **Including EOF, which on this road can be a refusal.**
853          // `is_hw_decode_failure` deliberately excludes EOF so a
854          // genuinely drained stream is not trapped in a fallback loop
855          // — but a `get_format` declination leaves the decoder drained
856          // too, and reporting that as "stream over" is the quietest
857          // wrong answer of the set. The funnel tells the two apart:
858          // it answers with the recorded refusal when there is one, and
859          // with the end of the stream when there is not.
860          return Err(self.hw_exit(Error::Ffmpeg(e)));
861        }
862        Ok(()) => {
863          // Always attempt the HW→CPU transfer. With strict `get_format`,
864          // libavcodec can only deliver frames in the wired-up HW format
865          // (or fail). If a misbehaving codec ever hands us a CPU-side
866          // frame anyway, `av_hwframe_transfer_data` returns AVERROR(EINVAL)
867          // (neither src nor dst has an AVHWFramesContext attached) and we
868          // route through the same error path below.
869          // **The transfer is priced before it is paid, and a refusal
870          // here is final.** See [`judge_hw_transfer`]: neither ceiling
871          // hook reaches this allocation — `hwaccel->alloc_frame`
872          // bypasses `get_buffer2` entirely, and the CPU destination is
873          // allocated by `av_hwframe_transfer_data` outside both — so
874          // this is the seat that bounds what the hardware road hands
875          // back.
876          //
877          // Judged out here rather than inside `transfer_hw_frame`
878          // deliberately. Errors from that function are FFmpeg's, and
879          // the arms below reclassify them into "the hardware failed,
880          // fall back to software". A byte ceiling is not a hardware
881          // failure: software would decode the same oversized frame and
882          // be refused again, so retrying it silently is exactly the
883          // wrong answer. The named refusal returns straight to the
884          // caller.
885          if let Err(e) =
886            unsafe { judge_hw_transfer(self.hw_frame.as_ptr(), self.frame_limits.frame()) }
887          {
888            return Err(Error::HwTransferTooLarge(e));
889          }
890          match unsafe { transfer_hw_frame(frame, &mut self.hw_frame) } {
891            Ok(()) => {
892              self.probe = None;
893              return Ok(());
894            }
895            Err(e) => {
896              if self.probe.is_some() {
897                self.advance_probe(Error::Ffmpeg(e))?;
898                unsafe { av_frame_unref(frame.as_inner_mut().as_mut_ptr()) };
899                if self.try_pop_pending(frame) {
900                  return Ok(());
901                }
902                continue;
903              }
904              // Post-commit transfer failure: an unsupported CPU output
905              // pix_fmt surfaces as AVERROR(EINVAL) and a context-loss as
906              // Bug/Bug2/Unknown — all HW-output problems, never input
907              // corruption. Reclassify so the wrapper falls back to software.
908              if is_hw_decode_failure(&e) {
909                return Err(self.post_commit_hw_failure(e));
910              }
911              return Err(Error::Ffmpeg(e));
912            }
913          }
914        }
915      }
916    }
917  }
918
919  /// Pop one queued frame (produced by a candidate decoder during probe
920  /// replay) into the caller's slot. Returns `true` when a frame was
921  /// delivered, `false` when the queue was empty.
922  fn try_pop_pending(&mut self, frame: &mut Frame) -> bool {
923    let Some(mut buffered) = self.pending_frames.pop_front() else {
924      return false;
925    };
926    // SAFETY: `buffered` is a CPU-side AVFrame we previously transferred
927    // and pushed into the queue; both pointers are valid.
928    unsafe {
929      av_frame_unref(frame.as_inner_mut().as_mut_ptr());
930      av_frame_move_ref(frame.as_inner_mut().as_mut_ptr(), buffered.as_mut_ptr());
931    }
932    // Probe semantics: delivering a frame collapses the probe.
933    self.probe = None;
934    true
935  }
936
937  /// Flush internal buffers (e.g. after a seek).
938  ///
939  /// Discards every frame buffered by the decoder, every frame queued during
940  /// probe replay (`pending_frames`), and the residual `hw_frame` scratch
941  /// buffer. Probe-time replay state (buffered packets, EOF marker) is also
942  /// cleared since post-seek packets do not align with the previously
943  /// captured history. After a flush, the next `receive_frame` waits for new
944  /// post-seek input.
945  pub fn flush(&mut self) {
946    self.state.inner.flush();
947    // SAFETY: hw_frame is a valid AVFrame we own; av_frame_unref is a no-op
948    // for an already-empty frame.
949    unsafe { av_frame_unref(self.hw_frame.as_mut_ptr()) };
950    self.pending_frames.clear();
951    if let Some(probe) = self.probe.as_mut() {
952      probe.buffered_packets.clear();
953      probe.buffered_bytes = 0;
954      probe.eof_sent = false;
955    }
956  }
957
958  /// Takes the coded-surface refusal the `get_format` callback left
959  /// behind, if it left one, clearing it for the next candidate.
960  fn take_ceiling_declination(&self) -> Option<Error> {
961    ceiling_declination_of(self.state.callback_state)
962  }
963
964  /// **The single hardware-exit funnel.** Every road that turns a
965  /// hardware failure — or an end-of-stream that is really a refusal —
966  /// into an `Error` goes through here, and it reads the callback's
967  /// declination *before* anything wraps or tears down state.
968  ///
969  /// The reason there is a funnel at all: a `get_format` callback
970  /// cannot return a reason, so it leaves one behind, and every exit
971  /// that forgets to collect it hands the caller libavcodec's
972  /// `Invalid data found when processing input` for a refusal this
973  /// crate made — or, on the explicit-backend road, a stream that
974  /// simply drains to EOF with nothing said at all.
975  ///
976  /// The lesson this encodes: R14 claimed four consumers of the
977  /// declination and production had exactly one. Consumers added
978  /// helper-by-helper are lost the next time the surrounding code is
979  /// restructured; a single funnel that every exit *must* call is the
980  /// only version of this that stays true. The per-road table in
981  /// `decoder/tests.rs` is what checks that it did.
982  fn hw_exit(&self, fallback: Error) -> Error {
983    self
984      .take_ceiling_declination()
985      .or_else(|| frame_budget_declination_of(self.state.callback_state))
986      .unwrap_or(fallback)
987  }
988
989  /// Try the next backend in `remaining_backends`. Transactional: a
990  /// candidate must successfully build and accept the replayed history
991  /// before any probe state is consumed. Backends that fail to build or
992  /// reject the replay are recorded into `probe.attempts` and the loop
993  /// continues to the next one.
994  ///
995  /// `last_error` is the error that triggered this advance — i.e. the
996  /// failure of the currently active backend on `send_packet` /
997  /// `send_eof` / `receive_frame`. It is recorded against the active
998  /// backend before any candidate is tried so that a final
999  /// `AllBackendsFailed` carries the full attempt log including the
1000  /// initially-opened backend's runtime failure.
1001  ///
1002  /// Returns:
1003  /// - `Ok(())` when a candidate is installed and replay completed —
1004  ///   caller should retry the operation.
1005  /// - `Err(Error::AllBackendsFailed(p))` when every remaining
1006  ///   backend has been exhausted (including the just-failed active one).
1007  ///   `p.attempts()` carries the per-backend failure log.
1008  ///   This is what the documented `open` contract promises, surfaced at
1009  ///   runtime so the caller can branch into a software fallback. On a
1010  ///   single-backend platform (e.g. macOS), this fires after the only
1011  ///   backend's first-frame failure; on multi-backend platforms it
1012  ///   fires after the last candidate's failure.
1013  /// - `Err(_)` for other fatal conditions surfaced by probe machinery
1014  ///   itself (e.g. `alloc_av_frame` ENOMEM during replay drain).
1015  fn advance_probe(&mut self, last_error: Error) -> Result<()> {
1016    // Record the failure that triggered this advance against the active
1017    // backend. If the probe was somehow already gone (shouldn't happen —
1018    // call sites guard with `self.probe.is_some()`), just propagate the
1019    // error so behaviour matches the pre-fix code path.
1020    let active_backend = self.state.backend;
1021    // **The reason the callback could not return.** Declining a format
1022    // in `get_format` surfaces from libavcodec as
1023    // `Invalid data found when processing input` — true about what it
1024    // saw, false about what happened, because the data was fine and
1025    // this crate declined it over the coded surface's size. The
1026    // callback leaves the real reason in its own state; this is where
1027    // it becomes the error the caller reads.
1028    let last_error = self.hw_exit(last_error);
1029    match self.probe.as_mut() {
1030      Some(probe) => probe.attempts.push((active_backend, Box::new(last_error))),
1031      None => return Err(last_error),
1032    }
1033
1034    // Drop frames previously queued from the backend we're now abandoning.
1035    // They came from a candidate that just failed for cause and cannot be
1036    // trusted alongside frames we may queue from the next candidate. (If
1037    // this method is called repeatedly via chained probe advances, this
1038    // also keeps `pending_frames` from accumulating frames from multiple
1039    // rejected backends.)
1040    self.pending_frames.clear();
1041
1042    loop {
1043      // Snapshot inputs without mutating probe state. Use the checked
1044      // clone helper rather than `Parameters::clone` (which masks ENOMEM).
1045      let (next_backend, parameters, codec) = match self.probe.as_ref() {
1046        Some(probe) if !probe.remaining_backends.is_empty() => {
1047          let parameters = match try_clone_parameters(
1048            &probe.parameters,
1049            self.frame_limits.max_codec_parameter_bytes(),
1050          ) {
1051            Ok(p) => p,
1052            Err(e) => {
1053              tracing::warn!(
1054                error = %e,
1055                "hwdecode: parameters clone failed during probe advance; popping backend and trying next"
1056              );
1057              let popped = self
1058                .probe
1059                .as_mut()
1060                .expect("probe state present")
1061                .remaining_backends
1062                .remove(0);
1063              self
1064                .probe
1065                .as_mut()
1066                .expect("probe state present")
1067                .attempts
1068                .push((popped, Box::new(e)));
1069              continue;
1070            }
1071          };
1072          (probe.remaining_backends[0], parameters, probe.codec)
1073        }
1074        // No more candidates — surface the accumulated attempt log as
1075        // AllBackendsFailed so single- and multi-backend platforms have
1076        // the same contract for "every HW backend failed."
1077        //
1078        // Hand the buffered packet history back to the caller along
1079        // with the attempt log: those packets were consumed from the
1080        // caller's demuxer (and refcounted-cloned into `buffered_packets`)
1081        // before the probe exhausted, and for non-seekable inputs the
1082        // caller cannot re-demux them. Returning them here lets a
1083        // caller-side software fallback replay the same byte history
1084        // through `ffmpeg::decoder::Video` without losing initial frames.
1085        // Dropping `ProbeState` after the take frees the codec/params
1086        // refs we no longer need; only `attempts` and `buffered_packets`
1087        // are retained.
1088        _ => {
1089          let (attempts, unconsumed_packets) = self
1090            .probe
1091            .take()
1092            .map(|p| (p.attempts, p.buffered_packets))
1093            .unwrap_or_default();
1094          return Err(Error::AllBackendsFailed(AllBackendsFailed::new(
1095            attempts,
1096            unconsumed_packets,
1097          )));
1098        }
1099      };
1100
1101      let prev_backend = self.state.backend;
1102      tracing::warn!(from = ?prev_backend, to = ?next_backend, "hwdecode: advancing probe");
1103
1104      // Build candidate. On failure, record into attempts and continue
1105      // without touching the packet buffer.
1106      let mut candidate_state =
1107        match Self::build_state(parameters, codec, next_backend, self.frame_limits) {
1108          Ok(s) => s,
1109          Err(e) => {
1110            tracing::warn!(?next_backend, error = %e, "hwdecode: candidate build failed");
1111            self
1112              .probe
1113              .as_mut()
1114              .expect("probe state present")
1115              .remaining_backends
1116              .remove(0);
1117            self
1118              .probe
1119              .as_mut()
1120              .expect("probe state present")
1121              .attempts
1122              .push((next_backend, Box::new(e)));
1123            continue;
1124          }
1125        };
1126
1127      // Replay buffered history through the candidate WITHOUT installing it.
1128      // We borrow the buffer immutably; if replay fails the candidate's Drop
1129      // releases the FFmpeg state and the buffer is preserved for the next
1130      // attempt.
1131      //
1132      // EAGAIN handling: `avcodec_send_packet` may return EAGAIN when its
1133      // internal queue is full and the user is expected to drain output
1134      // first (B-frame buffering, candidate-specific queue depth, etc.).
1135      // This is normal flow — we drain frames out of the candidate, transfer
1136      // each one to a CPU frame, and stash them in `local_pending`. After
1137      // commit they move to `self.pending_frames` and are delivered FIFO
1138      // by `receive_frame`, so the caller never loses initial frames.
1139      let mut local_pending: VecDeque<frame::Video> = VecDeque::new();
1140      let mut local_pending_bytes: usize = 0;
1141      let max_pending_bytes = self.max_probe_pending_bytes;
1142      let replay_result: std::result::Result<(), ffmpeg_next::Error> = {
1143        let probe = self.probe.as_ref().expect("probe state present");
1144        let mut hw_buf = match alloc_av_frame() {
1145          Ok(f) => f,
1146          Err(e) => return Err(Error::Ffmpeg(e)),
1147        };
1148        let mut r: std::result::Result<(), ffmpeg_next::Error> = Ok(());
1149
1150        'replay: for pkt in &probe.buffered_packets {
1151          loop {
1152            match candidate_state.inner.send_packet(pkt) {
1153              Ok(()) => break,
1154              Err(e) if is_eagain(&e) => {
1155                // Drain candidate output (transferring + queueing each frame)
1156                // and retry the same packet.
1157                if let Err(de) = drain_into_pending(
1158                  &mut candidate_state.inner,
1159                  &mut hw_buf,
1160                  &mut local_pending,
1161                  &mut local_pending_bytes,
1162                  max_pending_bytes,
1163                  self.frame_limits.frame(),
1164                ) {
1165                  r = Err(de);
1166                  break 'replay;
1167                }
1168              }
1169              Err(e) => {
1170                r = Err(e);
1171                break 'replay;
1172              }
1173            }
1174          }
1175        }
1176        if r.is_ok() && probe.eof_sent {
1177          // `avcodec_send_packet(NULL)` (which `send_eof` becomes) can
1178          // return EAGAIN with the same drain-output-first semantics as
1179          // a regular send_packet. Loop drain+retry instead of failing
1180          // the candidate on backpressure.
1181          loop {
1182            match candidate_state.inner.send_eof() {
1183              Ok(()) => break,
1184              Err(e) if is_eagain(&e) => {
1185                if let Err(de) = drain_into_pending(
1186                  &mut candidate_state.inner,
1187                  &mut hw_buf,
1188                  &mut local_pending,
1189                  &mut local_pending_bytes,
1190                  max_pending_bytes,
1191                  self.frame_limits.frame(),
1192                ) {
1193                  r = Err(de);
1194                  break;
1195                }
1196              }
1197              Err(e) => {
1198                r = Err(e);
1199                break;
1200              }
1201            }
1202          }
1203        }
1204        r
1205      };
1206
1207      if let Err(e) = replay_result {
1208        tracing::warn!(?next_backend, error = %e, "hwdecode: candidate replay failed");
1209        // **The candidate's own refusal, read before the candidate
1210        // dies.** `hw_exit` consults `self.state` — the backend that is
1211        // still active — but the error being recorded here belongs to
1212        // `candidate_state`, whose `get_format` callback is the one
1213        // that may have declined. Classifying through the wrong state
1214        // and then dropping the right one lost the reason entirely: the
1215        // attempt log recorded FFmpeg's `Invalid data found when
1216        // processing input` for a coded surface this crate refused.
1217        //
1218        // Order matters and is the whole fix — read, then drop.
1219        let recorded =
1220          ceiling_declination_of(candidate_state.callback_state).unwrap_or(Error::Ffmpeg(e));
1221        // Drop candidate explicitly so its FFI cleanup runs now. Discard any
1222        // frames we drained from this candidate — they're tied to a decoder
1223        // we're throwing away.
1224        drop(candidate_state);
1225        drop(local_pending);
1226        self
1227          .probe
1228          .as_mut()
1229          .expect("probe state present")
1230          .remaining_backends
1231          .remove(0);
1232        self
1233          .probe
1234          .as_mut()
1235          .expect("probe state present")
1236          .attempts
1237          .push((next_backend, Box::new(recorded)));
1238        continue;
1239      }
1240
1241      // Commit: install the candidate, clear residual hw_frame, queue the
1242      // drained frames for the caller, and pop the now-active backend.
1243      self.state = candidate_state;
1244      unsafe { av_frame_unref(self.hw_frame.as_mut_ptr()) };
1245      self.pending_frames.append(&mut local_pending);
1246      self
1247        .probe
1248        .as_mut()
1249        .expect("probe state present")
1250        .remaining_backends
1251        .remove(0);
1252      return Ok(());
1253    }
1254  }
1255
1256  /// Build raw FFmpeg state for one hardware backend. Strict `get_format`
1257  /// (NONE on missing HW format); cross-backend fallback is the caller's job.
1258  fn build_state(
1259    parameters: codec::Parameters,
1260    codec: Codec,
1261    backend: Backend,
1262    limits: crate::limits::DecoderLimits,
1263  ) -> Result<DecoderState> {
1264    // Use our checked allocator instead of Context::from_parameters, which
1265    // does not null-check avcodec_alloc_context3 and would feed a null
1266    // AVCodecContext into FFmpeg under OOM.
1267    let (mut ctx, mut state) = build_codec_context(&parameters, limits)?;
1268    let av_type = backend.av_hwdevice_type();
1269
1270    // Verify the codec advertises this hwaccel **with the exact HW pix_fmt
1271    // we're about to wire up in `get_format`**. FFmpeg's HW config table
1272    // is keyed per (device_type, pix_fmt); a codec can advertise the same
1273    // device with several HW pix_fmts, so matching only on device_type
1274    // would let probing succeed for a backend whose pix_fmt the codec
1275    // never offers — the failure would then surface deep inside the
1276    // probe/decode loop. Matching the exact pix_fmt keeps the strict
1277    // `get_format` honest and gives `open_with` a clean rejection.
1278    let hw_pix_fmt = backend.hw_pixel_format();
1279    if !codec_supports_hwaccel(unsafe { codec.as_ptr() }, av_type, hw_pix_fmt as i32) {
1280      return Err(Error::BackendUnsupportedByCodec(backend));
1281    }
1282
1283    // Create the device context.
1284    let mut hw_device_ref: *mut AVBufferRef = ptr::null_mut();
1285    // SAFETY: `hw_device_ref` is a stack ptr we hand FFmpeg to fill.
1286    let ret = unsafe {
1287      av_hwdevice_ctx_create(&mut hw_device_ref, av_type, ptr::null(), ptr::null_mut(), 0)
1288    };
1289    if ret < 0 {
1290      return Err(Error::HwDeviceInitFailed(HwDeviceInitFailed::new(
1291        backend,
1292        ffmpeg_next::Error::from(ret),
1293      )));
1294    }
1295
1296    // The state `build_codec_context` already installed in `opaque`,
1297    // told which format this backend wants. One allocation, one seat:
1298    // the budget the judge reads and the declination the funnel reads
1299    // are the same object, and `Box::into_raw` hands its ownership to
1300    // the guard below without moving it — so the pointer the context
1301    // holds stays the one that is freed.
1302    state.wanted = hw_pix_fmt;
1303    state.wanted_int = hw_pix_fmt as i32;
1304    let callback_state = Box::into_raw(state);
1305    // RAII guard: from now until the end-of-function `into_owned()`, every
1306    // early return — `av_buffer_ref` failure, `open_as` failure, codec_type
1307    // mismatch, or any future error path added between here and the
1308    // `DecoderState` construction — frees `hw_device_ref` and
1309    // `callback_state` via the guard's Drop. Without it, each error site
1310    // had to remember to clean up these two FFI-owned resources by hand;
1311    // the codec_type-mismatch branch was missed and silently leaked one
1312    // device ref + one heap allocation per bad input.
1313    let guard = PartialBuildState {
1314      hw_device_ref,
1315      callback_state,
1316    };
1317
1318    // SAFETY: ctx is a freshly-constructed AVCodecContext we own;
1319    // av_buffer_ref bumps the refcount of the device buffer for FFmpeg's
1320    // use (we keep our own ref in `hw_device_ref` for cleanup).
1321    // av_buffer_ref returns NULL on allocation failure; we must check it
1322    // before assigning, otherwise the codec context would be opened with a
1323    // HW-flagged setup but no actual device reference.
1324    let device_ref_for_ctx = unsafe { av_buffer_ref(hw_device_ref) };
1325    if device_ref_for_ctx.is_null() {
1326      // guard's Drop frees hw_device_ref (the first ref) and callback_state.
1327      return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
1328        errno: libc::ENOMEM,
1329      }));
1330    }
1331    // SAFETY: device_ref_for_ctx is a valid AVBufferRef* from av_buffer_ref;
1332    // ctx is freshly built and owned by us. After this point ctx aliases
1333    // `callback_state` via `opaque` (FFmpeg never frees opaque, so
1334    // `callback_state` ownership stays with us / the guard) and aliases
1335    // `device_ref_for_ctx` (the second ref) via `hw_device_ctx` (FFmpeg
1336    // unrefs that on codec context drop, independent of the guard's first
1337    // ref).
1338    unsafe {
1339      let raw = ctx.as_mut_ptr();
1340      (*raw).hw_device_ctx = device_ref_for_ctx;
1341      (*raw).opaque = callback_state.cast();
1342      (*raw).get_format = Some(get_hw_format);
1343    }
1344
1345    // Open the decoder. On failure `ctx`/`opened` Drop releases the codec
1346    // context (and via that the second device ref); the guard releases the
1347    // first device ref and the callback state.
1348    //
1349    // We deliberately bypass `Opened::video()` because it calls
1350    // `Context::medium()`, which reads `AVCodecContext.codec_type` as the
1351    // bindgen `AVMediaType` enum — the same UB hazard we've been
1352    // systematically removing. Instead: validate `codec_type` as a raw
1353    // `c_int` ourselves, then construct the `decoder::Video` wrapper
1354    // directly via its public tuple field.
1355    // Through the funnel's free-standing half — there is no decoder yet
1356    // to ask, and the guard frees the callback state on the way out, so
1357    // the reason has to be collected here or not at all.
1358    let opened = match ctx.decoder().open_as(codec) {
1359      Ok(opened) => opened,
1360      Err(e) => return Err(ceiling_declination_of(callback_state).unwrap_or(Error::Ffmpeg(e))),
1361    };
1362
1363    // Validate codec_type as a raw integer — never construct AVMediaType
1364    // from an unvalidated runtime value. On failure `opened`'s Drop
1365    // releases the codec context; the guard releases the first
1366    // hw_device_ref and the callback state.
1367    if let Err(e) = ensure_video_codec_type(&opened) {
1368      // Same exit, same collection: a declined format can leave the
1369      // context looking like the wrong medium.
1370      return Err(ceiling_declination_of(callback_state).unwrap_or(e));
1371    }
1372    // SAFETY of construction: `decoder::Video` is `pub struct Video(pub Opened)`.
1373    // We construct via the public field; this is the same wrapping
1374    // `Opened::video()` does on success, just without the enum read.
1375    let opened = ffmpeg_next::decoder::Video(opened);
1376
1377    // Disarm the guard and transfer ownership of both resources into the
1378    // returned DecoderState (whose own Drop handles their lifetime).
1379    let (hw_device_ref, callback_state) = guard.into_owned();
1380    Ok(DecoderState {
1381      inner: ManuallyDrop::new(opened),
1382      backend,
1383      hw_device_ref,
1384      callback_state,
1385    })
1386  }
1387}
1388
1389/// RAII guard for the partially-owned FFmpeg state that
1390/// [`VideoDecoder::build_state`] holds between the
1391/// `av_hwdevice_ctx_create` and `Box::into_raw(CallbackState)`
1392/// allocations and the final `DecoderState` construction.
1393///
1394/// If `build_state` returns `Err` for any reason in that window
1395/// (`av_buffer_ref` ENOMEM, `open_as` failure, codec_type mismatch, or
1396/// any future error path), this guard's `Drop` releases
1397/// `hw_device_ref` — the first ref returned by `av_hwdevice_ctx_create`,
1398/// distinct from the second ref FFmpeg unrefs when the codec context
1399/// drops — and the boxed `CallbackState`, which FFmpeg never touches
1400/// because `AVCodecContext::opaque` is purely user-owned.
1401///
1402/// Successful construction calls [`Self::into_owned`] to disarm the
1403/// guard and hand both pointers to the new `DecoderState`.
1404struct PartialBuildState {
1405  hw_device_ref: *mut AVBufferRef,
1406  callback_state: *mut CallbackState,
1407}
1408
1409impl PartialBuildState {
1410  /// Disarm the guard: return the owned pointers and replace the guard's
1411  /// fields with null so its Drop is a no-op.
1412  fn into_owned(mut self) -> (*mut AVBufferRef, *mut CallbackState) {
1413    let hw = std::mem::replace(&mut self.hw_device_ref, ptr::null_mut());
1414    let cb = std::mem::replace(&mut self.callback_state, ptr::null_mut());
1415    (hw, cb)
1416  }
1417}
1418
1419impl Drop for PartialBuildState {
1420  fn drop(&mut self) {
1421    // SAFETY: pointers are either freshly allocated by `build_state` (via
1422    // `av_hwdevice_ctx_create` and `Box::into_raw`) or null after
1423    // `into_owned`. Both `av_buffer_unref` and `Box::from_raw` need the
1424    // null check we apply here; both are otherwise sound on resources we
1425    // own.
1426    unsafe {
1427      if !self.hw_device_ref.is_null() {
1428        let mut hw = self.hw_device_ref;
1429        av_buffer_unref(&mut hw);
1430      }
1431      if !self.callback_state.is_null() {
1432        drop(Box::from_raw(self.callback_state));
1433      }
1434    }
1435  }
1436}
1437
1438/// Download a HW frame into a CPU [`Frame`]. Always unrefs the destination
1439/// first so reuse across resolution changes is safe.
1440///
1441/// Deliberately does **not** call `av_frame_copy_props`. That FFmpeg
1442/// helper deep-copies AVFrame side data (SEI, mastering display, ICC
1443/// profiles, dynamic HDR, etc.), the metadata dict, and bumps both
1444/// `opaque_ref` and `private_ref` on every receive — none of which
1445/// `Frame` exposes via its public accessors. On a crafted stream with
1446/// megabytes of per-frame metadata that would mean an unbounded
1447/// allocation per receive, with no caller-visible benefit. We instead
1448/// copy only the scalar fields the public API can read (today: `pts`);
1449/// pixel layout (`width`, `height`, `format`, `linesize`, `data`) is
1450/// already set by `av_hwframe_transfer_data`. If `Frame` ever grows
1451/// accessors for timing extras (`duration`, `time_base`, `pkt_dts`) or
1452/// color metadata, add those to `copy_frame_props_minimal` at the same
1453/// time.
1454unsafe fn transfer_hw_frame(
1455  dst: &mut Frame,
1456  src: &mut frame::Video,
1457) -> std::result::Result<(), ffmpeg_next::Error> {
1458  unsafe {
1459    av_frame_unref(dst.as_inner_mut().as_mut_ptr());
1460    let ret = av_hwframe_transfer_data(dst.as_inner_mut().as_mut_ptr(), src.as_ptr(), 0);
1461    if ret < 0 {
1462      return Err(ffmpeg_next::Error::from(ret));
1463    }
1464    // Validate the post-transfer CPU pix_fmt against the safe `Frame`
1465    // accessor's supported set. FFmpeg picks the destination format
1466    // when `dst.format == AV_PIX_FMT_NONE` on entry (which it always is
1467    // here — `av_frame_unref` clears it) by walking the result of
1468    // `av_hwframe_transfer_get_formats`. Driver/version ordering can
1469    // pick a layout outside our NV*/P0xx/P2xx/P4xx set; the call would
1470    // return success while the resulting frame is unreadable through
1471    // `Frame::row` / `Frame::as_ptr` (those return `None` for
1472    // unsupported formats). Surface the unsupported result as a
1473    // transfer failure so `receive_frame`'s probe-active path advances
1474    // to the next backend rather than collapsing on an unusable frame;
1475    // post-probe, the caller gets an `Err` they can branch into a
1476    // software fallback.
1477    let dst_raw_fmt: i32 = (*dst.as_inner_mut().as_ptr()).format;
1478    let dst_pix_fmt = crate::boundary::from_av_pixel_format(dst_raw_fmt);
1479    if !crate::frame::is_supported_cpu_pix_fmt(&dst_pix_fmt) {
1480      tracing::warn!(
1481        pix_fmt = dst_raw_fmt,
1482        "hwdecode: hw->cpu transfer produced unsupported pix_fmt; \
1483         treating as backend failure"
1484      );
1485      av_frame_unref(dst.as_inner_mut().as_mut_ptr());
1486      return Err(ffmpeg_next::Error::Other {
1487        errno: libc::EINVAL,
1488      });
1489    }
1490    if let Err(e) = copy_frame_props_minimal(dst.as_inner_mut().as_mut_ptr(), src.as_ptr()) {
1491      // Failed to propagate metadata. Reset the destination so the
1492      // partial frame doesn't leak (its pixel buffers were attached
1493      // by `av_hwframe_transfer_data` above) and surface as a
1494      // backend failure — the probe path will advance to the next
1495      // candidate; post-probe, the caller branches into SW fallback.
1496      av_frame_unref(dst.as_inner_mut().as_mut_ptr());
1497      return Err(e);
1498    }
1499  }
1500  Ok(())
1501}
1502
1503/// Copies AVFrame metadata (timestamps, color metadata, crop rect,
1504/// flags, side data, etc.) from the source HW frame to the destination
1505/// CPU frame so the post-transfer frame surfaces the same metadata a
1506/// SW-decoded frame would.
1507///
1508/// Defers to FFmpeg's `av_frame_copy_props`, which handles the per-
1509/// `side_data[i]` allocation, dict copy, and refcounted buffer
1510/// replacements internally. The cost is bounded by what the source
1511/// frame attaches — typical HDR streams carry 1–3 side-data entries
1512/// (mastering display, content light level, dolby/HDR10+ dynamic
1513/// metadata) totalling a few hundred bytes, so per-frame allocation
1514/// overhead stays negligible relative to the pixel data already
1515/// transferred via `av_hwframe_transfer_data`.
1516///
1517/// # Safety
1518/// Both pointers must be valid `AVFrame` pointers we own. We do not
1519/// form `&AVFrame` — `av_frame_copy_props` operates on raw pointers
1520/// directly.
1521/// Sum the byte sizes of every entry in `(*frame).side_data[]`.
1522/// Used by the probe replay queue's byte-cap accounting so a
1523/// frame's deep-copied side-data is charged against
1524/// `max_probe_pending_bytes` along with its pixel buffers.
1525///
1526/// # Safety
1527/// `frame` must be a live `*const AVFrame`. Reads only `nb_side_data`,
1528/// the `side_data` pointer array, and each `AVFrameSideData.size` —
1529/// no `&AVFrame` reference is formed.
1530unsafe fn sum_side_data_bytes(frame: *const AVFrame) -> usize {
1531  // Clamp `nb_side_data` to the same entry cap the copy path
1532  // enforces. Without the clamp, a decoder-controlled or
1533  // version-skew `nb_side_data` value (the bindgen field is
1534  // `c_int`, signed) could drive this walk arbitrarily long
1535  // before the cap downstream kicks in. Negative values are
1536  // pinned to zero before casting.
1537  let raw = unsafe { (*frame).nb_side_data };
1538  let arr = unsafe { (*frame).side_data };
1539  if raw <= 0 || arr.is_null() {
1540    return 0;
1541  }
1542  let count = (raw as usize).min(HW_COPY_SIDE_DATA_MAX_ENTRIES);
1543  let mut total: usize = 0;
1544  for i in 0..count {
1545    // SAFETY: `arr` points to `nb_side_data` valid `*mut AVFrameSideData`
1546    // entries per FFmpeg's contract; `i < count` is in-bounds.
1547    let entry = unsafe { *arr.add(i) };
1548    if entry.is_null() {
1549      continue;
1550    }
1551    let sz = unsafe { (*entry).size };
1552    total = total.saturating_add(sz);
1553    if total >= HW_COPY_SIDE_DATA_MAX_TOTAL_BYTES {
1554      // Already at or above the byte cap — further entries can't
1555      // change the projected-vs-cap decision the caller makes.
1556      total = HW_COPY_SIDE_DATA_MAX_TOTAL_BYTES;
1557      break;
1558    }
1559  }
1560  total
1561}
1562
1563/// Hard cap on the number of `AVFrameSideData` entries we copy from
1564/// HW source frame to CPU destination frame on the HW transfer
1565/// path. Mirrors `convert::SIDE_DATA_MAX_ENTRIES`; the public
1566/// converter re-enforces the same cap so this is defense in depth.
1567const HW_COPY_SIDE_DATA_MAX_ENTRIES: usize = 64;
1568/// Hard cap on the total side-data byte budget per HW transfer.
1569/// Mirrors `convert::SIDE_DATA_MAX_TOTAL_BYTES`.
1570const HW_COPY_SIDE_DATA_MAX_TOTAL_BYTES: usize = 256 * 1024;
1571
1572/// Maps a raw `AV_FRAME_DATA_*` integer to the matching bindgen
1573/// `AVFrameSideDataType` enum value when (and only when) the integer
1574/// is a known discriminant in the linked FFmpeg's bindgen output.
1575/// Returns `None` for unknown / version-skew / corrupt values —
1576/// the caller drops those entries instead of `transmute`-ing an
1577/// arbitrary integer back into the enum (which would be immediate
1578/// UB if the discriminant isn't in the enum's set).
1579///
1580/// The whitelist covers the entries safe to preserve across HW
1581/// transfer:
1582/// - HDR10 / HDR10+ / Dolby Vision / Vivid / ambient HDR metadata
1583/// - SMPTE / GOP timecodes
1584/// - ICC color profile
1585/// - A53 closed captions
1586/// - Spherical / display matrix orientation
1587/// - Stereo3D layout
1588///
1589/// Other AV_FRAME_DATA_* constants exist (motion vectors, encoder
1590/// params, RPU buffers, …) but are either decoder-internal or
1591/// rarely useful through the public mediadecode API; dropping them
1592/// is the safe default.
1593fn whitelisted_side_data_kind(kind_raw: i32) -> Option<ffmpeg_next::ffi::AVFrameSideDataType> {
1594  use ffmpeg_next::ffi::AVFrameSideDataType;
1595  // Each match arm compares `kind_raw` against the i32 cast of a
1596  // known constant, then returns the constant itself — we never
1597  // construct the enum from arbitrary integer bytes.
1598  let kind = match kind_raw {
1599    x if x == AVFrameSideDataType::AV_FRAME_DATA_PANSCAN as i32 => {
1600      AVFrameSideDataType::AV_FRAME_DATA_PANSCAN
1601    }
1602    x if x == AVFrameSideDataType::AV_FRAME_DATA_A53_CC as i32 => {
1603      AVFrameSideDataType::AV_FRAME_DATA_A53_CC
1604    }
1605    x if x == AVFrameSideDataType::AV_FRAME_DATA_STEREO3D as i32 => {
1606      AVFrameSideDataType::AV_FRAME_DATA_STEREO3D
1607    }
1608    x if x == AVFrameSideDataType::AV_FRAME_DATA_DISPLAYMATRIX as i32 => {
1609      AVFrameSideDataType::AV_FRAME_DATA_DISPLAYMATRIX
1610    }
1611    x if x == AVFrameSideDataType::AV_FRAME_DATA_AFD as i32 => {
1612      AVFrameSideDataType::AV_FRAME_DATA_AFD
1613    }
1614    x if x == AVFrameSideDataType::AV_FRAME_DATA_MASTERING_DISPLAY_METADATA as i32 => {
1615      AVFrameSideDataType::AV_FRAME_DATA_MASTERING_DISPLAY_METADATA
1616    }
1617    x if x == AVFrameSideDataType::AV_FRAME_DATA_GOP_TIMECODE as i32 => {
1618      AVFrameSideDataType::AV_FRAME_DATA_GOP_TIMECODE
1619    }
1620    x if x == AVFrameSideDataType::AV_FRAME_DATA_SPHERICAL as i32 => {
1621      AVFrameSideDataType::AV_FRAME_DATA_SPHERICAL
1622    }
1623    x if x == AVFrameSideDataType::AV_FRAME_DATA_CONTENT_LIGHT_LEVEL as i32 => {
1624      AVFrameSideDataType::AV_FRAME_DATA_CONTENT_LIGHT_LEVEL
1625    }
1626    x if x == AVFrameSideDataType::AV_FRAME_DATA_ICC_PROFILE as i32 => {
1627      AVFrameSideDataType::AV_FRAME_DATA_ICC_PROFILE
1628    }
1629    x if x == AVFrameSideDataType::AV_FRAME_DATA_S12M_TIMECODE as i32 => {
1630      AVFrameSideDataType::AV_FRAME_DATA_S12M_TIMECODE
1631    }
1632    x if x == AVFrameSideDataType::AV_FRAME_DATA_DYNAMIC_HDR_PLUS as i32 => {
1633      AVFrameSideDataType::AV_FRAME_DATA_DYNAMIC_HDR_PLUS
1634    }
1635    x if x == AVFrameSideDataType::AV_FRAME_DATA_REGIONS_OF_INTEREST as i32 => {
1636      AVFrameSideDataType::AV_FRAME_DATA_REGIONS_OF_INTEREST
1637    }
1638    x if x == AVFrameSideDataType::AV_FRAME_DATA_SEI_UNREGISTERED as i32 => {
1639      AVFrameSideDataType::AV_FRAME_DATA_SEI_UNREGISTERED
1640    }
1641    x if x == AVFrameSideDataType::AV_FRAME_DATA_FILM_GRAIN_PARAMS as i32 => {
1642      AVFrameSideDataType::AV_FRAME_DATA_FILM_GRAIN_PARAMS
1643    }
1644    x if x == AVFrameSideDataType::AV_FRAME_DATA_DOVI_RPU_BUFFER as i32 => {
1645      AVFrameSideDataType::AV_FRAME_DATA_DOVI_RPU_BUFFER
1646    }
1647    x if x == AVFrameSideDataType::AV_FRAME_DATA_DOVI_METADATA as i32 => {
1648      AVFrameSideDataType::AV_FRAME_DATA_DOVI_METADATA
1649    }
1650    x if x == AVFrameSideDataType::AV_FRAME_DATA_DYNAMIC_HDR_VIVID as i32 => {
1651      AVFrameSideDataType::AV_FRAME_DATA_DYNAMIC_HDR_VIVID
1652    }
1653    x if x == AVFrameSideDataType::AV_FRAME_DATA_AMBIENT_VIEWING_ENVIRONMENT as i32 => {
1654      AVFrameSideDataType::AV_FRAME_DATA_AMBIENT_VIEWING_ENVIRONMENT
1655    }
1656    _ => return None,
1657  };
1658  Some(kind)
1659}
1660
1661unsafe fn copy_frame_props_minimal(
1662  dst: *mut AVFrame,
1663  src: *const AVFrame,
1664) -> std::result::Result<(), ffmpeg_next::Error> {
1665  // We deliberately do NOT use `av_frame_copy_props` here, despite
1666  // its convenience. Upstream `av_frame_copy_props` deep-copies
1667  // *every* `AVFrameSideData` entry, the metadata `AVDictionary`,
1668  // and refcounted `opaque_ref` / `private_ref` buffers — all from
1669  // attacker-controlled decoder output. A crafted stream with many
1670  // multi-MiB side-data entries could drive the per-frame
1671  // allocation cost arbitrarily high (one alloc per entry, with the
1672  // entry's bytes copied via `memcpy`). The downstream
1673  // `convert::collect_side_data` cap helps the *Rust* side but the
1674  // FFmpeg-side allocations have already happened.
1675  //
1676  // Instead we copy scalar fields manually (timestamps, color
1677  // metadata, picture type, flags) and copy side-data with a hard
1678  // cap matching the converter's. Metadata dict and opaque_ref /
1679  // private_ref are intentionally NOT copied — they're rarely
1680  // populated on decoded frames and represent unbounded surfaces.
1681  use core::ptr::{addr_of, addr_of_mut, read_unaligned, write_unaligned};
1682  use ffmpeg_next::ffi::av_frame_new_side_data;
1683  unsafe {
1684    // Scalar timestamps / flags / color / SAR / crop. None of
1685    // these allocate.
1686    (*dst).pts = (*src).pts;
1687    (*dst).pkt_dts = (*src).pkt_dts;
1688    (*dst).duration = (*src).duration;
1689    (*dst).best_effort_timestamp = (*src).best_effort_timestamp;
1690    (*dst).quality = (*src).quality;
1691    (*dst).repeat_pict = (*src).repeat_pict;
1692    (*dst).flags = (*src).flags;
1693    (*dst).sample_aspect_ratio = (*src).sample_aspect_ratio;
1694    (*dst).crop_left = (*src).crop_left;
1695    (*dst).crop_top = (*src).crop_top;
1696    (*dst).crop_right = (*src).crop_right;
1697    (*dst).crop_bottom = (*src).crop_bottom;
1698    (*dst).time_base = (*src).time_base;
1699
1700    // Enum-typed fields: bit-copy raw to avoid materializing an
1701    // invalid `AVColorPrimaries` etc. on either side. `read_unaligned`
1702    // / `write_unaligned` on `i32` projections sidestep the bindgen
1703    // enum's discriminant-validity invariant.
1704    let pict_type_raw = read_unaligned(addr_of!((*src).pict_type) as *const i32);
1705    write_unaligned(addr_of_mut!((*dst).pict_type) as *mut i32, pict_type_raw);
1706    let cp_raw = read_unaligned(addr_of!((*src).color_primaries) as *const i32);
1707    write_unaligned(addr_of_mut!((*dst).color_primaries) as *mut i32, cp_raw);
1708    let trc_raw = read_unaligned(addr_of!((*src).color_trc) as *const i32);
1709    write_unaligned(addr_of_mut!((*dst).color_trc) as *mut i32, trc_raw);
1710    let cs_raw = read_unaligned(addr_of!((*src).colorspace) as *const i32);
1711    write_unaligned(addr_of_mut!((*dst).colorspace) as *mut i32, cs_raw);
1712    let cr_raw = read_unaligned(addr_of!((*src).color_range) as *const i32);
1713    write_unaligned(addr_of_mut!((*dst).color_range) as *mut i32, cr_raw);
1714    let cl_raw = read_unaligned(addr_of!((*src).chroma_location) as *const i32);
1715    write_unaligned(addr_of_mut!((*dst).chroma_location) as *mut i32, cl_raw);
1716
1717    // Side-data: bounded copy. `av_frame_new_side_data(dst, type,
1718    // size)` allocates the entry and returns a pointer to write
1719    // the payload bytes into; a null return is the OOM signal.
1720    // Callers (`transfer_hw_frame`, `drain_into_pending`) hand us
1721    // freshly-unref'd `dst` frames, so any prior side-data has
1722    // already been freed by `av_frame_unref` — we don't need to
1723    // strip dst's existing side-data here.
1724    // Read `nb_side_data` as the bindgen `c_int` and clamp non-
1725    // positive values BEFORE casting to `usize`. A negative value
1726    // (corrupt / version-skew decoder output) cast directly to
1727    // `usize` becomes a huge positive count and would walk OOB
1728    // memory below; pinning to zero up front collapses that to a
1729    // no-op. Same signed-count guard `sum_side_data_bytes` applies.
1730    let nb_side_data_raw = (*src).nb_side_data;
1731    let src_arr = (*src).side_data;
1732    if nb_side_data_raw > 0 && !src_arr.is_null() {
1733      let count_raw = nb_side_data_raw as usize;
1734      let count = count_raw.min(HW_COPY_SIDE_DATA_MAX_ENTRIES);
1735      if count_raw > HW_COPY_SIDE_DATA_MAX_ENTRIES {
1736        tracing::warn!(
1737          cap = HW_COPY_SIDE_DATA_MAX_ENTRIES,
1738          requested = count_raw,
1739          "mediadecode-ffmpeg: HW->CPU transfer side-data entry cap reached; truncating",
1740        );
1741      }
1742      let mut total_bytes: usize = 0;
1743      for i in 0..count {
1744        let entry = *src_arr.add(i);
1745        if entry.is_null() {
1746          continue;
1747        }
1748        let kind_raw = read_unaligned(addr_of!((*entry).type_) as *const i32);
1749        let size = (*entry).size;
1750        let data_ptr = (*entry).data;
1751        if size == 0 || data_ptr.is_null() {
1752          continue;
1753        }
1754        // Whitelist gate: only proceed when `kind_raw` matches a
1755        // known `AV_FRAME_DATA_*` constant the linked FFmpeg's
1756        // bindgen output knows about. Without this gate, a
1757        // version-skew or hostile decoder could write a side-data
1758        // type integer outside our bindgen's discriminant set, and
1759        // constructing the `AVFrameSideDataType` enum value (so
1760        // we could pass it to `av_frame_new_side_data`) would be
1761        // immediate UB before the call. Unknown types are dropped
1762        // with a debug-level log — the public converter's
1763        // `collect_side_data` walks the destination raw and would
1764        // also surface them as bare integers in `SideDataEntry.kind`.
1765        let Some(kind_enum) = whitelisted_side_data_kind(kind_raw) else {
1766          tracing::debug!(
1767            kind_raw,
1768            "mediadecode-ffmpeg: unknown AV_FRAME_DATA type during HW->CPU transfer; dropping",
1769          );
1770          continue;
1771        };
1772        let projected = total_bytes.saturating_add(size);
1773        if projected > HW_COPY_SIDE_DATA_MAX_TOTAL_BYTES {
1774          tracing::warn!(
1775            cap = HW_COPY_SIDE_DATA_MAX_TOTAL_BYTES,
1776            projected,
1777            "mediadecode-ffmpeg: HW->CPU transfer side-data byte cap reached; dropping rest",
1778          );
1779          break;
1780        }
1781        let new_entry = av_frame_new_side_data(dst, kind_enum, size);
1782        if new_entry.is_null() {
1783          // **OOM is reported, not absorbed.** This used to `break` and
1784          // return `Ok(())`, which published a frame carrying whatever
1785          // side data happened to fit before the allocator gave out —
1786          // silently dropping the entries behind it. Those entries are
1787          // the HDR mastering metadata, the ICC profile and the display
1788          // matrix: a picture that comes back with its colours or its
1789          // orientation quietly missing is worse than one that does not
1790          // come back, because nothing downstream can tell.
1791          //
1792          // The caller already knows what to do with an error here: it
1793          // unrefs the partial destination and either advances to the
1794          // next backend or surfaces the failure for a software retry.
1795          tracing::warn!("mediadecode-ffmpeg: av_frame_new_side_data OOM during HW->CPU transfer",);
1796          return Err(ffmpeg_next::Error::Other {
1797            errno: libc::ENOMEM,
1798          });
1799        }
1800        // SAFETY: `(*new_entry).data` is allocated for `size` bytes
1801        // per av_frame_new_side_data's contract; `data_ptr` is
1802        // valid for `size` reads per AVFrameSideData's contract.
1803        core::ptr::copy_nonoverlapping(data_ptr, (*new_entry).data, size);
1804        total_bytes = projected;
1805      }
1806    }
1807  }
1808  Ok(())
1809}
1810
1811/// `EAGAIN` and `EOF` are normal flow signals from `avcodec_receive_frame`
1812/// and must not be treated as backend failures.
1813fn is_transient(e: &ffmpeg_next::Error) -> bool {
1814  is_eagain(e) || matches!(e, ffmpeg_next::Error::Eof)
1815}
1816
1817/// Post-commit, a HW-only decoder's non-transient, non-EOF error means the
1818/// committed HW backend can't decode this content → fall back to SW. VT's
1819/// "hardware accelerator failed" surfaces as AVERROR_EXTERNAL; some HW
1820/// backends report unsupported geometry as InvalidData; context loss as
1821/// Bug/Bug2/Unknown. Broad-by-design (decode-all-kinds); fixtures will let us
1822/// narrow if a real backend proves a code should NOT trigger fallback.
1823///
1824/// `EAGAIN`/`EOF` are deliberately excluded by the caller (each call site
1825/// guards on `is_transient` first): `EAGAIN` is backpressure and `EOF` is a
1826/// genuine end-of-stream that must propagate, never be trapped in an infinite
1827/// fallback-retry loop. `Other { errno: EINVAL }` from the HW→CPU transfer
1828/// path is also covered — an unsupported CPU output pix_fmt is a HW-output
1829/// problem, never input corruption.
1830fn is_hw_decode_failure(e: &ffmpeg_next::Error) -> bool {
1831  matches!(
1832    e,
1833    ffmpeg_next::Error::External
1834      | ffmpeg_next::Error::Bug
1835      | ffmpeg_next::Error::Bug2
1836      | ffmpeg_next::Error::Unknown
1837      | ffmpeg_next::Error::InvalidData
1838      | ffmpeg_next::Error::Other {
1839        errno: libc::EINVAL
1840      }
1841  )
1842}
1843
1844/// Reject a `codec::Parameters` whose inner `*mut AVCodecParameters` is
1845/// null. This guards the public trust boundary: ffmpeg-next can produce
1846/// such a `Parameters` under OOM (`Parameters::new()` does not check
1847/// `avcodec_parameters_alloc`), and a safe caller can legally hand one
1848/// in. Without this check, the very next `(*p.as_ptr()).field` read
1849/// would be a null deref.
1850fn ensure_parameters_non_null(parameters: &codec::Parameters) -> Result<()> {
1851  // SAFETY: as_ptr() returns the inner *const AVCodecParameters; we just
1852  // inspect the pointer value (no deref).
1853  if unsafe { parameters.as_ptr() }.is_null() {
1854    return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
1855      errno: libc::ENOMEM,
1856    }));
1857  }
1858  Ok(())
1859}
1860
1861/// Allocate a fresh `frame::Video`, checking that `av_frame_alloc` did not
1862/// return NULL. ffmpeg-next's `frame::Video::empty()` does not surface that
1863/// failure and the resulting null pointer would be UB on the next field
1864/// access; this wrapper catches it and surfaces it as `ENOMEM`.
1865fn alloc_av_frame() -> std::result::Result<frame::Video, ffmpeg_next::Error> {
1866  let inner = frame::Video::empty();
1867  // SAFETY: as_ptr() just exposes the inner pointer for inspection.
1868  if unsafe { inner.as_ptr() }.is_null() {
1869    return Err(ffmpeg_next::Error::Other {
1870      errno: libc::ENOMEM,
1871    });
1872  }
1873  Ok(inner)
1874}
1875
1876/// Build a fresh `Context` from `parameters`, checking the underlying
1877/// `avcodec_alloc_context3` for NULL before passing it to
1878/// `avcodec_parameters_to_context`. ffmpeg-next's `Context::from_parameters`
1879/// skips that check and would feed a null pointer into FFmpeg under OOM —
1880/// undefined behavior. This helper surfaces the failure as `ENOMEM` and
1881/// frees the context if `parameters_to_context` itself errors.
1882pub(crate) fn build_codec_context(
1883  parameters: &codec::Parameters,
1884  limits: crate::limits::DecoderLimits,
1885) -> Result<(Context, Box<CallbackState>)> {
1886  ensure_parameters_non_null(parameters)?;
1887  // **The choke point.** `avcodec_parameters_to_context` below is a
1888  // wholesale copy *into* FFmpeg — it duplicates `extradata`, every
1889  // `coded_side_data` entry and the channel map into the context, at
1890  // whatever size the caller's parameters declare. Every road that
1891  // opens a decoder in this crate arrives here, so measuring and
1892  // admitting once, right here, is what stops a caller handing
1893  // libavcodec parameters nobody budgeted: the four session `open`s,
1894  // the HW probe's `build_state`, its per-backend advances, and the
1895  // software fallback all pass through this function and none of them
1896  // can reach `avcodec_parameters_to_context` any other way.
1897  //
1898  // The outbound clone (`extras::bounded_clone_parameters`) closed the
1899  // Rust-side copy; this closes the FFmpeg-side one. They are the same
1900  // budget.
1901  //
1902  // SAFETY: `ensure_parameters_non_null` just proved the pointer is
1903  // live; the measurement allocates nothing.
1904  let footprint = unsafe { crate::extras::measure_parameters(parameters.as_ptr()) };
1905  let declared = footprint.and_then(|f| f.total()).unwrap_or(usize::MAX);
1906  if declared > limits.max_codec_parameter_bytes() {
1907    return Err(Error::ParametersTooLarge(
1908      crate::demuxer::ParametersTooLarge::new(0, declared, limits.max_codec_parameter_bytes()),
1909    ));
1910  }
1911  // SAFETY: avcodec_alloc_context3(NULL) returns a fresh AVCodecContext
1912  // or NULL on allocation failure.
1913  let ctx_ptr = unsafe { avcodec_alloc_context3(ptr::null()) };
1914  if ctx_ptr.is_null() {
1915    return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
1916      errno: libc::ENOMEM,
1917    }));
1918  }
1919  // SAFETY: ctx_ptr is non-null and freshly allocated; parameters.as_ptr()
1920  // returns a valid AVCodecParameters pointer; the function copies bytes
1921  // out of parameters into the context.
1922  let ret = unsafe { avcodec_parameters_to_context(ctx_ptr, parameters.as_ptr()) };
1923  if ret < 0 {
1924    // SAFETY: ctx_ptr was allocated by us and never handed to anyone else.
1925    let mut p = ctx_ptr;
1926    unsafe { avcodec_free_context(&mut p) };
1927    return Err(Error::Ffmpeg(ffmpeg_next::Error::from(ret)));
1928  }
1929  // **The push-down.** The same pixel ceiling this crate checks against
1930  // a decoded frame is written into the decoder itself, so libavcodec
1931  // refuses an oversized picture *before allocating it*. Checking only
1932  // on our side would mean FFmpeg had already paid for the frame by the
1933  // time we declined to copy it — two layers, one number, and this is
1934  // the layer that matters.
1935  //
1936  // FFmpeg's own default here is `INT_MAX`, i.e. no ceiling worth the
1937  // name. `max_pixels` is a plain `int64_t` field on `AVCodecContext`
1938  // (and has been since FFmpeg 4.0), so it is set directly rather than
1939  // through `av_opt_set_int` and a stringly-typed option name.
1940  //
1941  // **And the byte ceiling, pushed down through the same field.**
1942  //
1943  // The pixel ceiling alone does not bound bytes, because a pixel is not
1944  // a fixed price: 10000x10000 is 100 Mpx — comfortably under the 256
1945  // Mpx default — and in `rgba64` it is 800 MB, well over the 512 MiB
1946  // byte ceiling. A highly compressible frame of that shape is a few KB
1947  // on disk, so nothing upstream sees it coming.
1948  //
1949  // **`max_pixels` carries the caller's number, verbatim.** It used to
1950  // carry `min(that, max_frame_bytes / worst-bytes-per-pixel)`, so the
1951  // byte ceiling could be enforced before libavcodec allocated — and
1952  // that translation charged every stream the widest format in
1953  // existence, 16 bytes a pixel. A 1920x1080 `yuv420p` frame costs
1954  // 3.14 MiB and was refused under a 4 MiB budget, at
1955  // `ff_set_dimensions`, before anything accurate had a chance to look
1956  // at it. Over-refusing ordinary video is not a conservative failure;
1957  // it is a broken decoder.
1958  //
1959  // The translation is gone because it is no longer needed: the byte
1960  // ceiling is enforced by [`judge_buffer`], which is *also* a
1961  // pre-allocation seat — `get_buffer2` is the allocator, so it runs
1962  // before the allocation and prices the frame's real format at its
1963  // real aligned dimensions. Nothing is lost on the software road by
1964  // stating the pixel limit as what it is.
1965  //
1966  // SAFETY: `ctx_ptr` is the non-null context just allocated and
1967  // populated above; `max_pixels` is a public field.
1968  unsafe {
1969    (*ctx_ptr).max_pixels = i64::try_from(limits.frame().max_pixels()).unwrap_or(i64::MAX);
1970  }
1971
1972  // **The byte ceiling's own seat, in the allocator itself.**
1973  // `max_pixels` bounds an extent; what an extent costs depends on its
1974  // format and on how the allocator aligns it — a `gray8` frame of
1975  // 65536x1 is 64 KiB by `w * h` and 2 MiB once its single row is
1976  // rounded up. No scalar compared against a pixel product can bound
1977  // that, so the byte question is asked where the answer is knowable:
1978  // in `get_buffer2`, which *is* the allocation, against the caller's
1979  // own `max_frame_bytes`.
1980  //
1981  // See [`judge_buffer`] for why this hook rather than `get_format`
1982  // (measured: `get_format` never fires for a one-shot `png` decode).
1983  //
1984  // SAFETY: `ctx_ptr` is the non-null context; `get_buffer2` is a
1985  // public function-pointer field, and `judge_buffer` delegates every
1986  // frame it accepts to the allocator libavcodec would have used.
1987  unsafe {
1988    (*ctx_ptr).get_buffer2 = Some(judge_buffer);
1989  }
1990
1991  // **`max_samples` is deliberately left alone.**
1992  //
1993  // It bounds `nb_samples * channels`, so bounding *bytes* with it
1994  // means dividing by a per-channel-sample cost — and the only sound
1995  // divisor is the widest sample format the build can emit, 8 bytes.
1996  // That charged every stream `f64` rates: a 6-channel `s16` frame
1997  // fitting a 64 KiB budget was refused, because the translation
1998  // priced it at four times its real cost.
1999  //
2000  // The audio pre-allocation story is now the same as the video one,
2001  // and it is stronger than the translation was: [`judge_buffer`] runs
2002  // in `get_buffer2`, before the planes are allocated, and prices the
2003  // frame's real sample format at its real channel count through
2004  // [`crate::footprint`] — which asks `av_samples_get_buffer_size`, the
2005  // allocator's own ruler. An exact judge at the allocation beats an
2006  // approximate one before it.
2007
2008  // **The judge's budget seat.** `judge_buffer` runs as a C callback
2009  // with nothing but the context to read, and the byte ceiling is not
2010  // recoverable from any field on it — see
2011  // [`CallbackState::max_frame_bytes`]. So the state that already
2012  // carries the `get_format` declination carries the budget too, and
2013  // every road gets one: this is the single point every decoder in the
2014  // crate is built through.
2015  //
2016  // Ownership stays with the caller, which keeps the box alive for as
2017  // long as the context. `Box` contents do not move when the box does,
2018  // so the pointer installed here stays valid across the return.
2019  let mut state = Box::new(CallbackState {
2020    wanted: ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_NONE,
2021    wanted_int: ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_NONE as i32,
2022    ceiling_declined: core::sync::atomic::AtomicBool::new(false),
2023    declined_pixels: core::sync::atomic::AtomicI64::new(0),
2024    declined_limit: core::sync::atomic::AtomicI64::new(0),
2025    max_frame_bytes: limits.frame().max_frame_bytes() as u64,
2026    frame_budget_declined: core::sync::atomic::AtomicBool::new(false),
2027    declined_frame_bytes: core::sync::atomic::AtomicU64::new(0),
2028    declined_frame_audio: core::sync::atomic::AtomicBool::new(false),
2029  });
2030  // SAFETY: `ctx_ptr` is the non-null context; `opaque` is a public
2031  // field FFmpeg never reads or frees.
2032  unsafe {
2033    (*ctx_ptr).opaque = (&raw mut *state).cast();
2034  }
2035
2036  // SAFETY: ctx_ptr is valid; passing `owner: None` means our wrapper owns
2037  // the allocation and `Context::drop` will run `avcodec_free_context`.
2038  Ok((unsafe { Context::wrap(ctx_ptr, None) }, state))
2039}
2040
2041/// Checked deep-clone of `codec::Parameters`. ffmpeg-next's
2042/// `Parameters::clone` allocates via `avcodec_parameters_alloc` without
2043/// checking for NULL and runs `avcodec_parameters_copy` without checking
2044/// the return code. On `ENOMEM` the result is a `Parameters` with a null
2045/// inner pointer, which becomes UB when later passed to FFmpeg.
2046///
2047/// This helper performs both calls explicitly, frees a partial allocation
2048/// on failure, and surfaces the AVERROR. The returned `Parameters` has
2049/// `owner: None`, severing any Rc link to the caller's demuxer (the
2050/// reason we deep-clone in the first place — see Send safety in
2051/// `VideoDecoder::open`).
2052pub(crate) fn try_clone_parameters(
2053  src: &codec::Parameters,
2054  budget: usize,
2055) -> std::result::Result<codec::Parameters, Error> {
2056  // Through the bounded clone, like every other parameter copy in this
2057  // crate — see [`crate::extras::bounded_clone_parameters`] for the
2058  // rule and why the wholesale `avcodec_parameters_copy` this used to
2059  // call is gone. This path is attacker-facing: `VideoDecoder::open`
2060  // takes whatever `stream.parameters()` hands it, straight off a
2061  // container.
2062  //
2063  // `budget` is the **active** ceiling, threaded from the session's own
2064  // `DecoderLimits` — through the initial ownership clone, the probe
2065  // state's copy, every probe advance and the software fallback. It
2066  // used to be the crate default, so a lowered ceiling did not bind
2067  // here (the clone admitted 16 MiB whatever the caller configured,
2068  // and only `build_codec_context` downstream refused) and a raised one
2069  // could not be used at all.
2070  //
2071  // The stream index is reported as 0: this helper is handed
2072  // parameters, not a stream, and inventing a coordinate it cannot
2073  // know would be worse than admitting it has none.
2074  crate::extras::bounded_clone_parameters(src, 0, budget).map_err(|e| match e {
2075    crate::demuxer::DemuxError::ParametersTooLarge(p) => Error::ParametersTooLarge(p),
2076    crate::demuxer::DemuxError::ParametersCopy(p) => Error::Ffmpeg(*p.source()),
2077    // A missing or unallocatable destination is the out-of-memory this
2078    // helper has always reported.
2079    _ => Error::Ffmpeg(ffmpeg_next::Error::Other {
2080      errno: libc::ENOMEM,
2081    }),
2082  })
2083}
2084
2085/// Checked counterpart to `Packet::clone()`. ffmpeg-next's `clone_from`
2086/// calls `av_packet_ref` and ignores the int return value; on `ENOMEM`
2087/// the destination is left empty while the caller assumes the clone
2088/// succeeded — corrupting any later replay history. This helper surfaces
2089/// the AVERROR. The result is a refcounted shallow clone — the payload
2090/// buffer is shared with `src` rather than deep-copied; the probe replay
2091/// only sends packets through `avcodec_send_packet`, which does not
2092/// require a writable buffer.
2093pub(crate) fn try_clone_packet(src: &Packet) -> std::result::Result<Packet, ffmpeg_next::Error> {
2094  let mut dst = Packet::empty();
2095  // SAFETY: dst is a freshly zero-initialized Packet (av_init_packet inside
2096  // Packet::empty); av_packet_ref initializes its data fields from src's
2097  // refcounted buffer or returns AVERROR(ENOMEM) on failure.
2098  let ret = unsafe { av_packet_ref(dst.as_mut_ptr(), src.as_ptr()) };
2099  if ret < 0 {
2100    return Err(ffmpeg_next::Error::from(ret));
2101  }
2102  Ok(dst)
2103}
2104
2105/// Sum of `AVPacket.side_data[i].size` across every entry, plus
2106/// `nb_entries * SIDE_DATA_ENTRY_OVERHEAD` (descriptor + AVBufferRef +
2107/// allocator bookkeeping per entry). `av_packet_ref` performs a deep
2108/// copy of side data via `av_packet_copy_props`, so each probe-buffered
2109/// clone retains every one of these bytes. Charging both keeps
2110/// `MAX_PROBE_PACKET_BYTES` a true upper bound — without the overhead,
2111/// many zero-size entries slip past the cap on pure descriptor cost.
2112///
2113/// Walks at most `max_entries` entries even when `side_data_elems`
2114/// reports a larger count. Defense-in-depth against a corrupt or hostile
2115/// packet whose `side_data_elems` lies about the actual array length:
2116/// the caller is expected to also reject any packet whose count exceeds
2117/// the cap (so the inflated clone is never created), but bounding the
2118/// walk here means a stale or weaponised value can never trigger an
2119/// unbounded raw-pointer scan from the safe API.
2120///
2121/// Reads only the `size` field of each `AVPacketSideData` entry — never
2122/// touches the bindgen `AVPacketSideDataType` enum, so no UB even if a
2123/// future FFmpeg adds a side-data type discriminant our build doesn't
2124/// know.
2125pub(crate) fn packet_side_data_bytes(packet: &Packet, max_entries: usize) -> usize {
2126  // SAFETY: AVPacket.side_data is `*mut AVPacketSideData` and
2127  // side_data_elems is `c_int`; both are raw struct fields safe to read.
2128  // Field projection (`.size`) does not reconstruct the enum-typed `type_`
2129  // field, so the bindgen-enum UB hazard does not apply here.
2130  unsafe {
2131    let raw = packet.as_ptr();
2132    let nel = (*raw).side_data_elems;
2133    let arr = (*raw).side_data;
2134    if arr.is_null() || nel <= 0 || max_entries == 0 {
2135      return 0;
2136    }
2137    let count = (nel as usize).min(max_entries);
2138    let mut total = count.saturating_mul(SIDE_DATA_ENTRY_OVERHEAD);
2139    for i in 0..count {
2140      let entry = arr.add(i);
2141      total = total.saturating_add((*entry).size);
2142    }
2143    total
2144  }
2145}
2146
2147/// Number of `AVPacketSideData` entries on `packet`. The probe buffer
2148/// uses this to enforce [`MAX_PROBE_PACKET_SIDE_DATA_ENTRIES`] before
2149/// cloning, so a packet whose entry count alone would dominate retained
2150/// memory is rejected up front.
2151pub(crate) fn packet_side_data_count(packet: &Packet) -> usize {
2152  // SAFETY: side_data_elems is `c_int`, safe to read; clamp negatives to 0.
2153  let nel = unsafe { (*packet.as_ptr()).side_data_elems };
2154  if nel <= 0 { 0 } else { nel as usize }
2155}
2156
2157/// Just `EAGAIN` (separate from EOF — the FFmpeg send/receive state machine
2158/// distinguishes "drain output and retry" from "stream over").
2159fn is_eagain(e: &ffmpeg_next::Error) -> bool {
2160  matches!(e, ffmpeg_next::Error::Other { errno } if *errno == ffmpeg_next::error::EAGAIN)
2161}
2162
2163/// The probe square the per-pixel cost is measured on.
2164///
2165/// 256 divides every chroma subsampling FFmpeg has **and** every
2166/// alignment libavcodec uses, so the measurement is exact: no plane is
2167/// rounded up to cover a half-sized dimension, and no row is padded to
2168/// an alignment boundary. Measured at 257 the same census reads 16.934
2169/// bytes per pixel instead of 16.000 — that 5.8% is per-*row* padding,
2170/// a term linear in height rather than in pixels, and it is not part of
2171/// the per-pixel rate.
2172pub(crate) const PROBE_PIXELS: usize = 256 * 256;
2173
2174/// Bytes a [`PROBE_PIXELS`]-pixel picture costs in the **most expensive
2175/// pixel format this build of libavcodec can describe**.
2176///
2177/// # Why the worst case and not the declared one
2178///
2179/// The first cut of this ceiling charged the format the *container*
2180/// declared, and a container's declaration is not an upper bound on
2181/// anything. It may be unset, it may be wrong, and it may be narrower
2182/// than what the decoder actually emits — a stream declaring `yuv420p`
2183/// at 1.5 bytes per pixel whose decoder outputs `rgbaf32` at 16 got a
2184/// ceiling more than ten times too generous, which is the same hole one
2185/// layer down from the one it was added to close.
2186///
2187/// So the rate is not negotiated with the file at all. Every stream is
2188/// charged the worst case, and the worst case is **measured**, not
2189/// tabulated: this build's descriptor list is walked once and each
2190/// format priced through `av_image_get_buffer_size`, the same function
2191/// `avcodec_default_get_buffer2` sizes from. A future FFmpeg that adds
2192/// a wider format is priced correctly without this crate learning its
2193/// name.
2194///
2195/// # The census, at the time of writing
2196///
2197/// 267 descriptors, 251 of them CPU formats that price (the rest are
2198/// hardware surfaces, which carry no CPU bytes and return no size). The
2199/// maximum is **16.000 bytes per pixel**, reached by eight formats —
2200/// `gbrapf32be/le`, `rgbaf32be/le`, `rgba128be/le`, `gbrap32be/le`.
2201/// Next below are the 12-byte `gbrpf32`/`rgbf32` family.
2202///
2203/// # What this trades
2204///
2205/// Over-refusal for cheap formats, and it is deliberate. At the 512 MiB
2206/// default the effective ceiling becomes ~33.55 Mpx, so 8K (33.18 Mpx)
2207/// still decodes in *any* format — including the 16-byte ones, where it
2208/// really does cost 506 MiB — but a 16K `yuv420p` frame, which would
2209/// only have cost 199 MB, is refused too. That is the honest shape of a
2210/// bound that has to hold before the format is known: the deployment
2211/// answer is to raise `max_frame_bytes`, which is exactly the knob that
2212/// says how much memory one frame may cost.
2213///
2214/// # The residual, stated
2215///
2216/// Row alignment adds at most `align x planes x height` bytes on top of
2217/// this rate — about 1 MB on an 8K frame, 0.2%, and covered by the fact
2218/// that `max_frame_bytes` is a policy number rather than a hardware
2219/// limit. It is only significant for degenerate aspect ratios (a
2220/// one-pixel-wide frame is all padding), which the *pixel* ceiling has
2221/// always been the wrong shape to bound and which this change neither
2222/// introduces nor worsens.
2223pub(crate) fn worst_bytes_per_probe() -> usize {
2224  /// The census result, taken once. `av_pix_fmt_desc_next` walks a
2225  /// static table that cannot change during the process.
2226  static WORST: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
2227  *WORST.get_or_init(|| {
2228    /// The measured maximum at the time of writing, and the floor this
2229    /// census may not fall below. A build whose census comes back
2230    /// *smaller* than the eight 16-byte formats has failed to walk the
2231    /// table, not discovered a cheaper world — take the known number
2232    /// rather than a ceiling built on a failed measurement.
2233    const KNOWN_WORST_BYTES_PER_PIXEL: usize = 16;
2234
2235    let mut worst = 0usize;
2236    let mut desc: *const ffmpeg_next::ffi::AVPixFmtDescriptor = ptr::null();
2237    loop {
2238      // SAFETY: `av_pix_fmt_desc_next` walks libavutil's own static
2239      // descriptor table, taking the previous entry (or null to start)
2240      // and returning null at the end. It traffics in descriptor
2241      // pointers, not enums, so it needs no shim.
2242      desc = unsafe { ffmpeg_next::ffi::av_pix_fmt_desc_next(desc) };
2243      if desc.is_null() {
2244        break;
2245      }
2246      // **Both of these go through the `c_int` shims**, and this is the
2247      // place it matters most: the whole point of walking the table is
2248      // to price formats this build's bindings may not name, and the
2249      // generated `av_pix_fmt_desc_get_id` hands those ids back as a
2250      // closed `AVPixelFormat`. Every future format would have become
2251      // an invalid enum value on the way into the pricing meant to
2252      // handle it — the census would have been UB on exactly its reason
2253      // for existing.
2254      //
2255      // SAFETY: `desc` is a live entry from libavutil's static table;
2256      // the id is passed straight back to libavutil as the integer it
2257      // is, and `av_image_get_buffer_size` returns a negative AVERROR
2258      // for ids it cannot size rather than misbehaving.
2259      let id = unsafe { c_shims::av_pix_fmt_desc_get_id(desc) };
2260      let size = unsafe { c_shims::av_image_get_buffer_size(id, 256, 256, 1) };
2261      if size > 0 {
2262        worst = worst.max(size as usize);
2263      }
2264    }
2265    worst.max(KNOWN_WORST_BYTES_PER_PIXEL * PROBE_PIXELS)
2266  })
2267}
2268/// `AVCodecContext.get_buffer2`: the same pixel ceiling, applied where
2269/// the **aligned** dimensions are knowable.
2270///
2271/// # The hole this closes
2272///
2273/// `max_pixels` is checked by libavcodec against the frame's *raw*
2274/// `width * height`. What it then allocates is the **aligned** shape —
2275/// `avcodec_align_dimensions2` rounds both dimensions up to whatever
2276/// the codec and the CPU want — and for degenerate aspect ratios those
2277/// are not the same number at all. Measured on this build:
2278///
2279/// | shape | raw | aligned | inflation |
2280/// |---|---|---|---|
2281/// | `gray8` 65536x1 | 65,536 px / 64 KiB | 65536x32 = 2,097,152 px / 2 MiB | **32x** |
2282/// | `gray8` 1x65536 | 65,536 px / 64 KiB | 16x65536 = 1,048,576 px / 2 MiB | 16x |
2283/// | `yuv420p` 7680x4320 | 33,177,600 px | 7680x4320 | 1.00x |
2284/// | `gray8` 1024x1024 | 1,048,576 px | 1024x1024 | 1.00x |
2285///
2286/// So a one-pixel-tall frame slips 32 times its declared cost past a
2287/// scalar compared against `w * h`, and no value of that scalar can fix
2288/// it: bounding the product cannot bound a product whose factors are
2289/// then rounded up independently. Real pictures inflate by nothing at
2290/// all, which is why the ceiling looked sound.
2291///
2292/// # Why this hook and not `get_format`
2293///
2294/// `get_format` was measured first, because it needs no allocation
2295/// decision and receives the context. It **does not fire on every
2296/// road**: on this build a one-shot `mjpeg` decode calls it once and a
2297/// `png` decode calls it *zero* times. Cover art is overwhelmingly
2298/// mjpeg or png, so half the road this ceiling exists to guard would
2299/// have been unguarded.
2300///
2301/// `get_buffer2` fired on both — it is the allocator, so every frame
2302/// libavcodec hands back comes through it, and it sees the frame's
2303/// *real* format rather than a negotiated candidate.
2304///
2305/// # No state, so no lifetime to prove
2306///
2307/// The composed-`opaque` design was not needed. This callback reads the
2308/// ceiling from `AVCodecContext.max_pixels` — the field this crate set
2309/// itself, one number, already carrying the byte ceiling converted at
2310/// the worst per-pixel rate — and applies it to the aligned dimensions.
2311/// Same scalar, same meaning, applied where alignment is knowable.
2312/// `opaque` is untouched, so the hardware path keeps it and there is no
2313/// allocation whose lifetime has to outlive a C callback.
2314///
2315/// Panic discipline is likewise structural rather than asserted: the
2316/// body allocates nothing, indexes nothing, unwraps nothing, and calls
2317/// exactly three FFmpeg functions. There is no Rust operation in it
2318/// that can panic, and an `extern "C"` function aborts rather than
2319/// unwinding into C in any case.
2320///
2321/// # Safety
2322///
2323/// Called by libavcodec with a live context and a frame whose `format`,
2324/// `width` and `height` are set. Delegates every accepted frame to
2325/// `avcodec_default_get_buffer2`, which is what libavcodec would have
2326/// called had this hook not been installed.
2327unsafe extern "C" fn judge_buffer(
2328  ctx: *mut ffmpeg_next::ffi::AVCodecContext,
2329  frame: *mut ffmpeg_next::ffi::AVFrame,
2330  flags: libc::c_int,
2331) -> libc::c_int {
2332  // SAFETY: libavcodec passes a live context and frame; both fields are
2333  // plain integers.
2334  let (width, height) = unsafe { ((*frame).width, (*frame).height) };
2335
2336  // **This seat judges cost, and only cost.**
2337  //
2338  // `max_pixels` is a *logical* limit on a picture's extent, and
2339  // libavcodec already enforces it — against the **raw** dimensions, in
2340  // `ff_set_dimensions` via `av_image_check_size2`, before any frame
2341  // exists. That is the semantics the caller asked for and the
2342  // semantics FFmpeg documents, and this callback does not restate it.
2343  //
2344  // It used to. R11 added an *aligned*-dimension comparison here
2345  // against `max_pixels`, because at the time the callback had no
2346  // accurate byte check and a degenerate shape could slip its real cost
2347  // past a raw-pixel gate — 65536x1 aligns to 65536x32, thirty-two
2348  // times the pixels. That instrument is now both **redundant** and
2349  // **wrong**:
2350  //
2351  // * redundant, because since the byte ceiling was threaded in the
2352  //   footprint below prices the aligned dimensions itself, so the
2353  //   degenerate shape is refused on its actual cost; and
2354  // * wrong, because `max_pixels` is `min(the caller's pixel limit,
2355  //   byte ceiling / worst-bytes-per-pixel)` — so when the caller's
2356  //   pixel limit was the tighter seat, alignment inflation alone
2357  //   refused frames satisfying *both* requested limits. A 65536x1
2358  //   `gray8` frame under `max_pixels = 65536` and a generous byte
2359  //   budget fits the pixel limit exactly and costs 2 MiB, and was
2360  //   refused anyway — for arithmetic the caller never asked about.
2361  //
2362  // Logical extent is libavcodec's gate on raw dimensions; allocation
2363  // cost is this one, against the caller's own `max_frame_bytes`. One
2364  // question each.
2365  //
2366  // Audio reaches here too, and used to pass unpriced entirely:
2367  // `max_samples` bounds the sample *count*, so one sample across eight
2368  // packed `f64` channels is 64 valid bytes under a 64-byte ceiling and
2369  // a 2,080-byte allocation — delivered, because the copy-out only ever
2370  // rechecks the valid bytes.
2371  //
2372  // SAFETY: `ctx` and `frame` are live; every field read is a plain
2373  // integer, and `format` stays an integer throughout.
2374  // SAFETY: `frame` is live; the field is a plain pointer.
2375  let hw_frames = unsafe { (*frame).hw_frames_ctx };
2376
2377  // A hardware frame carries no CPU bytes for this seat to price — its
2378  // pool is judged where it is declared, in the `get_format` callback —
2379  // so it is delegated rather than failed closed on an unpriceable
2380  // format.
2381  if hw_frames.is_null() {
2382    // **The caller's own number, read from the seat that carries it.**
2383    // This used to recover a byte ceiling from `AVCodecContext.max_pixels`,
2384    // and the recovery was wrong in both directions:
2385    //
2386    // * `max_pixels` is `min(pixel ceiling, byte ceiling / worst)`, so
2387    //   when the *pixel* seat was the tighter of the two it stopped
2388    //   encoding the byte ceiling at all — and the recovery invented a
2389    //   smaller one. A 256x256 frame at 16 bytes a pixel under
2390    //   `max_pixels = 65536` with a 2 MiB byte budget satisfies both of
2391    //   the caller's limits, costs 1,050,624 bytes, and was judged
2392    //   against 1,048,576 and refused. The claim that the conflation
2393    //   was harmless in one direction was simply wrong: it omitted the
2394    //   footprint's own alignment and slack, which is exactly where
2395    //   those extra 2,048 bytes live.
2396    // * and for audio a pixel ceiling has no business being consulted
2397    //   at all.
2398    //
2399    // The audio road briefly recovered from `max_samples` instead,
2400    // which *is* exact — but two sources of truth for one number is how
2401    // the first one went wrong. Both media read the seat now.
2402    //
2403    // SAFETY: `opaque` holds the `CallbackState` that
2404    // `build_codec_context` installed and whose owner outlives the
2405    // context. A null one means a context this crate did not build, and
2406    // is refused rather than assumed generous.
2407    let state = unsafe { (*ctx).opaque } as *const CallbackState;
2408    if state.is_null() {
2409      return -(libc::EINVAL);
2410    }
2411    // SAFETY: non-null per the check above; the field is a plain `u64`.
2412    let byte_ceiling = u128::from(unsafe { (*state).max_frame_bytes });
2413
2414    // SAFETY: `frame` is live; both are plain integer fields.
2415    let (format_raw, nb_samples) = unsafe { ((*frame).format, (*frame).nb_samples) };
2416    let priced = if width > 0 && height > 0 {
2417      crate::footprint::video_frame_bytes(format_raw, width, height)
2418    } else if nb_samples > 0 {
2419      // **The frame's layout, not the context's.** FFmpeg's
2420      // `get_buffer2` contract says the callback reads the values on
2421      // the *frame*, and `avcodec_default_get_buffer2` sizes from them
2422      // — the context's layout is whatever was last negotiated and can
2423      // differ outright. A context claiming mono against a frame
2424      // carrying 255 `dblp` channels at 130,000 samples prices about a
2425      // megabyte and allocates about 265 MB.
2426      //
2427      // Read raw and signed, per the house discipline, and refused
2428      // rather than floored: a negative count is malformed, and
2429      // flooring it to zero would price an allocation that is about to
2430      // happen at nothing.
2431      // SAFETY: `frame` is live; `ch_layout.nb_channels` is a plain
2432      // `c_int`.
2433      let channels = unsafe { (*frame).ch_layout.nb_channels };
2434      if channels <= 0 {
2435        return -(libc::EINVAL);
2436      }
2437      crate::footprint::audio_frame_bytes(format_raw, nb_samples as usize, channels as usize)
2438    } else {
2439      // Neither geometry nor samples: nothing is being allocated that
2440      // this seat can price, and nothing is claimed.
2441      Some(0)
2442    };
2443
2444    // **The refusal leaves its reason behind.** A `get_buffer2`
2445    // callback can only answer libavcodec with an errno, and
2446    // `AVERROR(EINVAL)` is also what libavcodec reports for corrupt
2447    // input — so a bare refusal here was indistinguishable from a
2448    // broken file, and only one of those is worth retrying with a
2449    // larger ceiling. The decoder funnels collect this the same way
2450    // they collect the `get_format` declination.
2451    let record = |bytes: u64| {
2452      use core::sync::atomic::Ordering;
2453      // SAFETY: `state` was proved non-null above.
2454      unsafe {
2455        (*state)
2456          .declined_frame_bytes
2457          .store(bytes, Ordering::Relaxed);
2458        (*state)
2459          .declined_frame_audio
2460          .store(width <= 0 && height <= 0, Ordering::Relaxed);
2461        (*state)
2462          .frame_budget_declined
2463          .store(true, Ordering::Release);
2464      }
2465      -(libc::EINVAL)
2466    };
2467    match priced {
2468      // Fail closed. An allocation whose size cannot be established is
2469      // not a small one — the same stance every other judge here takes.
2470      // Reported as an unbounded cost, which is what an unprovable one
2471      // is.
2472      None => return record(u64::MAX),
2473      // Nothing to buy, so nothing to refuse.
2474      Some(0) => {}
2475      // A budget of zero admits nothing, and this is the arm that used
2476      // to be a skipped guard.
2477      Some(bytes) if byte_ceiling == 0 => return record(bytes as u64),
2478      Some(bytes) if bytes as u128 > byte_ceiling => return record(bytes as u64),
2479      Some(_) => {}
2480    }
2481  }
2482
2483  // SAFETY: delegating to the allocator libavcodec would have used.
2484  unsafe { ffmpeg_next::ffi::avcodec_default_get_buffer2(ctx, frame, flags) }
2485}
2486
2487/// Prices the CPU frame `av_hwframe_transfer_data` would allocate, and
2488/// refuses it if it is over the ceiling — **before** the transfer runs.
2489///
2490/// # Why the hardware road needs its own seat
2491///
2492/// [`judge_buffer`] is not a universal choke point, and the census says
2493/// so on this machine. `ff_get_buffer` calls `hwaccel->alloc_frame`
2494/// directly and never reaches `get_buffer2` at all: a VideoToolbox
2495/// h264 decode of a 160x120 clip records **zero** `get_buffer2` calls
2496/// while producing a hardware frame. And the CPU destination of a
2497/// download is allocated by `av_hwframe_transfer_data` itself, outside
2498/// both hooks.
2499///
2500/// # What the census settled about the surface itself
2501///
2502/// `max_pixels` **does** bite before `alloc_frame`, and this was
2503/// measured rather than assumed: with `max_pixels = 100`, a 160x120
2504/// VideoToolbox h264 decode fails at `avcodec_open2` with
2505/// `Picture size 160x120 exceeds specified max pixel count 100` from
2506/// `av_image_check_size2`, zero `get_buffer2` calls and no frame. The
2507/// check lives in `ff_set_dimensions`, which every decoder runs when it
2508/// learns its dimensions and before any surface pool exists — so the
2509/// seat `max_pixels` already occupies covers the hardware surface too.
2510///
2511/// The residual on that road is the aligned-dimensions gap
2512/// [`judge_buffer`] closes for software frames, and it applies to
2513/// **driver-owned GPU memory** rather than to anything this crate
2514/// carries. What this crate does carry off the hardware road is the CPU
2515/// frame downloaded here, and that is bounded exactly, by this
2516/// function.
2517///
2518/// # How the price is taken
2519///
2520/// The destination format is not chosen by this crate: `dst.format` is
2521/// `AV_PIX_FMT_NONE` on entry and FFmpeg picks from
2522/// `av_hwframe_transfer_get_formats`. So the whole candidate list is
2523/// priced and the **worst** taken — walked as `*const c_int` through
2524/// the shim, because a driver may offer a format this build's bindings
2525/// do not name, which is the same discipline the pixel census keeps.
2526///
2527/// When the list cannot be obtained the global worst rate stands in;
2528/// over-refusing is the safe direction for a ceiling.
2529///
2530/// # Safety
2531///
2532/// `hw_frame` must be a live `*const AVFrame`.
2533unsafe fn judge_hw_transfer(
2534  hw_frame: *const ffmpeg_next::ffi::AVFrame,
2535  limits: crate::FrameLimits,
2536) -> std::result::Result<(), crate::error::HwTransferTooLarge> {
2537  // SAFETY: `hw_frame` is live per the contract; the field is a plain
2538  // pointer.
2539  let frames_ctx = unsafe { (*hw_frame).hw_frames_ctx };
2540
2541  // **The allocated extent, not the displayed one.** `AVFrame.width` /
2542  // `.height` are the *display* dims; what
2543  // `av_hwframe_transfer_data` allocates is sized from the frames
2544  // context, and on a cropped stream the two diverge by orders of
2545  // magnitude — measured on this build, an h264 stream with SPS
2546  // cropping shows 32x32 display over a 1920x1088 coded surface, a
2547  // 2040x gap. This crate already had a helper that reads the pool
2548  // dims, with a doc comment naming this exact trap; the first version
2549  // of this judge reached past it for `AVFrame.width` anyway.
2550  //
2551  // **Fail closed.** No context, no dims, or no priceable candidate
2552  // means the allocation extent cannot be proved — and an unprovable
2553  // extent is not a small one. The same stance
2554  // `estimate_transfer_bytes` takes next door, and for the same reason:
2555  // falling back to display dims here would restore precisely the hole
2556  // this judge exists to close.
2557  if frames_ctx.is_null() {
2558    // Not a hardware frame at all. `av_hwframe_transfer_data` refuses
2559    // such a source with `EINVAL` and allocates nothing, so there is no
2560    // extent to bound here — and answering "too large" would put a
2561    // ceiling's name on a completely different fault. The existing path
2562    // reports it accurately.
2563    return Ok(());
2564  }
2565  let Some((width, height)) = (unsafe { hw_frames_ctx_dimensions_raw(hw_frame) }) else {
2566    // A hardware frame whose pool extent cannot be read. The transfer
2567    // may well allocate; nothing here can say how much. Charged as
2568    // unbounded, which is what an unprovable extent is.
2569    return Err(crate::error::HwTransferTooLarge::new(
2570      usize::MAX,
2571      limits.max_frame_bytes(),
2572    ));
2573  };
2574
2575  // **Every candidate folded in, priceable or not.**
2576  //
2577  // FFmpeg picks the destination format from this list; this crate does
2578  // not get to choose. So the bound has to be the maximum over the
2579  // *whole* list — and the fold used to skip the members libavutil
2580  // would not size, updating `worst` only on priceable ones and
2581  // reaching for a fallback only when *nothing* priced. A list holding
2582  // one cheap priceable format beside one unpriceable format was
2583  // therefore judged at the cheap price, while FFmpeg remained free to
2584  // select the one that was ignored.
2585  //
2586  // An unpriceable candidate is charged
2587  // [`crate::footprint::video_frame_bytes_upper_bound`] instead: the
2588  // same dimension alignment and per-plane overhead at the widest rate,
2589  // so it dominates whatever that layout would have cost had it been
2590  // priceable.
2591  let mut worst: usize = 0;
2592  let mut judged_any = false;
2593  if !frames_ctx.is_null() {
2594    let mut list: *mut libc::c_int = ptr::null_mut();
2595    // `AV_HWFRAME_TRANSFER_DIRECTION_FROM` is 0 — passed as the integer
2596    // it is, like every other open C enum on this road.
2597    // SAFETY: `frames_ctx` is the frame's live `AVHWFramesContext`
2598    // reference; on success FFmpeg allocates a NONE-terminated list
2599    // that the caller frees.
2600    let rc = unsafe { c_shims::av_hwframe_transfer_get_formats(frames_ctx, 0, &mut list, 0) };
2601    if rc >= 0 && !list.is_null() {
2602      let none = ffmpeg_next::ffi::AVPixelFormat::AV_PIX_FMT_NONE as libc::c_int;
2603      let mut p = list;
2604      loop {
2605        // SAFETY: FFmpeg guarantees the list is NONE-terminated; reads
2606        // up to and including the sentinel are in bounds.
2607        let candidate = unsafe { ptr::read(p) };
2608        if candidate == none {
2609          break;
2610        }
2611        // **The allocator's arithmetic, not the payload's.** Pricing
2612        // `av_image_get_buffer_size` at a fixed alignment is what the
2613        // pixels weigh laid out tightly — for a 16x16 NV12 destination
2614        // that is 768 bytes against the 1,792 `av_frame_get_buffer`
2615        // really takes. See [`crate::footprint`].
2616        let cost = crate::footprint::video_frame_bytes(candidate, width, height)
2617          .or_else(|| crate::footprint::video_frame_bytes_upper_bound(width, height));
2618        match cost {
2619          Some(size) => {
2620            worst = worst.max(size);
2621            judged_any = true;
2622          }
2623          // Not even the dimension-only bound could be formed, so the
2624          // extent itself is not a picture. Nothing here will guess.
2625          None => {
2626            // SAFETY: `list` is freed exactly once, on every road out.
2627            unsafe { ffmpeg_next::ffi::av_freep(ptr::addr_of_mut!(list).cast()) };
2628            return Err(crate::error::HwTransferTooLarge::new(
2629              usize::MAX,
2630              limits.max_frame_bytes(),
2631            ));
2632          }
2633        }
2634        p = unsafe { p.add(1) };
2635      }
2636      // SAFETY: `list` was allocated by `av_hwframe_transfer_get_formats`
2637      // and is freed exactly once here.
2638      unsafe { ffmpeg_next::ffi::av_freep(ptr::addr_of_mut!(list).cast()) };
2639    }
2640  }
2641
2642  if !judged_any {
2643    // An empty list, or a query that failed: no candidate was seen at
2644    // all. Charge the dimension-only bound over the pool extent, which
2645    // is the most any format this build can emit could cost there.
2646    let Some(bound) = crate::footprint::video_frame_bytes_upper_bound(width, height) else {
2647      return Err(crate::error::HwTransferTooLarge::new(
2648        usize::MAX,
2649        limits.max_frame_bytes(),
2650      ));
2651    };
2652    worst = bound;
2653  }
2654
2655  if worst > limits.max_frame_bytes() {
2656    return Err(crate::error::HwTransferTooLarge::new(
2657      worst,
2658      limits.max_frame_bytes(),
2659    ));
2660  }
2661  Ok(())
2662}
2663/// Reads and clears the coded-surface refusal a `get_format` callback
2664/// left in its state, if it left one.
2665///
2666/// Free-standing rather than a method because the reason has to survive
2667/// on **every** hardware exit, and one of them — the open-time failure
2668/// path — runs before a decoder exists to ask.
2669fn ceiling_declination_of(state: *const CallbackState) -> Option<Error> {
2670  use core::sync::atomic::Ordering;
2671  if state.is_null() {
2672    return None;
2673  }
2674  // SAFETY: `state` is the live `CallbackState` the caller owns; it is
2675  // freed only after the codec context it belongs to.
2676  let (declined, pixels, limit) = unsafe {
2677    (
2678      (*state).ceiling_declined.swap(false, Ordering::Acquire),
2679      (*state).declined_pixels.load(Ordering::Relaxed),
2680      (*state).declined_limit.load(Ordering::Relaxed),
2681    )
2682  };
2683  declined.then(|| Error::HwSurfaceTooLarge(crate::error::HwSurfaceTooLarge::new(pixels, limit)))
2684}
2685/// The software decoders' error funnel.
2686///
2687/// Every road that turns a libavcodec decode failure into an `Error`
2688/// goes through here, so a frame the allocator judge refused comes back
2689/// named instead of as the `EINVAL` libavcodec also uses for corrupt
2690/// input. The hardware roads have their own funnel (`hw_exit`); this is
2691/// its software twin, and the discipline is the same one: **a consumer
2692/// added helper-by-helper is lost the next time the surrounding code is
2693/// restructured, so every exit calls one function.**
2694///
2695/// # Safety
2696///
2697/// `state` must be null or a live `CallbackState` the caller owns.
2698pub(crate) fn software_exit(state: *const CallbackState, e: ffmpeg_next::Error) -> Error {
2699  frame_budget_declination_of(state).unwrap_or(Error::Ffmpeg(e))
2700}
2701
2702/// Reads and clears a software frame-budget refusal left by
2703/// [`judge_buffer`], as the named error it deserves.
2704///
2705/// The software twin of [`ceiling_declination_of`]: the allocator judge
2706/// can only answer libavcodec with an errno, so the reason lives in the
2707/// callback state and every decoder funnel collects it.
2708pub(crate) fn frame_budget_declination_of(state: *const CallbackState) -> Option<Error> {
2709  crate::ffi::take_frame_budget_declination(state).map(|(bytes, limit, audio)| {
2710    Error::FrameBudgetExceeded(crate::error::FrameBudgetExceeded::new(
2711      bytes,
2712      limit,
2713      if audio {
2714        crate::error::FrameMedium::Audio
2715      } else {
2716        crate::error::FrameMedium::Video
2717      },
2718    ))
2719  })
2720}
2721
2722/// Proves an opened codec context is a **video** one without going
2723/// through `Opened::video()`.
2724///
2725/// `Opened::video()` calls `Context::medium()`, which reads
2726/// `AVCodecContext.codec_type` as the bindgen `AVMediaType` enum — a
2727/// value outside this build's discriminant set is UB the moment it is
2728/// formed, before any comparison can run. The hardware path has always
2729/// bypassed that API for this reason; this is that bypass, extracted so
2730/// the second caller reuses it instead of restating it.
2731///
2732/// The caller keeps ownership of `opened` on failure, so its `Drop`
2733/// still releases the codec context.
2734pub(crate) fn ensure_video_codec_type(opened: &codec::decoder::Opened) -> Result<()> {
2735  ensure_codec_type(opened, AVMediaType::AVMEDIA_TYPE_VIDEO)
2736}
2737
2738/// The general form: proves an opened context has the medium expected,
2739/// reading `codec_type` as the integer it is.
2740///
2741/// `Opened::{video,audio,subtitle}()` all go through
2742/// `Context::medium()`, so all three carried the same hazard and all
2743/// three now come through here.
2744pub(crate) fn ensure_codec_type(
2745  opened: &codec::decoder::Opened,
2746  expected: AVMediaType,
2747) -> Result<()> {
2748  // SAFETY: `codec_type` is bound as `AVMediaType` (`#[repr(i32)]`),
2749  // the same size and alignment as `i32`; reading the bytes as `i32`
2750  // cannot be UB whatever FFmpeg wrote there.
2751  let codec_type_int: i32 =
2752    unsafe { ptr::read(ptr::addr_of!((*opened.as_ptr()).codec_type) as *const i32) };
2753  if codec_type_int != expected as i32 {
2754    // The same error `Opened::video()` would have produced, without the
2755    // enum construction.
2756    return Err(Error::Ffmpeg(ffmpeg_next::Error::InvalidData));
2757  }
2758  Ok(())
2759}
2760
2761/// Look up the decoder for `parameters` without going through the bindgen
2762/// `AVCodecID` Rust enum. Reads the codec_id field as raw `u32` via
2763/// `addr_of!` + `ptr::read` so a value not in our build's discriminant
2764/// set never invokes UB.
2765pub(crate) fn find_decoder(parameters: &codec::Parameters) -> Result<Codec> {
2766  ensure_parameters_non_null(parameters)?;
2767  // SAFETY: parameters' inner pointer is non-null (checked above);
2768  // addr_of! projects to the codec_id field; the *const u32 cast is sound
2769  // because AVCodecID is `#[repr(u32)]` (same size and alignment as u32).
2770  // Reading as u32 cannot be UB regardless of the value FFmpeg wrote.
2771  let raw_id: u32 =
2772    unsafe { ptr::read(ptr::addr_of!((*parameters.as_ptr()).codec_id) as *const u32) };
2773
2774  // Call C `avcodec_find_decoder` via our local `c_int`-typed shim — we
2775  // never construct an `AVCodecID` enum from `raw_id`. The C function
2776  // returns NULL for unknown ids, which we surface as `Error::NoCodec`.
2777  // SAFETY: avcodec_find_decoder is a pure FFmpeg lookup; passing any
2778  // c_int is sound (returns NULL for unknown).
2779  let codec_ptr = unsafe { c_shims::avcodec_find_decoder(raw_id as libc::c_int) };
2780  if codec_ptr.is_null() {
2781    return Err(Error::NoCodec(raw_id));
2782  }
2783  // SAFETY: codec_ptr is a non-null *const AVCodec into FFmpeg's static
2784  // codec table; it lives for the duration of the program.
2785  Ok(unsafe { Codec::wrap(codec_ptr) })
2786}
2787
2788/// Drain output frames from a candidate decoder during probe replay,
2789/// transferring each one from the candidate's HW context to a fresh CPU
2790/// frame and queueing it. Returns `Ok(())` once the candidate signals
2791/// EAGAIN/EOF. The transfer happens while the candidate is still alive
2792/// (its `AVHWFramesContext` is reachable); the resulting CPU frames remain
2793/// valid after the candidate is committed because they hold their own
2794/// buffer references with no dependency on the original device context.
2795fn drain_into_pending(
2796  decoder: &mut ffmpeg_next::decoder::Video,
2797  hw_buf: &mut frame::Video,
2798  pending: &mut VecDeque<frame::Video>,
2799  pending_bytes: &mut usize,
2800  max_bytes: usize,
2801  frame_limits: crate::FrameLimits,
2802) -> std::result::Result<(), ffmpeg_next::Error> {
2803  loop {
2804    match decoder.receive_frame(hw_buf) {
2805      Ok(()) => {
2806        // Pre-transfer cap check: if we are already at or over either cap,
2807        // the candidate is producing more than we can hold. Treat as an
2808        // explicit candidate failure so `advance_probe` can try the next
2809        // backend instead of committing a stream with silently-dropped
2810        // frames in the middle.
2811        //
2812        // TODO: at very large frame sizes (8K HDR P010, > ~96 MiB each)
2813        // even a single retained frame is significant. Future direction:
2814        // memmap-backed pending frames (write to a temp file or shared
2815        // memory segment) so the resident set stays bounded even when the
2816        // byte cap is raised. Out of scope for now.
2817        if pending.len() >= MAX_PROBE_PENDING_FRAMES || *pending_bytes >= max_bytes {
2818          tracing::warn!(
2819            frames = pending.len(),
2820            bytes = *pending_bytes,
2821            max_frames = MAX_PROBE_PENDING_FRAMES,
2822            max_bytes = max_bytes,
2823            "hwdecode: probe pending cap reached; failing candidate replay"
2824          );
2825          // SAFETY: hw_buf is owned and valid; unref of an empty frame is a no-op.
2826          unsafe { av_frame_unref(hw_buf.as_mut_ptr()) };
2827          return Err(ffmpeg_next::Error::Other {
2828            errno: libc::ENOMEM,
2829          });
2830        }
2831        // Pre-transfer size guard: `av_hwframe_transfer_data` will
2832        // allocate the CPU buffer based on `hw_buf`'s dimensions. If a
2833        // single frame's worst-case footprint already pushes past the
2834        // cap, refuse the candidate **before** allocating so RSS does
2835        // not spike on a frame we'd immediately drop. Uses a width *
2836        // height * `WORST_CASE_BYTES_PER_PIXEL` upper bound; the
2837        // post-transfer accounting via `cpu_frame_bytes` below stays in
2838        // place as a backstop using the actual stride/format.
2839        let estimated_bytes = match estimate_transfer_bytes(hw_buf) {
2840          Some(b) => b,
2841          None => {
2842            // SAFETY: AVFrame.width/height are c_int reads.
2843            let (w, h) = unsafe {
2844              let raw = hw_buf.as_ptr();
2845              ((*raw).width, (*raw).height)
2846            };
2847            tracing::warn!(
2848              width = w,
2849              height = h,
2850              "hwdecode: HW frame dimensions invalid for sizing; failing candidate replay"
2851            );
2852            unsafe { av_frame_unref(hw_buf.as_mut_ptr()) };
2853            return Err(ffmpeg_next::Error::Other {
2854              errno: libc::ENOMEM,
2855            });
2856          }
2857        };
2858        let estimated_total = pending_bytes.saturating_add(estimated_bytes);
2859        if estimated_total > max_bytes {
2860          // SAFETY: AVFrame.width/height are c_int reads.
2861          let (w, h) = unsafe {
2862            let raw = hw_buf.as_ptr();
2863            ((*raw).width, (*raw).height)
2864          };
2865          tracing::warn!(
2866            pending_bytes = *pending_bytes,
2867            estimated_bytes,
2868            width = w,
2869            height = h,
2870            max_bytes = max_bytes,
2871            "hwdecode: pre-transfer size estimate exceeds cap; \
2872             refusing candidate replay before allocating CPU frame"
2873          );
2874          unsafe { av_frame_unref(hw_buf.as_mut_ptr()) };
2875          return Err(ffmpeg_next::Error::Other {
2876            errno: libc::ENOMEM,
2877          });
2878        }
2879        // **The same exact judge, on the replay road.** This site
2880        // already had a pre-transfer *estimate* (`w * h * 8`) against
2881        // the probe's own pending budget; that stays, and this adds the
2882        // frame ceiling itself, priced exactly.
2883        //
2884        // The refusal is reported through this function's existing
2885        // `ffmpeg_next::Error` channel rather than the named arm: every
2886        // error out of a probe-replay drain is collapsed by the caller
2887        // into "this candidate failed, try the next backend", so a name
2888        // has no consumer here. The reason is logged so it is not lost.
2889        // SAFETY: `hw_buf` holds a live decoded HW frame.
2890        if let Err(e) = unsafe { judge_hw_transfer(hw_buf.as_ptr(), frame_limits) } {
2891          tracing::warn!(
2892            bytes = e.bytes(),
2893            limit = e.limit(),
2894            "hwdecode: candidate's hw->cpu transfer would exceed the frame ceiling; \
2895             refusing the candidate before the download"
2896          );
2897          // SAFETY: `hw_buf` is owned and valid.
2898          unsafe { av_frame_unref(hw_buf.as_mut_ptr()) };
2899          return Err(ffmpeg_next::Error::Other {
2900            errno: libc::EINVAL,
2901          });
2902        }
2903        let mut cpu = alloc_av_frame()?;
2904        // SAFETY: hw_buf is a freshly-decoded HW frame;
2905        // `av_hwframe_transfer_data` allocates pixel buffers on `cpu`.
2906        // We use `copy_frame_props_minimal` (only `pts`) instead of
2907        // `av_frame_copy_props` for the same reason as
2908        // `transfer_hw_frame`: the public `Frame` API does not expose
2909        // side data / metadata / opaque refs, so deep-copying them per
2910        // frame is pure cost and an unbounded allocation source on
2911        // attacker-controlled streams.
2912        unsafe {
2913          let r1 = av_hwframe_transfer_data(cpu.as_mut_ptr(), hw_buf.as_ptr(), 0);
2914          if r1 < 0 {
2915            return Err(ffmpeg_next::Error::from(r1));
2916          }
2917        }
2918        // Same post-transfer pix_fmt validation as `transfer_hw_frame`.
2919        // A driver that picks a CPU format outside our supported set
2920        // would queue an unusable frame here; later, when
2921        // `try_pop_pending` hands it to the caller, `Frame::row` /
2922        // `Frame::as_ptr` would return `None`. Refuse the candidate
2923        // before the queue grows so probing advances to the next
2924        // backend instead.
2925        let cpu_raw_fmt: i32 = unsafe { (*cpu.as_ptr()).format };
2926        let cpu_pix_fmt = crate::boundary::from_av_pixel_format(cpu_raw_fmt);
2927        if !crate::frame::is_supported_cpu_pix_fmt(&cpu_pix_fmt) {
2928          tracing::warn!(
2929            pix_fmt = cpu_raw_fmt,
2930            "hwdecode: candidate produced unsupported CPU pix_fmt during \
2931             probe replay; failing candidate"
2932          );
2933          return Err(ffmpeg_next::Error::Other {
2934            errno: libc::EINVAL,
2935          });
2936        }
2937        let pixel_bytes = match cpu_frame_bytes(&cpu) {
2938          Some(b) => b,
2939          None => {
2940            // Unknown pix_fmt or vertically-flipped layout — we cannot
2941            // bound this frame's contribution against the byte cap, so up
2942            // to MAX_PROBE_PENDING_FRAMES of them could exhaust memory.
2943            // Fail the candidate so probing tries the next backend
2944            // rather than queueing untracked allocations.
2945            // SAFETY: AVFrame.format is c_int, safe to read.
2946            let pix_fmt: i32 = unsafe { (*cpu.as_ptr()).format };
2947            tracing::warn!(
2948              pix_fmt,
2949              "hwdecode: cannot size unknown CPU pix_fmt during replay; failing candidate"
2950            );
2951            // cpu drops here.
2952            return Err(ffmpeg_next::Error::Other {
2953              errno: libc::ENOMEM,
2954            });
2955          }
2956        };
2957        // Account for side-data bytes that `av_frame_copy_props`
2958        // will deep-copy from the source HW frame. HDR streams
2959        // typically carry mastering display + content light level
2960        // (~50 bytes) and dynamic HDR metadata (~few hundred bytes);
2961        // pathological side-data could otherwise quietly bypass the
2962        // pixel-data byte cap.
2963        // SAFETY: hw_buf is a valid AVFrame; we read scalar fields
2964        // and pointer arrays without forming a `&AVFrame`.
2965        let side_data_bytes = unsafe { sum_side_data_bytes(hw_buf.as_ptr()) };
2966        let new_total = pending_bytes
2967          .saturating_add(pixel_bytes)
2968          .saturating_add(side_data_bytes);
2969        if new_total > max_bytes {
2970          tracing::warn!(
2971            pending_bytes = *pending_bytes,
2972            pixel_bytes,
2973            side_data_bytes,
2974            max_bytes,
2975            "hwdecode: queueing this frame would exceed byte cap; \
2976             failing candidate replay"
2977          );
2978          // cpu drops here without ever paying a metadata deep copy.
2979          return Err(ffmpeg_next::Error::Other {
2980            errno: libc::ENOMEM,
2981          });
2982        }
2983        // Cap check passed — copy AVFrame metadata. SAFETY: cpu and
2984        // hw_buf are both valid AVFrames we own. On failure (OOM
2985        // during side-data alloc) we propagate so the probe candidate
2986        // is treated as failed rather than queueing a frame whose
2987        // metadata silently disappeared.
2988        unsafe { copy_frame_props_minimal(cpu.as_mut_ptr(), hw_buf.as_ptr()) }?;
2989        *pending_bytes = new_total;
2990        pending.push_back(cpu);
2991      }
2992      Err(e) if is_transient(&e) => return Ok(()),
2993      Err(e) => return Err(e),
2994    }
2995  }
2996}
2997
2998/// Allocated frame dimensions according to `hw_buf.hw_frames_ctx`.
2999///
3000/// Per FFmpeg's `libavutil/hwcontext.c::transfer_data_alloc`, the CPU
3001/// destination of `av_hwframe_transfer_data` is allocated using
3002/// `AVHWFramesContext.width / .height` (the *allocated* surface size of
3003/// the HW pool); only afterwards is `dst->width / dst->height` reset to
3004/// `src->width / src->height` (the *display* size). For cropped or
3005/// heavily aligned streams the allocated dims can be much larger than
3006/// the display dims (e.g. coded 8192×8192 surface with a 100×100
3007/// display crop), so any byte-cap accounting that uses display dims
3008/// undercounts by `allocated_height / display_height` and lets the
3009/// real allocation slip past the cap.
3010///
3011/// Returns `None` when no `hw_frames_ctx` is attached or the dimensions
3012/// are non-positive — the caller treats `None` as "cannot prove
3013/// allocation extent, fail the candidate."
3014fn hw_frames_ctx_dimensions(frame: &frame::Video) -> Option<(i32, i32)> {
3015  // SAFETY: `frame` owns a live `AVFrame` for the call.
3016  unsafe { hw_frames_ctx_dimensions_raw(frame.as_ptr()) }
3017}
3018
3019/// Pointer form of [`hw_frames_ctx_dimensions`], for the judges that
3020/// hold a raw `AVFrame` rather than a wrapper.
3021///
3022/// # Safety
3023///
3024/// `raw` must be a live `*const AVFrame`.
3025unsafe fn hw_frames_ctx_dimensions_raw(raw: *const AVFrame) -> Option<(i32, i32)> {
3026  // SAFETY: AVFrame.hw_frames_ctx is `*mut AVBufferRef`. When non-null,
3027  // its `data` field points to an `AVHWFramesContext`. We read `.width`
3028  // and `.height` (both `c_int`) via field projection — neither field is
3029  // enum-typed, so no bindgen-enum UB hazard.
3030  unsafe {
3031    let hw_ctx_ref = (*raw).hw_frames_ctx;
3032    if hw_ctx_ref.is_null() {
3033      return None;
3034    }
3035    let data = (*hw_ctx_ref).data;
3036    if data.is_null() {
3037      return None;
3038    }
3039    let frames_ctx = data as *const AVHWFramesContext;
3040    let w: i32 = ptr::read(ptr::addr_of!((*frames_ctx).width));
3041    let h: i32 = ptr::read(ptr::addr_of!((*frames_ctx).height));
3042    if w <= 0 || h <= 0 {
3043      return None;
3044    }
3045    Some((w, h))
3046  }
3047}
3048
3049/// Conservative upper-bound estimate of the bytes
3050/// `av_hwframe_transfer_data` will allocate when downloading `hw_buf` to
3051/// a CPU frame. Used by [`drain_into_pending`] as a pre-transfer guard
3052/// so a candidate replay can refuse a frame whose footprint would
3053/// exceed the byte budget *without* first paying the allocation.
3054///
3055/// Sizes from `hw_buf.hw_frames_ctx` (the allocated dims used by the
3056/// FFmpeg transfer path) rather than `AVFrame.width / .height` (display
3057/// dims). On a cropped stream the two can differ by orders of magnitude
3058/// and using display dims would let the real allocation slip past the
3059/// cap.
3060///
3061/// Returns `None` when `hw_frames_ctx` is missing or its width/height
3062/// are non-positive — caller treats as candidate failure since we
3063/// cannot prove the allocation extent. (A SW source frame on the probe
3064/// replay path is not expected; we don't fall back to display dims
3065/// because that's the exact attack the cap is meant to prevent.)
3066fn estimate_transfer_bytes(hw_buf: &frame::Video) -> Option<usize> {
3067  let (w, h) = hw_frames_ctx_dimensions(hw_buf)?;
3068  Some(
3069    (w as usize)
3070      .saturating_mul(h as usize)
3071      .saturating_mul(WORST_CASE_BYTES_PER_PIXEL),
3072  )
3073}
3074
3075/// Exact resident size of a CPU frame: sum of `AVFrame.buf[i].size`
3076/// across every populated buffer.
3077///
3078/// `AVBufferRef.size` is documented as "Size of data in bytes" — the
3079/// real allocated extent FFmpeg used. Reading it directly handles the
3080/// cropped/aligned case where `AVFrame.height` (display) is smaller
3081/// than the underlying allocation height (the `AVHWFramesContext`
3082/// surface size FFmpeg sized the buffer for); a `linesize *
3083/// plane_height_for(display_height)` formula would undercount in that
3084/// case.
3085///
3086/// Returns `None` only when `linesize[0]` is negative — FFmpeg's
3087/// vertically-flipped layout. The crate's safe row accessors
3088/// ([`crate::Frame::row`] / [`crate::Frame::rows`]) already reject
3089/// negative-stride frames, so queueing one during probe replay would
3090/// just delay the failure to the consumer; refusing here lets the
3091/// probe loop advance to the next backend instead.
3092fn cpu_frame_bytes(frame: &frame::Video) -> Option<usize> {
3093  // SAFETY: AVFrame.linesize is `[c_int; 8]`; AVFrame.buf is
3094  // `[*mut AVBufferRef; 8]`; AVBufferRef.size is `usize`. All are
3095  // primitive reads / pointer dereferences with no enum interpretation.
3096  unsafe {
3097    let raw = frame.as_ptr();
3098    let first_linesize = (*raw).linesize[0];
3099    // Vertically-flipped (negative linesize) is the only "unsizeable"
3100    // case we still surface as `None`; everything else can be exactly
3101    // measured from buf[i].size.
3102    if first_linesize < 0 {
3103      return None;
3104    }
3105    let mut total: usize = 0;
3106    for i in 0..(*raw).buf.len() {
3107      let buf = (*raw).buf[i];
3108      if buf.is_null() {
3109        continue;
3110      }
3111      total = total.saturating_add((*buf).size);
3112    }
3113    Some(total)
3114  }
3115}
3116
3117#[allow(dead_code)]
3118fn _assert_send() {
3119  fn check<T: Send>() {}
3120  check::<VideoDecoder>();
3121}
3122
3123#[cfg(test)]
3124mod tests;