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