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