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