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_alloc,
20 avcodec_parameters_copy, avcodec_parameters_free, avcodec_parameters_to_context,
21 },
22 frame,
23};
24
25/// Local FFI shim: `avcodec_find_decoder` declared with `c_int` instead of
26/// the bindgen `AVCodecID` enum. Constructing `AVCodecID` from a runtime
27/// integer that isn't in our build's discriminant set is UB; calling the
28/// C function with a raw int avoids that boundary entirely. Both Rust
29/// declarations resolve to the same C symbol at link time.
30mod c_shims {
31 use super::AVCodec;
32 use libc::c_int;
33 unsafe extern "C" {
34 pub fn avcodec_find_decoder(id: c_int) -> *const AVCodec;
35 }
36}
37
38use crate::{
39 backend::{self, Backend},
40 error::{AllBackendsFailed, Error, HwDeviceInitFailed, Result},
41 ffi::{CallbackState, codec_supports_hwaccel, get_hw_format},
42 frame::Frame,
43};
44
45/// Hardware-accelerated video decoder.
46///
47/// Hardware-only — there is no software fallback inside this crate. If
48/// every hardware backend in the platform's probe order fails to open,
49/// `open` returns [`Error::AllBackendsFailed`] and the caller is
50/// responsible for falling back to a software decoder of their choice
51/// (e.g. `ffmpeg::decoder::Video`).
52///
53/// Mirrors `ffmpeg::decoder::Video`'s `send_packet`/`receive_frame` interface.
54/// Decoded frames are returned through [`crate::Frame`], a CPU-side wrapper
55/// whose accessors avoid the `AVPixelFormat`-enum UB that an unvalidated read
56/// of FFmpeg's raw integer pixel formats can trigger.
57///
58/// `open` does a true probe: each backend opens with a strict `get_format`
59/// callback. On the first non-transient error from a backend the decoder is
60/// torn down and the next backend in probe order is tried, with all packets
61/// seen so far replayed through it. The advance is *transactional* — the
62/// candidate backend must successfully build and accept the replayed packets
63/// before any probe state is consumed, so a failing backend in the middle of
64/// the order does not strand the caller without history. Once the first frame
65/// is delivered the probe collapses and subsequent calls go straight to the
66/// active (committed) backend.
67///
68/// The committed backend can still fail at runtime — e.g. VideoToolbox can
69/// decode a clip's first frames and then hit content its kernel can't handle
70/// (H.264 High 4:2:2 10-bit), surfacing `AVERROR_EXTERNAL`. Post-commit a
71/// non-transient, non-EOF error from the committed backend is reclassified to
72/// [`Error::AllBackendsFailed`] (see the `is_hw_decode_failure` predicate), so
73/// the [`crate::FfmpegVideoStreamDecoder`] wrapper still recognises it as a
74/// HW-path exhaustion and falls back to software. The post-commit
75/// `unconsumed_packets` is empty (the probe buffer is gone); the wrapper's
76/// rolling since-last-keyframe buffer supplies the replay set.
77pub struct VideoDecoder {
78 /// Live FFmpeg state for the currently active backend.
79 state: DecoderState,
80 /// Reusable frame buffer used for hw-side decoding before transfer / move.
81 /// Internal use only — never handed to callers.
82 hw_frame: frame::Video,
83 /// Probe state: present until the first frame is received from the active
84 /// backend, then `None`. While `Some`, packets are buffered for replay and
85 /// non-transient errors / decoder failures advance to the next backend.
86 probe: Option<ProbeState>,
87 /// CPU-side frames produced by a candidate decoder during probe replay
88 /// (when its internal queue filled and we had to drain output before the
89 /// next `send_packet`). Already transferred from the candidate's
90 /// `AVHWFramesContext` to a CPU frame, so they remain valid after the
91 /// candidate state is committed. [`Self::receive_frame`] dequeues these
92 /// FIFO before reading from `state.inner`.
93 pending_frames: VecDeque<frame::Video>,
94 /// Per-decoder byte budget for [`Self::pending_frames`] during probe
95 /// replay. Defaults to [`DEFAULT_MAX_PROBE_PENDING_BYTES`]; override via
96 /// [`Self::with_max_probe_pending_bytes`].
97 max_probe_pending_bytes: usize,
98}
99
100/// Owned FFmpeg state for one open codec context. Has its own `Drop` so we
101/// can swap it out cleanly during a probe advance via `mem::replace`.
102struct DecoderState {
103 /// Wrapped FFmpeg decoder. `ManuallyDrop` so we can sequence its drop
104 /// before freeing the callback state.
105 inner: ManuallyDrop<ffmpeg_next::decoder::Video>,
106 /// Backend driving this state.
107 backend: Backend,
108 /// Owned reference produced by `av_hwdevice_ctx_create`.
109 hw_device_ref: *mut AVBufferRef,
110 /// Owned `Box<CallbackState>` raw pointer; `AVCodecContext::opaque`
111 /// aliases it.
112 callback_state: *mut CallbackState,
113}
114
115/// Maximum number of packets we are willing to buffer for probe replay
116/// before abandoning the fallback safety net. Set high enough to absorb
117/// long B-frame GOPs and codec setup latency, low enough to bound memory
118/// against malicious / pathological streams that never produce a first
119/// frame.
120const MAX_PROBE_PACKETS: usize = 256;
121
122/// Maximum total compressed-byte size of buffered probe packets. Each
123/// `Packet` clone holds a refcounted reference to the demuxer's bitstream
124/// data — even though the clone itself is shallow, the underlying buffers
125/// stay alive until we drop them. 64 MiB is generous for normal video and
126/// gives untrusted media a hard ceiling.
127const MAX_PROBE_PACKET_BYTES: usize = 64 * 1024 * 1024;
128
129/// Hard cap on the number of side-data entries we tolerate per buffered
130/// packet. `av_packet_ref` allocates an `AVPacketSideData` descriptor and
131/// an `AVBufferRef` per entry, so a packet stuffed with many tiny or
132/// zero-sized entries can consume significant memory in descriptor /
133/// allocator overhead even after [`packet_side_data_bytes`] charges
134/// [`SIDE_DATA_ENTRY_OVERHEAD`] bytes per entry. Refusing to clone such
135/// packets short-circuits the descriptor explosion path.
136///
137/// Sized for legitimate streams (typical video packets carry 0-5 side-
138/// data entries; SEI-heavy HEVC/AV1 maybe a dozen) while comfortably
139/// rejecting weaponised input.
140///
141/// Shared with the [`crate::FfmpegVideoStreamDecoder`] rolling GOP buffer,
142/// which charges the same side-data budget so its byte cap is a true upper
143/// bound on retained memory rather than counting bare payloads.
144pub(crate) const MAX_PROBE_PACKET_SIDE_DATA_ENTRIES: usize = 64;
145
146/// Conservative per-side-data-entry overhead estimate used by both
147/// [`packet_side_data_bytes`] and the budget accounting in
148/// [`VideoDecoder::send_packet`]. Counts the `AVPacketSideData`
149/// descriptor (24 bytes per the FFmpeg 8.x bindings), the `AVBufferRef`
150/// FFmpeg allocates per entry, and a margin for malloc bookkeeping
151/// (header bytes, alignment slack). Setting it on the high side keeps
152/// the byte cap a true upper bound on retained memory; under-charging
153/// would let many tiny entries slip past the cap.
154const SIDE_DATA_ENTRY_OVERHEAD: usize = 80;
155
156/// Conservative upper-bound bytes-per-pixel multiplier used to estimate
157/// the size of a CPU frame **before** `av_hwframe_transfer_data`
158/// allocates its pixel buffers. Covers every HW download format this
159/// crate produces (worst case is `P416LE` / `P412LE` at 6 bytes/pixel
160/// for 16-bit 4:4:4 semi-planar) plus a margin for FFmpeg's per-row
161/// stride alignment (typically 32-byte aligned, ~5% extra at HD widths
162/// and below).
163///
164/// Used by [`drain_into_pending`] as a pre-transfer guard: if the
165/// product `width * height * WORST_CASE_BYTES_PER_PIXEL` would already
166/// push `pending_bytes` past `max_probe_pending_bytes`, the candidate
167/// replay refuses the frame *before* allocating. Without this, FFmpeg
168/// would perform the full HW→CPU download (potentially ~100 MiB for
169/// 8K HDR) and we would only reject the frame after RSS had already
170/// spiked. The post-transfer accounting via [`cpu_frame_bytes`] stays in
171/// place as a backstop using the frame's actual stride/format.
172///
173/// Slightly over-charges true 4:2:0 NV12 / P010 frames (which dominate
174/// real workloads) — that's the right side to err on. Callers feeding
175/// 8K+ workloads through the probe path can tune
176/// [`VideoDecoder::with_max_probe_pending_bytes`] upward to compensate.
177const WORST_CASE_BYTES_PER_PIXEL: usize = 8;
178
179/// Maximum number of CPU frames we are willing to queue from a candidate
180/// during probe replay. Each frame is a fully-allocated CPU buffer
181/// (~3 MiB for 1080p NV12, ~24 MiB for 4K P010, ~96 MiB for 8K P010), so
182/// an unbounded queue would OOM on a candidate with a shallow internal
183/// queue against a deep replay history. This cap, together with
184/// [`DEFAULT_MAX_PROBE_PENDING_BYTES`], is enforced as a hard limit during
185/// replay: once either limit is reached, probe buffering fails for the
186/// candidate (returns `ENOMEM` from `drain_into_pending`) instead of
187/// queueing additional drained frames. The probe loop then advances to
188/// the next backend or returns `Error::AllBackendsFailed` if exhausted.
189const MAX_PROBE_PENDING_FRAMES: usize = 16;
190
191/// Default byte budget for probe-replay drained frames. 256 MiB is enough
192/// for 16 frames at 4K P010 (~24 MiB each = 384 MiB worst case under the
193/// count cap), and is the cap that fires first for very high-resolution
194/// content (8K P010: ~96 MiB per frame → only ~2 frames fit).
195///
196/// Override per-decoder with [`VideoDecoder::with_max_probe_pending_bytes`]
197/// when targeting 8K+ workloads or memory-constrained environments.
198///
199/// TODO: when frames significantly exceed typical sizes, consider
200/// memmap-backed pending buffers (write transferred frames to a temp file
201/// or shared-memory segment) so the resident set stays bounded even when
202/// the byte cap is raised. Out of scope for now.
203pub const DEFAULT_MAX_PROBE_PENDING_BYTES: usize = 256 * 1024 * 1024;
204
205/// State carried only during the probe window (before the first successful
206/// frame). Holds enough information to tear down the current decoder and
207/// retry with the next backend.
208struct ProbeState {
209 parameters: codec::Parameters,
210 codec: Codec,
211 /// Backends still to try, in order. Empty means "no more options after
212 /// the active one fails" — `advance_probe` then surfaces
213 /// [`Error::AllBackendsFailed`] so the contract is the same on
214 /// single-backend platforms (e.g. macOS) as on multi-backend ones.
215 remaining_backends: Vec<Backend>,
216 /// Packets sent so far, kept for replay through any candidate backend.
217 /// Preserved across failed candidates — only cleared when the probe
218 /// collapses on a successful first frame, or when the probe is
219 /// abandoned due to the size caps.
220 buffered_packets: Vec<Packet>,
221 /// Cumulative size (in compressed bytes) of `buffered_packets`. Tracked
222 /// incrementally so we don't have to re-sum on every send.
223 buffered_bytes: usize,
224 /// Whether `send_eof` has been called; replayed alongside packets.
225 eof_sent: bool,
226 /// Per-backend errors captured since the probe window opened. Pushed
227 /// whenever a backend's failure triggers `advance_probe` (the active
228 /// backend that just failed) or a candidate's build / replay rejects
229 /// it. Drained into [`Error::AllBackendsFailed`] when the probe
230 /// exhausts every option.
231 attempts: Vec<(Backend, Box<Error>)>,
232}
233
234// SAFETY: All raw pointers are exclusively owned by `DecoderState` and never
235// shared. `ffmpeg::decoder::Video` is itself `Send` (its `Context` carries an
236// `unsafe impl Send`). The decoder is not safe for concurrent use, hence not
237// `Sync`.
238unsafe impl Send for DecoderState {}
239unsafe impl Send for VideoDecoder {}
240
241impl Drop for DecoderState {
242 fn drop(&mut self) {
243 // Order matters:
244 // 1. Drop the codec context first. While it lives, FFmpeg may invoke
245 // `get_format`, which dereferences `callback_state` via `opaque`.
246 // 2. Free the callback state heap allocation.
247 // 3. Release our hw device reference (FFmpeg released its own when
248 // the codec context was freed in step 1).
249 unsafe {
250 ManuallyDrop::drop(&mut self.inner);
251 if !self.callback_state.is_null() {
252 drop(Box::from_raw(self.callback_state));
253 self.callback_state = ptr::null_mut();
254 }
255 if !self.hw_device_ref.is_null() {
256 av_buffer_unref(&mut self.hw_device_ref);
257 }
258 }
259 }
260}
261
262impl VideoDecoder {
263 /// Auto-probe hardware backends in the platform's default order.
264 ///
265 /// Each backend opens with a strict `get_format` callback. The first
266 /// backend whose `avcodec_open2` succeeds becomes active; if its first
267 /// frame is unusable (decode error, transfer failure, or a CPU-format
268 /// frame from a HW context) the decoder is torn down and the next backend
269 /// is tried — packets sent so far are replayed through the new decoder
270 /// transparently. The probe advance is transactional: the next backend
271 /// must build *and* accept the replayed history before any probe state is
272 /// consumed, so a misbehaving middle backend cannot strand the caller.
273 ///
274 /// [`Self::backend`] reflects whichever backend ultimately produced the
275 /// first frame.
276 ///
277 /// [`Error::AllBackendsFailed`] surfaces in two places, with the same
278 /// meaning ("no hardware backend can decode this stream — fall back to
279 /// software yourself"):
280 /// - From `open` itself, when no backend even opens.
281 /// - From [`Self::send_packet`] / [`Self::send_eof`] /
282 /// [`Self::receive_frame`], when the initially-opened backend fails
283 /// at decode time and every remaining backend in the probe order
284 /// either also fails or doesn't exist. On single-backend platforms
285 /// (e.g. macOS, where the order is `[VideoToolbox]`), this is the
286 /// only place a HW-only failure surfaces.
287 ///
288 /// In both cases, `attempts` carries the per-backend error log. When
289 /// the runtime path fires, `unconsumed_packets` also contains the
290 /// packets the decoder consumed from the caller before the probe
291 /// exhausted (refcounted shallow clones); for non-seekable inputs
292 /// (live streams, pipes) the caller can replay these directly into
293 /// a software decoder of their choice without re-demuxing. From the
294 /// open-time path the vec is empty since no packets have been sent.
295 ///
296 /// On `Ok`, the returned decoder **always** has an active probe
297 /// rescue safety net. If a parameters clone fails under memory
298 /// pressure before the probe state can be set up, `open` returns
299 /// `Err(Error::Ffmpeg(Other { errno: ENOMEM }))` rather than handing
300 /// back a live decoder with no fallback contract. No packets have
301 /// been sent yet, so the caller can retry or fall back to software
302 /// with the original `parameters` directly.
303 pub fn open(parameters: codec::Parameters) -> Result<Self> {
304 let codec = find_decoder(¶meters)?;
305 let order = backend::probe_order();
306
307 let mut attempts: Vec<(Backend, Box<Error>)> = Vec::new();
308 for (i, &backend) in order.iter().enumerate() {
309 // Use the checked clone — ffmpeg-next's `Parameters::clone` does
310 // `avcodec_parameters_alloc` without a null check and ignores the
311 // return of `avcodec_parameters_copy`. Under OOM that path silently
312 // produces a Parameters with a null inner pointer.
313 let cloned_for_build = match try_clone_parameters(¶meters) {
314 Ok(p) => p,
315 Err(e) => {
316 tracing::warn!(?backend, error = %e, "hwdecode: parameters clone failed");
317 attempts.push((backend, Box::new(Error::Ffmpeg(e))));
318 continue;
319 }
320 };
321 match Self::build_state(cloned_for_build, codec, backend) {
322 Ok(state) => {
323 tracing::info!(?backend, "hwdecode: opened video decoder (probing)");
324 let remaining = order[(i + 1)..].to_vec();
325 // Deep-copy the caller's `parameters` before storing in ProbeState.
326 // `codec::Parameters` from `stream.parameters()` carries an Rc
327 // owner pointing at the demuxer; moving that Rc to a worker
328 // thread (when VideoDecoder is sent) would race with the demuxer's
329 // Rc on the original thread. The checked clone copies the bytes
330 // into a fresh allocation with `owner: None`, severing the link.
331 //
332 // We always create ProbeState — even when `remaining` is empty
333 // (single-backend platforms like macOS) — so that a first-frame
334 // failure on the only backend surfaces as
335 // `Error::AllBackendsFailed` from `receive_frame` /
336 // `send_packet` rather than as a raw FFmpeg error. That keeps
337 // the API contract the same regardless of how many HW backends
338 // the platform exposes.
339 //
340 // If the clone fails (ENOMEM), fail the **whole open call**
341 // rather than returning a live decoder with `probe: None`.
342 // Returning Ok here would let the caller send packets that the
343 // active backend consumes, and a subsequent backend failure
344 // would then surface as a raw FFmpeg error with no
345 // `unconsumed_packets` — silently breaking the rescue contract
346 // for non-seekable inputs (live streams, pipes). Dropping the
347 // already-built `state` here runs its FFmpeg cleanup, and the
348 // caller can retry / fall back to software with the original
349 // parameters in their hand (no packets were consumed yet).
350 // Seed the probe's attempt log with any backends that failed
351 // to open earlier in this loop (including
352 // `BackendUnsupportedByCodec` and parameters-clone errors).
353 // Without this, a runtime exhaustion on the active backend
354 // would surface an `AllBackendsFailed` containing only the
355 // active backend's runtime failure — losing the original
356 // open-time causes that, on multi-backend platforms (Linux,
357 // Windows), are usually the more diagnostic signal. E.g. a
358 // VAAPI-then-CUDA host where VAAPI fails to open and CUDA
359 // later fails at first-frame must report both failures in
360 // probe order, not just CUDA.
361 let probe = match try_clone_parameters(¶meters) {
362 Ok(probe_params) => ProbeState {
363 parameters: probe_params,
364 codec,
365 remaining_backends: remaining,
366 buffered_packets: Vec::new(),
367 buffered_bytes: 0,
368 eof_sent: false,
369 attempts: std::mem::take(&mut attempts),
370 },
371 Err(e) => {
372 tracing::warn!(
373 error = %e,
374 "hwdecode: parameters clone failed for probe state at open; \
375 failing closed instead of returning a decoder without rescue"
376 );
377 return Err(Error::Ffmpeg(e));
378 }
379 };
380 return Ok(Self {
381 state,
382 hw_frame: alloc_av_frame().map_err(Error::Ffmpeg)?,
383 probe: Some(probe),
384 pending_frames: VecDeque::new(),
385 max_probe_pending_bytes: DEFAULT_MAX_PROBE_PENDING_BYTES,
386 });
387 }
388 Err(e) => {
389 tracing::warn!(?backend, error = %e, "hwdecode: backend open failed");
390 attempts.push((backend, Box::new(e)));
391 }
392 }
393 }
394 // No packets have been consumed at open time.
395 Err(Error::AllBackendsFailed(AllBackendsFailed::new(
396 attempts,
397 Vec::new(),
398 )))
399 }
400
401 /// Open the decoder with a specific backend. No probe, no fallback.
402 ///
403 /// If `backend` cannot actually decode this stream, the failure surfaces
404 /// from [`Self::receive_frame`] (the strict `get_format` callback returns
405 /// `AV_PIX_FMT_NONE`, the decoder errors out). The caller is responsible
406 /// for retrying with another hardware backend or falling back to a
407 /// software decoder of their choice (e.g. `ffmpeg::decoder::Video`).
408 pub fn open_with(parameters: codec::Parameters, backend: Backend) -> Result<Self> {
409 let codec = find_decoder(¶meters)?;
410 let state = Self::build_state(parameters, codec, backend)?;
411 Ok(Self {
412 state,
413 hw_frame: alloc_av_frame().map_err(Error::Ffmpeg)?,
414 probe: None,
415 pending_frames: VecDeque::new(),
416 max_probe_pending_bytes: DEFAULT_MAX_PROBE_PENDING_BYTES,
417 })
418 }
419
420 /// Override the byte budget for probe-replay queued frames. Defaults to
421 /// [`DEFAULT_MAX_PROBE_PENDING_BYTES`]. Use a higher value when targeting
422 /// 8K+ workloads where 16 frames at full size could exceed the default;
423 /// use a lower value in memory-constrained services to bound peak
424 /// allocation more tightly.
425 ///
426 /// Setting after the first frame has been delivered is harmless but has
427 /// no observable effect — the probe has already collapsed and the cap
428 /// only applies during replay drain.
429 ///
430 /// Returns `self` for builder-style chaining:
431 /// ```ignore
432 /// let decoder = VideoDecoder::open(params)?
433 /// .with_max_probe_pending_bytes(1024 * 1024 * 1024); // 1 GiB
434 /// ```
435 #[must_use]
436 pub fn with_max_probe_pending_bytes(mut self, bytes: usize) -> Self {
437 self.max_probe_pending_bytes = bytes;
438 self
439 }
440
441 /// The backend currently producing frames. While the probe is still in
442 /// progress (no frame received yet) this returns the optimistically
443 /// selected backend; after the first frame, it is the backend that
444 /// actually produced it. Once stable, never changes again.
445 pub fn backend(&self) -> Backend {
446 self.state.backend
447 }
448
449 /// Decoder width in pixels.
450 pub fn width(&self) -> u32 {
451 self.state.inner.width()
452 }
453
454 /// Decoder height in pixels.
455 pub fn height(&self) -> u32 {
456 self.state.inner.height()
457 }
458
459 /// Codec context time base.
460 pub fn time_base(&self) -> Rational {
461 self.state.inner.time_base()
462 }
463
464 /// Frame rate from the codec context, if known.
465 pub fn frame_rate(&self) -> Option<Rational> {
466 self.state.inner.frame_rate()
467 }
468
469 /// Reclassify a post-commit runtime error from the committed HW backend
470 /// into [`Error::AllBackendsFailed`] so the [`crate::FfmpegVideoStreamDecoder`]
471 /// wrapper recognises it as a HW-path exhaustion and falls back to
472 /// software. The single attempt records the committed backend
473 /// (`self.state.backend` is the live backend post-commit) paired with the
474 /// underlying FFmpeg error. `unconsumed_packets` is empty: the probe
475 /// buffer is gone after commit, so the wrapper's rolling
476 /// since-last-keyframe buffer supplies the replay set.
477 fn post_commit_hw_failure(&self, e: ffmpeg_next::Error) -> Error {
478 // `new_post_commit` stamps `FallbackOrigin::PostCommit`: the wrapper
479 // routes its replay on that explicit signal, not on the (here-empty)
480 // `unconsumed_packets`, which a probe-era first-packet cap trip also
481 // leaves empty.
482 Error::AllBackendsFailed(AllBackendsFailed::new_post_commit(vec![(
483 self.state.backend,
484 Box::new(Error::Ffmpeg(e)),
485 )]))
486 }
487
488 /// Submit a packet to the decoder.
489 ///
490 /// On success — and only on success — the packet is buffered for potential
491 /// replay through a fallback backend while the probe is active. EAGAIN
492 /// (decoder needs `receive_frame` to drain output first) propagates as
493 /// normal backpressure; the caller drains then retries.
494 ///
495 /// While the probe is active, a non-transient error (e.g. the active HW
496 /// backend rejecting this stream's geometry on first packet) advances the
497 /// probe to the next candidate and retries the packet there. The caller
498 /// observes only the eventual success or, if the probe is exhausted, the
499 /// final error.
500 ///
501 /// **Atomic probe rescue.** While the probe is active, the rescue
502 /// invariant is that everything FFmpeg has consumed since open is
503 /// reflected in `buffered_packets` (so a future
504 /// [`Error::AllBackendsFailed`] can hand a complete replay history
505 /// back to the caller for software fallback on a non-seekable input).
506 /// If we cannot prove this packet is buffer-able — its side-data
507 /// entry count exceeds [`MAX_PROBE_PACKET_SIDE_DATA_ENTRIES`], its
508 /// bytes would push the probe past [`MAX_PROBE_PACKETS`] or
509 /// [`MAX_PROBE_PACKET_BYTES`], or [`av_packet_ref`] fails ENOMEM —
510 /// `send_packet` returns [`Error::AllBackendsFailed`] **without
511 /// invoking** `state.inner.send_packet` on this packet. The caller's
512 /// packet stays in their hand and `unconsumed_packets` carries the
513 /// pre-existing buffered history, so they can replay
514 /// `unconsumed_packets` plus the current packet through their
515 /// software decoder of choice. The post-probe path (after the first
516 /// frame, when `self.probe` is `None`) skips this pre-flight
517 /// entirely.
518 pub fn send_packet(&mut self, packet: &Packet) -> Result<()> {
519 loop {
520 // Pre-flight while probe is active: prove we can record this
521 // packet for replay BEFORE the active decoder consumes it.
522 // `staged_clone` carries the refcounted clone and the new
523 // `buffered_bytes` value through the send below; we only commit
524 // them to the probe state if FFmpeg accepts the packet.
525 let staged_clone: Option<(Packet, usize)> = if let Some(probe) = self.probe.as_ref() {
526 // Step 1: side-data entry count cap. Read just `side_data_elems`
527 // (no array walk yet) so a corrupt or weaponised value cannot
528 // drive an unbounded loop from the safe entry point.
529 let side_count = packet_side_data_count(packet);
530 if side_count > MAX_PROBE_PACKET_SIDE_DATA_ENTRIES {
531 let probe = self.probe.take().expect("probe present");
532 tracing::warn!(
533 side_data_entries = side_count,
534 max_side_data_entries = MAX_PROBE_PACKET_SIDE_DATA_ENTRIES,
535 trigger = "side_data_entry_cap",
536 "hwdecode: probe rescue exhausted before consuming packet; \
537 returning AllBackendsFailed without invoking decoder"
538 );
539 return Err(Error::AllBackendsFailed(AllBackendsFailed::new(
540 probe.attempts,
541 probe.buffered_packets,
542 )));
543 }
544 // Step 2: byte / packet count cap. `packet_side_data_bytes`
545 // clamps its walk to MAX_PROBE_PACKET_SIDE_DATA_ENTRIES as
546 // defense-in-depth even though the count check above already
547 // bounded the array length.
548 let pkt_size = packet.size().saturating_add(packet_side_data_bytes(
549 packet,
550 MAX_PROBE_PACKET_SIDE_DATA_ENTRIES,
551 ));
552 let new_count = probe.buffered_packets.len() + 1;
553 let new_bytes = probe.buffered_bytes.saturating_add(pkt_size);
554 if new_count > MAX_PROBE_PACKETS || new_bytes > MAX_PROBE_PACKET_BYTES {
555 let probe = self.probe.take().expect("probe present");
556 tracing::warn!(
557 packets = new_count,
558 bytes = new_bytes,
559 side_data_entries = side_count,
560 max_packets = MAX_PROBE_PACKETS,
561 max_bytes = MAX_PROBE_PACKET_BYTES,
562 trigger = "byte_or_packet_cap",
563 "hwdecode: probe rescue exhausted before consuming packet; \
564 returning AllBackendsFailed without invoking decoder"
565 );
566 return Err(Error::AllBackendsFailed(AllBackendsFailed::new(
567 probe.attempts,
568 probe.buffered_packets,
569 )));
570 }
571 // Step 3: pre-clone before consuming. `av_packet_ref` is a
572 // refcounted shallow clone (no payload deep-copy) but can still
573 // ENOMEM on heavy side-data; if it does we bail rather than
574 // consuming a packet we can't track.
575 match try_clone_packet(packet) {
576 Ok(c) => Some((c, new_bytes)),
577 Err(e) => {
578 let probe = self.probe.take().expect("probe present");
579 tracing::warn!(
580 error = %e,
581 "hwdecode: packet clone failed before consuming; \
582 returning AllBackendsFailed without invoking decoder"
583 );
584 return Err(Error::AllBackendsFailed(AllBackendsFailed::new(
585 probe.attempts,
586 probe.buffered_packets,
587 )));
588 }
589 }
590 } else {
591 None
592 };
593
594 match self.state.inner.send_packet(packet) {
595 Ok(()) => {
596 if let Some((cloned, new_bytes)) = staged_clone {
597 // Probe is still Some here: the only paths that take it are
598 // the bailouts above (which return) and `advance_probe`'s
599 // exhaustion (which would have propagated via `?`). Commit
600 // the clone now that FFmpeg has accepted the packet.
601 if let Some(probe) = self.probe.as_mut() {
602 probe.buffered_packets.push(cloned);
603 probe.buffered_bytes = new_bytes;
604 }
605 }
606 return Ok(());
607 }
608 Err(e) if is_transient(&e) => {
609 // EAGAIN / EOF backpressure — pass through unchanged. The
610 // staged clone drops; the caller will retry after draining
611 // and we'll re-clone at the top of the loop.
612 return Err(Error::Ffmpeg(e));
613 }
614 Err(e) => {
615 if self.probe.is_some() {
616 // advance_probe consumes the error into `attempts` and
617 // either installs a candidate (Ok — loop top re-clones for
618 // the new candidate) or surfaces AllBackendsFailed (Err —
619 // `?` propagates). Either way the staged clone we just
620 // built drops without entering history; the next iteration
621 // clones afresh against the new active state.
622 self.advance_probe(Error::Ffmpeg(e))?;
623 continue;
624 }
625 // Post-commit (probe gone): the committed HW backend just failed
626 // at runtime. A HW-only decoder's non-transient, non-EOF error
627 // means the backend can't decode this content — reclassify to
628 // AllBackendsFailed so the wrapper falls back to software.
629 if is_hw_decode_failure(&e) {
630 return Err(self.post_commit_hw_failure(e));
631 }
632 return Err(Error::Ffmpeg(e));
633 }
634 }
635 }
636 }
637
638 /// Signal end-of-stream to the decoder.
639 ///
640 /// Recorded for replay only if the underlying `send_eof` succeeds. While
641 /// the probe is active, non-transient errors trigger probe advance and
642 /// retry, matching `send_packet`'s behaviour.
643 pub fn send_eof(&mut self) -> Result<()> {
644 loop {
645 match self.state.inner.send_eof() {
646 Ok(()) => {
647 if let Some(probe) = self.probe.as_mut() {
648 probe.eof_sent = true;
649 }
650 return Ok(());
651 }
652 Err(e) if is_transient(&e) => return Err(Error::Ffmpeg(e)),
653 Err(e) => {
654 if self.probe.is_some() {
655 self.advance_probe(Error::Ffmpeg(e))?;
656 continue;
657 }
658 // Post-commit: committed HW backend failed draining at EOF.
659 // Reclassify a HW-decode failure so the wrapper falls back to SW
660 // (the SW decoder will re-receive the buffered GOP + EOF).
661 if is_hw_decode_failure(&e) {
662 return Err(self.post_commit_hw_failure(e));
663 }
664 return Err(Error::Ffmpeg(e));
665 }
666 }
667 }
668 }
669
670 /// Receive a CPU-side decoded frame.
671 ///
672 /// The frame is downloaded with `av_hwframe_transfer_data` and metadata
673 /// is copied via `av_frame_copy_props`. The caller's frame is always
674 /// unref'd first, so reuse across resolution changes or different
675 /// decoders is safe.
676 ///
677 /// While the probe window is open, *any* non-transient failure (decode
678 /// error, transfer error, copy_props error, or a CPU-format frame from a
679 /// HW-opened context) tears down the current decoder and advances to the
680 /// next hardware backend in probe order, replaying buffered packets
681 /// through it. Frames the candidate produced during replay (drained when
682 /// `send_packet` returned EAGAIN) are queued and delivered FIFO via this
683 /// method, so the caller never loses initial frames after a fallback.
684 ///
685 /// This crate is hardware-only: there is no software fallback inside the
686 /// decoder. When every backend in the probe order has been exhausted —
687 /// including the case of a single-backend platform whose only backend
688 /// failed — this returns [`Error::AllBackendsFailed`] with the per-
689 /// backend attempt log so the caller can branch into a software
690 /// decoder of their choice.
691 ///
692 /// Returns the same transient signals as `ffmpeg::decoder::Video`:
693 /// `Error::Ffmpeg(Other { errno: EAGAIN })` when no frame is ready and
694 /// more packets must be sent, and `Error::Ffmpeg(Eof)` once fully drained.
695 pub fn receive_frame(&mut self, frame: &mut Frame) -> Result<()> {
696 // Pre-drain frames queued during probe replay. They are already CPU-side
697 // (transferred at drain time, when the candidate's HW context was alive)
698 // so we just move them into the caller's slot.
699 if self.try_pop_pending(frame) {
700 return Ok(());
701 }
702
703 loop {
704 let res = self.state.inner.receive_frame(&mut self.hw_frame);
705 match res {
706 Err(e) => {
707 // EAGAIN is normal backpressure — pass through unconditionally.
708 if is_eagain(&e) {
709 return Err(Error::Ffmpeg(e));
710 }
711 // EOF (and every other non-transient error): if we are still
712 // probing, treat it as candidate failure — a backend that drains
713 // to EOF without ever producing a frame should not silently
714 // present as "stream over" to the caller. Advance and retry; if
715 // every backend has been exhausted, advance_probe surfaces
716 // AllBackendsFailed and `?` propagates it.
717 if self.probe.is_some() {
718 self.advance_probe(Error::Ffmpeg(e))?;
719 // Probe advance may have populated `pending_frames`; deliver
720 // one of those before reading more from the new candidate.
721 if self.try_pop_pending(frame) {
722 return Ok(());
723 }
724 continue;
725 }
726 // Probe collapsed already. A non-transient, non-EOF error from the
727 // committed HW backend is a runtime HW-decode failure — reclassify
728 // to AllBackendsFailed so the wrapper falls back to software.
729 // `is_hw_decode_failure` excludes EOF, so a genuine end-of-stream
730 // still propagates as `Error::Ffmpeg(Eof)` (never trapped in an
731 // infinite fallback-retry loop).
732 if is_hw_decode_failure(&e) {
733 return Err(self.post_commit_hw_failure(e));
734 }
735 // Surface the error (including EOF for a genuinely drained stream).
736 return Err(Error::Ffmpeg(e));
737 }
738 Ok(()) => {
739 // Always attempt the HW→CPU transfer. With strict `get_format`,
740 // libavcodec can only deliver frames in the wired-up HW format
741 // (or fail). If a misbehaving codec ever hands us a CPU-side
742 // frame anyway, `av_hwframe_transfer_data` returns AVERROR(EINVAL)
743 // (neither src nor dst has an AVHWFramesContext attached) and we
744 // route through the same error path below.
745 match unsafe { transfer_hw_frame(frame, &mut self.hw_frame) } {
746 Ok(()) => {
747 self.probe = None;
748 return Ok(());
749 }
750 Err(e) => {
751 if self.probe.is_some() {
752 self.advance_probe(Error::Ffmpeg(e))?;
753 unsafe { av_frame_unref(frame.as_inner_mut().as_mut_ptr()) };
754 if self.try_pop_pending(frame) {
755 return Ok(());
756 }
757 continue;
758 }
759 // Post-commit transfer failure: an unsupported CPU output
760 // pix_fmt surfaces as AVERROR(EINVAL) and a context-loss as
761 // Bug/Bug2/Unknown — all HW-output problems, never input
762 // corruption. Reclassify so the wrapper falls back to software.
763 if is_hw_decode_failure(&e) {
764 return Err(self.post_commit_hw_failure(e));
765 }
766 return Err(Error::Ffmpeg(e));
767 }
768 }
769 }
770 }
771 }
772 }
773
774 /// Pop one queued frame (produced by a candidate decoder during probe
775 /// replay) into the caller's slot. Returns `true` when a frame was
776 /// delivered, `false` when the queue was empty.
777 fn try_pop_pending(&mut self, frame: &mut Frame) -> bool {
778 let Some(mut buffered) = self.pending_frames.pop_front() else {
779 return false;
780 };
781 // SAFETY: `buffered` is a CPU-side AVFrame we previously transferred
782 // and pushed into the queue; both pointers are valid.
783 unsafe {
784 av_frame_unref(frame.as_inner_mut().as_mut_ptr());
785 av_frame_move_ref(frame.as_inner_mut().as_mut_ptr(), buffered.as_mut_ptr());
786 }
787 // Probe semantics: delivering a frame collapses the probe.
788 self.probe = None;
789 true
790 }
791
792 /// Flush internal buffers (e.g. after a seek).
793 ///
794 /// Discards every frame buffered by the decoder, every frame queued during
795 /// probe replay (`pending_frames`), and the residual `hw_frame` scratch
796 /// buffer. Probe-time replay state (buffered packets, EOF marker) is also
797 /// cleared since post-seek packets do not align with the previously
798 /// captured history. After a flush, the next `receive_frame` waits for new
799 /// post-seek input.
800 pub fn flush(&mut self) {
801 self.state.inner.flush();
802 // SAFETY: hw_frame is a valid AVFrame we own; av_frame_unref is a no-op
803 // for an already-empty frame.
804 unsafe { av_frame_unref(self.hw_frame.as_mut_ptr()) };
805 self.pending_frames.clear();
806 if let Some(probe) = self.probe.as_mut() {
807 probe.buffered_packets.clear();
808 probe.buffered_bytes = 0;
809 probe.eof_sent = false;
810 }
811 }
812
813 /// Try the next backend in `remaining_backends`. Transactional: a
814 /// candidate must successfully build and accept the replayed history
815 /// before any probe state is consumed. Backends that fail to build or
816 /// reject the replay are recorded into `probe.attempts` and the loop
817 /// continues to the next one.
818 ///
819 /// `last_error` is the error that triggered this advance — i.e. the
820 /// failure of the currently active backend on `send_packet` /
821 /// `send_eof` / `receive_frame`. It is recorded against the active
822 /// backend before any candidate is tried so that a final
823 /// `AllBackendsFailed` carries the full attempt log including the
824 /// initially-opened backend's runtime failure.
825 ///
826 /// Returns:
827 /// - `Ok(())` when a candidate is installed and replay completed —
828 /// caller should retry the operation.
829 /// - `Err(Error::AllBackendsFailed(p))` when every remaining
830 /// backend has been exhausted (including the just-failed active one).
831 /// `p.attempts()` carries the per-backend failure log.
832 /// This is what the documented `open` contract promises, surfaced at
833 /// runtime so the caller can branch into a software fallback. On a
834 /// single-backend platform (e.g. macOS), this fires after the only
835 /// backend's first-frame failure; on multi-backend platforms it
836 /// fires after the last candidate's failure.
837 /// - `Err(_)` for other fatal conditions surfaced by probe machinery
838 /// itself (e.g. `alloc_av_frame` ENOMEM during replay drain).
839 fn advance_probe(&mut self, last_error: Error) -> Result<()> {
840 // Record the failure that triggered this advance against the active
841 // backend. If the probe was somehow already gone (shouldn't happen —
842 // call sites guard with `self.probe.is_some()`), just propagate the
843 // error so behaviour matches the pre-fix code path.
844 let active_backend = self.state.backend;
845 match self.probe.as_mut() {
846 Some(probe) => probe.attempts.push((active_backend, Box::new(last_error))),
847 None => return Err(last_error),
848 }
849
850 // Drop frames previously queued from the backend we're now abandoning.
851 // They came from a candidate that just failed for cause and cannot be
852 // trusted alongside frames we may queue from the next candidate. (If
853 // this method is called repeatedly via chained probe advances, this
854 // also keeps `pending_frames` from accumulating frames from multiple
855 // rejected backends.)
856 self.pending_frames.clear();
857
858 loop {
859 // Snapshot inputs without mutating probe state. Use the checked
860 // clone helper rather than `Parameters::clone` (which masks ENOMEM).
861 let (next_backend, parameters, codec) = match self.probe.as_ref() {
862 Some(probe) if !probe.remaining_backends.is_empty() => {
863 let parameters = match try_clone_parameters(&probe.parameters) {
864 Ok(p) => p,
865 Err(e) => {
866 tracing::warn!(
867 error = %e,
868 "hwdecode: parameters clone failed during probe advance; popping backend and trying next"
869 );
870 let popped = self
871 .probe
872 .as_mut()
873 .expect("probe state present")
874 .remaining_backends
875 .remove(0);
876 self
877 .probe
878 .as_mut()
879 .expect("probe state present")
880 .attempts
881 .push((popped, Box::new(Error::Ffmpeg(e))));
882 continue;
883 }
884 };
885 (probe.remaining_backends[0], parameters, probe.codec)
886 }
887 // No more candidates — surface the accumulated attempt log as
888 // AllBackendsFailed so single- and multi-backend platforms have
889 // the same contract for "every HW backend failed."
890 //
891 // Hand the buffered packet history back to the caller along
892 // with the attempt log: those packets were consumed from the
893 // caller's demuxer (and refcounted-cloned into `buffered_packets`)
894 // before the probe exhausted, and for non-seekable inputs the
895 // caller cannot re-demux them. Returning them here lets a
896 // caller-side software fallback replay the same byte history
897 // through `ffmpeg::decoder::Video` without losing initial frames.
898 // Dropping `ProbeState` after the take frees the codec/params
899 // refs we no longer need; only `attempts` and `buffered_packets`
900 // are retained.
901 _ => {
902 let (attempts, unconsumed_packets) = self
903 .probe
904 .take()
905 .map(|p| (p.attempts, p.buffered_packets))
906 .unwrap_or_default();
907 return Err(Error::AllBackendsFailed(AllBackendsFailed::new(
908 attempts,
909 unconsumed_packets,
910 )));
911 }
912 };
913
914 let prev_backend = self.state.backend;
915 tracing::warn!(from = ?prev_backend, to = ?next_backend, "hwdecode: advancing probe");
916
917 // Build candidate. On failure, record into attempts and continue
918 // without touching the packet buffer.
919 let mut candidate_state = match Self::build_state(parameters, codec, next_backend) {
920 Ok(s) => s,
921 Err(e) => {
922 tracing::warn!(?next_backend, error = %e, "hwdecode: candidate build failed");
923 self
924 .probe
925 .as_mut()
926 .expect("probe state present")
927 .remaining_backends
928 .remove(0);
929 self
930 .probe
931 .as_mut()
932 .expect("probe state present")
933 .attempts
934 .push((next_backend, Box::new(e)));
935 continue;
936 }
937 };
938
939 // Replay buffered history through the candidate WITHOUT installing it.
940 // We borrow the buffer immutably; if replay fails the candidate's Drop
941 // releases the FFmpeg state and the buffer is preserved for the next
942 // attempt.
943 //
944 // EAGAIN handling: `avcodec_send_packet` may return EAGAIN when its
945 // internal queue is full and the user is expected to drain output
946 // first (B-frame buffering, candidate-specific queue depth, etc.).
947 // This is normal flow — we drain frames out of the candidate, transfer
948 // each one to a CPU frame, and stash them in `local_pending`. After
949 // commit they move to `self.pending_frames` and are delivered FIFO
950 // by `receive_frame`, so the caller never loses initial frames.
951 let mut local_pending: VecDeque<frame::Video> = VecDeque::new();
952 let mut local_pending_bytes: usize = 0;
953 let max_pending_bytes = self.max_probe_pending_bytes;
954 let replay_result: std::result::Result<(), ffmpeg_next::Error> = {
955 let probe = self.probe.as_ref().expect("probe state present");
956 let mut hw_buf = match alloc_av_frame() {
957 Ok(f) => f,
958 Err(e) => return Err(Error::Ffmpeg(e)),
959 };
960 let mut r: std::result::Result<(), ffmpeg_next::Error> = Ok(());
961
962 'replay: for pkt in &probe.buffered_packets {
963 loop {
964 match candidate_state.inner.send_packet(pkt) {
965 Ok(()) => break,
966 Err(e) if is_eagain(&e) => {
967 // Drain candidate output (transferring + queueing each frame)
968 // and retry the same packet.
969 if let Err(de) = drain_into_pending(
970 &mut candidate_state.inner,
971 &mut hw_buf,
972 &mut local_pending,
973 &mut local_pending_bytes,
974 max_pending_bytes,
975 ) {
976 r = Err(de);
977 break 'replay;
978 }
979 }
980 Err(e) => {
981 r = Err(e);
982 break 'replay;
983 }
984 }
985 }
986 }
987 if r.is_ok() && probe.eof_sent {
988 // `avcodec_send_packet(NULL)` (which `send_eof` becomes) can
989 // return EAGAIN with the same drain-output-first semantics as
990 // a regular send_packet. Loop drain+retry instead of failing
991 // the candidate on backpressure.
992 loop {
993 match candidate_state.inner.send_eof() {
994 Ok(()) => break,
995 Err(e) if is_eagain(&e) => {
996 if let Err(de) = drain_into_pending(
997 &mut candidate_state.inner,
998 &mut hw_buf,
999 &mut local_pending,
1000 &mut local_pending_bytes,
1001 max_pending_bytes,
1002 ) {
1003 r = Err(de);
1004 break;
1005 }
1006 }
1007 Err(e) => {
1008 r = Err(e);
1009 break;
1010 }
1011 }
1012 }
1013 }
1014 r
1015 };
1016
1017 if let Err(e) = replay_result {
1018 tracing::warn!(?next_backend, error = %e, "hwdecode: candidate replay failed");
1019 // Drop candidate explicitly so its FFI cleanup runs now. Discard any
1020 // frames we drained from this candidate — they're tied to a decoder
1021 // we're throwing away.
1022 drop(candidate_state);
1023 drop(local_pending);
1024 self
1025 .probe
1026 .as_mut()
1027 .expect("probe state present")
1028 .remaining_backends
1029 .remove(0);
1030 self
1031 .probe
1032 .as_mut()
1033 .expect("probe state present")
1034 .attempts
1035 .push((next_backend, Box::new(Error::Ffmpeg(e))));
1036 continue;
1037 }
1038
1039 // Commit: install the candidate, clear residual hw_frame, queue the
1040 // drained frames for the caller, and pop the now-active backend.
1041 self.state = candidate_state;
1042 unsafe { av_frame_unref(self.hw_frame.as_mut_ptr()) };
1043 self.pending_frames.append(&mut local_pending);
1044 self
1045 .probe
1046 .as_mut()
1047 .expect("probe state present")
1048 .remaining_backends
1049 .remove(0);
1050 return Ok(());
1051 }
1052 }
1053
1054 /// Build raw FFmpeg state for one hardware backend. Strict `get_format`
1055 /// (NONE on missing HW format); cross-backend fallback is the caller's job.
1056 fn build_state(
1057 parameters: codec::Parameters,
1058 codec: Codec,
1059 backend: Backend,
1060 ) -> Result<DecoderState> {
1061 // Use our checked allocator instead of Context::from_parameters, which
1062 // does not null-check avcodec_alloc_context3 and would feed a null
1063 // AVCodecContext into FFmpeg under OOM.
1064 let mut ctx = build_codec_context(¶meters)?;
1065 let av_type = backend.av_hwdevice_type();
1066
1067 // Verify the codec advertises this hwaccel **with the exact HW pix_fmt
1068 // we're about to wire up in `get_format`**. FFmpeg's HW config table
1069 // is keyed per (device_type, pix_fmt); a codec can advertise the same
1070 // device with several HW pix_fmts, so matching only on device_type
1071 // would let probing succeed for a backend whose pix_fmt the codec
1072 // never offers — the failure would then surface deep inside the
1073 // probe/decode loop. Matching the exact pix_fmt keeps the strict
1074 // `get_format` honest and gives `open_with` a clean rejection.
1075 let hw_pix_fmt = backend.hw_pixel_format();
1076 if !codec_supports_hwaccel(unsafe { codec.as_ptr() }, av_type, hw_pix_fmt as i32) {
1077 return Err(Error::BackendUnsupportedByCodec(backend));
1078 }
1079
1080 // Create the device context.
1081 let mut hw_device_ref: *mut AVBufferRef = ptr::null_mut();
1082 // SAFETY: `hw_device_ref` is a stack ptr we hand FFmpeg to fill.
1083 let ret = unsafe {
1084 av_hwdevice_ctx_create(&mut hw_device_ref, av_type, ptr::null(), ptr::null_mut(), 0)
1085 };
1086 if ret < 0 {
1087 return Err(Error::HwDeviceInitFailed(HwDeviceInitFailed::new(
1088 backend,
1089 ffmpeg_next::Error::from(ret),
1090 )));
1091 }
1092
1093 let callback_state = Box::into_raw(Box::new(CallbackState {
1094 wanted: hw_pix_fmt,
1095 wanted_int: hw_pix_fmt as i32,
1096 }));
1097 // RAII guard: from now until the end-of-function `into_owned()`, every
1098 // early return — `av_buffer_ref` failure, `open_as` failure, codec_type
1099 // mismatch, or any future error path added between here and the
1100 // `DecoderState` construction — frees `hw_device_ref` and
1101 // `callback_state` via the guard's Drop. Without it, each error site
1102 // had to remember to clean up these two FFI-owned resources by hand;
1103 // the codec_type-mismatch branch was missed and silently leaked one
1104 // device ref + one heap allocation per bad input.
1105 let guard = PartialBuildState {
1106 hw_device_ref,
1107 callback_state,
1108 };
1109
1110 // SAFETY: ctx is a freshly-constructed AVCodecContext we own;
1111 // av_buffer_ref bumps the refcount of the device buffer for FFmpeg's
1112 // use (we keep our own ref in `hw_device_ref` for cleanup).
1113 // av_buffer_ref returns NULL on allocation failure; we must check it
1114 // before assigning, otherwise the codec context would be opened with a
1115 // HW-flagged setup but no actual device reference.
1116 let device_ref_for_ctx = unsafe { av_buffer_ref(hw_device_ref) };
1117 if device_ref_for_ctx.is_null() {
1118 // guard's Drop frees hw_device_ref (the first ref) and callback_state.
1119 return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
1120 errno: libc::ENOMEM,
1121 }));
1122 }
1123 // SAFETY: device_ref_for_ctx is a valid AVBufferRef* from av_buffer_ref;
1124 // ctx is freshly built and owned by us. After this point ctx aliases
1125 // `callback_state` via `opaque` (FFmpeg never frees opaque, so
1126 // `callback_state` ownership stays with us / the guard) and aliases
1127 // `device_ref_for_ctx` (the second ref) via `hw_device_ctx` (FFmpeg
1128 // unrefs that on codec context drop, independent of the guard's first
1129 // ref).
1130 unsafe {
1131 let raw = ctx.as_mut_ptr();
1132 (*raw).hw_device_ctx = device_ref_for_ctx;
1133 (*raw).opaque = callback_state.cast();
1134 (*raw).get_format = Some(get_hw_format);
1135 }
1136
1137 // Open the decoder. On failure `ctx`/`opened` Drop releases the codec
1138 // context (and via that the second device ref); the guard releases the
1139 // first device ref and the callback state.
1140 //
1141 // We deliberately bypass `Opened::video()` because it calls
1142 // `Context::medium()`, which reads `AVCodecContext.codec_type` as the
1143 // bindgen `AVMediaType` enum — the same UB hazard we've been
1144 // systematically removing. Instead: validate `codec_type` as a raw
1145 // `c_int` ourselves, then construct the `decoder::Video` wrapper
1146 // directly via its public tuple field.
1147 let opened = ctx.decoder().open_as(codec).map_err(Error::Ffmpeg)?;
1148
1149 // Validate codec_type as a raw integer — never construct AVMediaType
1150 // from an unvalidated runtime value.
1151 // SAFETY: codec_type is bound as AVMediaType (`#[repr(i32)]`), same
1152 // size and alignment as i32; reading the bytes as i32 cannot be UB.
1153 let codec_type_int: i32 =
1154 unsafe { ptr::read(ptr::addr_of!((*opened.as_ptr()).codec_type) as *const i32) };
1155 let video_type_int: i32 = AVMediaType::AVMEDIA_TYPE_VIDEO as i32;
1156 if codec_type_int != video_type_int {
1157 // Not a video codec context — surface the same error
1158 // `Opened::video()` would have, without going through enum
1159 // construction. `opened`'s Drop releases the codec context; the
1160 // guard releases the first hw_device_ref and the callback state.
1161 return Err(Error::Ffmpeg(ffmpeg_next::Error::InvalidData));
1162 }
1163 // SAFETY of construction: `decoder::Video` is `pub struct Video(pub Opened)`.
1164 // We construct via the public field; this is the same wrapping
1165 // `Opened::video()` does on success, just without the enum read.
1166 let opened = ffmpeg_next::decoder::Video(opened);
1167
1168 // Disarm the guard and transfer ownership of both resources into the
1169 // returned DecoderState (whose own Drop handles their lifetime).
1170 let (hw_device_ref, callback_state) = guard.into_owned();
1171 Ok(DecoderState {
1172 inner: ManuallyDrop::new(opened),
1173 backend,
1174 hw_device_ref,
1175 callback_state,
1176 })
1177 }
1178}
1179
1180/// RAII guard for the partially-owned FFmpeg state that
1181/// [`VideoDecoder::build_state`] holds between the
1182/// `av_hwdevice_ctx_create` and `Box::into_raw(CallbackState)`
1183/// allocations and the final `DecoderState` construction.
1184///
1185/// If `build_state` returns `Err` for any reason in that window
1186/// (`av_buffer_ref` ENOMEM, `open_as` failure, codec_type mismatch, or
1187/// any future error path), this guard's `Drop` releases
1188/// `hw_device_ref` — the first ref returned by `av_hwdevice_ctx_create`,
1189/// distinct from the second ref FFmpeg unrefs when the codec context
1190/// drops — and the boxed `CallbackState`, which FFmpeg never touches
1191/// because `AVCodecContext::opaque` is purely user-owned.
1192///
1193/// Successful construction calls [`Self::into_owned`] to disarm the
1194/// guard and hand both pointers to the new `DecoderState`.
1195struct PartialBuildState {
1196 hw_device_ref: *mut AVBufferRef,
1197 callback_state: *mut CallbackState,
1198}
1199
1200impl PartialBuildState {
1201 /// Disarm the guard: return the owned pointers and replace the guard's
1202 /// fields with null so its Drop is a no-op.
1203 fn into_owned(mut self) -> (*mut AVBufferRef, *mut CallbackState) {
1204 let hw = std::mem::replace(&mut self.hw_device_ref, ptr::null_mut());
1205 let cb = std::mem::replace(&mut self.callback_state, ptr::null_mut());
1206 (hw, cb)
1207 }
1208}
1209
1210impl Drop for PartialBuildState {
1211 fn drop(&mut self) {
1212 // SAFETY: pointers are either freshly allocated by `build_state` (via
1213 // `av_hwdevice_ctx_create` and `Box::into_raw`) or null after
1214 // `into_owned`. Both `av_buffer_unref` and `Box::from_raw` need the
1215 // null check we apply here; both are otherwise sound on resources we
1216 // own.
1217 unsafe {
1218 if !self.hw_device_ref.is_null() {
1219 let mut hw = self.hw_device_ref;
1220 av_buffer_unref(&mut hw);
1221 }
1222 if !self.callback_state.is_null() {
1223 drop(Box::from_raw(self.callback_state));
1224 }
1225 }
1226 }
1227}
1228
1229/// Download a HW frame into a CPU [`Frame`]. Always unrefs the destination
1230/// first so reuse across resolution changes is safe.
1231///
1232/// Deliberately does **not** call `av_frame_copy_props`. That FFmpeg
1233/// helper deep-copies AVFrame side data (SEI, mastering display, ICC
1234/// profiles, dynamic HDR, etc.), the metadata dict, and bumps both
1235/// `opaque_ref` and `private_ref` on every receive — none of which
1236/// `Frame` exposes via its public accessors. On a crafted stream with
1237/// megabytes of per-frame metadata that would mean an unbounded
1238/// allocation per receive, with no caller-visible benefit. We instead
1239/// copy only the scalar fields the public API can read (today: `pts`);
1240/// pixel layout (`width`, `height`, `format`, `linesize`, `data`) is
1241/// already set by `av_hwframe_transfer_data`. If `Frame` ever grows
1242/// accessors for timing extras (`duration`, `time_base`, `pkt_dts`) or
1243/// color metadata, add those to `copy_frame_props_minimal` at the same
1244/// time.
1245unsafe fn transfer_hw_frame(
1246 dst: &mut Frame,
1247 src: &mut frame::Video,
1248) -> std::result::Result<(), ffmpeg_next::Error> {
1249 unsafe {
1250 av_frame_unref(dst.as_inner_mut().as_mut_ptr());
1251 let ret = av_hwframe_transfer_data(dst.as_inner_mut().as_mut_ptr(), src.as_ptr(), 0);
1252 if ret < 0 {
1253 return Err(ffmpeg_next::Error::from(ret));
1254 }
1255 // Validate the post-transfer CPU pix_fmt against the safe `Frame`
1256 // accessor's supported set. FFmpeg picks the destination format
1257 // when `dst.format == AV_PIX_FMT_NONE` on entry (which it always is
1258 // here — `av_frame_unref` clears it) by walking the result of
1259 // `av_hwframe_transfer_get_formats`. Driver/version ordering can
1260 // pick a layout outside our NV*/P0xx/P2xx/P4xx set; the call would
1261 // return success while the resulting frame is unreadable through
1262 // `Frame::row` / `Frame::as_ptr` (those return `None` for
1263 // unsupported formats). Surface the unsupported result as a
1264 // transfer failure so `receive_frame`'s probe-active path advances
1265 // to the next backend rather than collapsing on an unusable frame;
1266 // post-probe, the caller gets an `Err` they can branch into a
1267 // software fallback.
1268 let dst_raw_fmt: i32 = (*dst.as_inner_mut().as_ptr()).format;
1269 let dst_pix_fmt = crate::boundary::from_av_pixel_format(dst_raw_fmt);
1270 if !crate::frame::is_supported_cpu_pix_fmt(dst_pix_fmt) {
1271 tracing::warn!(
1272 pix_fmt = dst_raw_fmt,
1273 "hwdecode: hw->cpu transfer produced unsupported pix_fmt; \
1274 treating as backend failure"
1275 );
1276 av_frame_unref(dst.as_inner_mut().as_mut_ptr());
1277 return Err(ffmpeg_next::Error::Other {
1278 errno: libc::EINVAL,
1279 });
1280 }
1281 if let Err(e) = copy_frame_props_minimal(dst.as_inner_mut().as_mut_ptr(), src.as_ptr()) {
1282 // Failed to propagate metadata. Reset the destination so the
1283 // partial frame doesn't leak (its pixel buffers were attached
1284 // by `av_hwframe_transfer_data` above) and surface as a
1285 // backend failure — the probe path will advance to the next
1286 // candidate; post-probe, the caller branches into SW fallback.
1287 av_frame_unref(dst.as_inner_mut().as_mut_ptr());
1288 return Err(e);
1289 }
1290 }
1291 Ok(())
1292}
1293
1294/// Copies AVFrame metadata (timestamps, color metadata, crop rect,
1295/// flags, side data, etc.) from the source HW frame to the destination
1296/// CPU frame so the post-transfer frame surfaces the same metadata a
1297/// SW-decoded frame would.
1298///
1299/// Defers to FFmpeg's `av_frame_copy_props`, which handles the per-
1300/// `side_data[i]` allocation, dict copy, and refcounted buffer
1301/// replacements internally. The cost is bounded by what the source
1302/// frame attaches — typical HDR streams carry 1–3 side-data entries
1303/// (mastering display, content light level, dolby/HDR10+ dynamic
1304/// metadata) totalling a few hundred bytes, so per-frame allocation
1305/// overhead stays negligible relative to the pixel data already
1306/// transferred via `av_hwframe_transfer_data`.
1307///
1308/// # Safety
1309/// Both pointers must be valid `AVFrame` pointers we own. We do not
1310/// form `&AVFrame` — `av_frame_copy_props` operates on raw pointers
1311/// directly.
1312/// Sum the byte sizes of every entry in `(*frame).side_data[]`.
1313/// Used by the probe replay queue's byte-cap accounting so a
1314/// frame's deep-copied side-data is charged against
1315/// `max_probe_pending_bytes` along with its pixel buffers.
1316///
1317/// # Safety
1318/// `frame` must be a live `*const AVFrame`. Reads only `nb_side_data`,
1319/// the `side_data` pointer array, and each `AVFrameSideData.size` —
1320/// no `&AVFrame` reference is formed.
1321unsafe fn sum_side_data_bytes(frame: *const AVFrame) -> usize {
1322 // Clamp `nb_side_data` to the same entry cap the copy path
1323 // enforces. Without the clamp, a decoder-controlled or
1324 // version-skew `nb_side_data` value (the bindgen field is
1325 // `c_int`, signed) could drive this walk arbitrarily long
1326 // before the cap downstream kicks in. Negative values are
1327 // pinned to zero before casting.
1328 let raw = unsafe { (*frame).nb_side_data };
1329 let arr = unsafe { (*frame).side_data };
1330 if raw <= 0 || arr.is_null() {
1331 return 0;
1332 }
1333 let count = (raw as usize).min(HW_COPY_SIDE_DATA_MAX_ENTRIES);
1334 let mut total: usize = 0;
1335 for i in 0..count {
1336 // SAFETY: `arr` points to `nb_side_data` valid `*mut AVFrameSideData`
1337 // entries per FFmpeg's contract; `i < count` is in-bounds.
1338 let entry = unsafe { *arr.add(i) };
1339 if entry.is_null() {
1340 continue;
1341 }
1342 let sz = unsafe { (*entry).size };
1343 total = total.saturating_add(sz);
1344 if total >= HW_COPY_SIDE_DATA_MAX_TOTAL_BYTES {
1345 // Already at or above the byte cap — further entries can't
1346 // change the projected-vs-cap decision the caller makes.
1347 total = HW_COPY_SIDE_DATA_MAX_TOTAL_BYTES;
1348 break;
1349 }
1350 }
1351 total
1352}
1353
1354/// Hard cap on the number of `AVFrameSideData` entries we copy from
1355/// HW source frame to CPU destination frame on the HW transfer
1356/// path. Mirrors `convert::SIDE_DATA_MAX_ENTRIES`; the public
1357/// converter re-enforces the same cap so this is defense in depth.
1358const HW_COPY_SIDE_DATA_MAX_ENTRIES: usize = 64;
1359/// Hard cap on the total side-data byte budget per HW transfer.
1360/// Mirrors `convert::SIDE_DATA_MAX_TOTAL_BYTES`.
1361const HW_COPY_SIDE_DATA_MAX_TOTAL_BYTES: usize = 256 * 1024;
1362
1363/// Maps a raw `AV_FRAME_DATA_*` integer to the matching bindgen
1364/// `AVFrameSideDataType` enum value when (and only when) the integer
1365/// is a known discriminant in the linked FFmpeg's bindgen output.
1366/// Returns `None` for unknown / version-skew / corrupt values —
1367/// the caller drops those entries instead of `transmute`-ing an
1368/// arbitrary integer back into the enum (which would be immediate
1369/// UB if the discriminant isn't in the enum's set).
1370///
1371/// The whitelist covers the entries safe to preserve across HW
1372/// transfer:
1373/// - HDR10 / HDR10+ / Dolby Vision / Vivid / ambient HDR metadata
1374/// - SMPTE / GOP timecodes
1375/// - ICC color profile
1376/// - A53 closed captions
1377/// - Spherical / display matrix orientation
1378/// - Stereo3D layout
1379///
1380/// Other AV_FRAME_DATA_* constants exist (motion vectors, encoder
1381/// params, RPU buffers, …) but are either decoder-internal or
1382/// rarely useful through the public mediadecode API; dropping them
1383/// is the safe default.
1384fn whitelisted_side_data_kind(kind_raw: i32) -> Option<ffmpeg_next::ffi::AVFrameSideDataType> {
1385 use ffmpeg_next::ffi::AVFrameSideDataType;
1386 // Each match arm compares `kind_raw` against the i32 cast of a
1387 // known constant, then returns the constant itself — we never
1388 // construct the enum from arbitrary integer bytes.
1389 let kind = match kind_raw {
1390 x if x == AVFrameSideDataType::AV_FRAME_DATA_PANSCAN as i32 => {
1391 AVFrameSideDataType::AV_FRAME_DATA_PANSCAN
1392 }
1393 x if x == AVFrameSideDataType::AV_FRAME_DATA_A53_CC as i32 => {
1394 AVFrameSideDataType::AV_FRAME_DATA_A53_CC
1395 }
1396 x if x == AVFrameSideDataType::AV_FRAME_DATA_STEREO3D as i32 => {
1397 AVFrameSideDataType::AV_FRAME_DATA_STEREO3D
1398 }
1399 x if x == AVFrameSideDataType::AV_FRAME_DATA_DISPLAYMATRIX as i32 => {
1400 AVFrameSideDataType::AV_FRAME_DATA_DISPLAYMATRIX
1401 }
1402 x if x == AVFrameSideDataType::AV_FRAME_DATA_AFD as i32 => {
1403 AVFrameSideDataType::AV_FRAME_DATA_AFD
1404 }
1405 x if x == AVFrameSideDataType::AV_FRAME_DATA_MASTERING_DISPLAY_METADATA as i32 => {
1406 AVFrameSideDataType::AV_FRAME_DATA_MASTERING_DISPLAY_METADATA
1407 }
1408 x if x == AVFrameSideDataType::AV_FRAME_DATA_GOP_TIMECODE as i32 => {
1409 AVFrameSideDataType::AV_FRAME_DATA_GOP_TIMECODE
1410 }
1411 x if x == AVFrameSideDataType::AV_FRAME_DATA_SPHERICAL as i32 => {
1412 AVFrameSideDataType::AV_FRAME_DATA_SPHERICAL
1413 }
1414 x if x == AVFrameSideDataType::AV_FRAME_DATA_CONTENT_LIGHT_LEVEL as i32 => {
1415 AVFrameSideDataType::AV_FRAME_DATA_CONTENT_LIGHT_LEVEL
1416 }
1417 x if x == AVFrameSideDataType::AV_FRAME_DATA_ICC_PROFILE as i32 => {
1418 AVFrameSideDataType::AV_FRAME_DATA_ICC_PROFILE
1419 }
1420 x if x == AVFrameSideDataType::AV_FRAME_DATA_S12M_TIMECODE as i32 => {
1421 AVFrameSideDataType::AV_FRAME_DATA_S12M_TIMECODE
1422 }
1423 x if x == AVFrameSideDataType::AV_FRAME_DATA_DYNAMIC_HDR_PLUS as i32 => {
1424 AVFrameSideDataType::AV_FRAME_DATA_DYNAMIC_HDR_PLUS
1425 }
1426 x if x == AVFrameSideDataType::AV_FRAME_DATA_REGIONS_OF_INTEREST as i32 => {
1427 AVFrameSideDataType::AV_FRAME_DATA_REGIONS_OF_INTEREST
1428 }
1429 x if x == AVFrameSideDataType::AV_FRAME_DATA_SEI_UNREGISTERED as i32 => {
1430 AVFrameSideDataType::AV_FRAME_DATA_SEI_UNREGISTERED
1431 }
1432 x if x == AVFrameSideDataType::AV_FRAME_DATA_FILM_GRAIN_PARAMS as i32 => {
1433 AVFrameSideDataType::AV_FRAME_DATA_FILM_GRAIN_PARAMS
1434 }
1435 x if x == AVFrameSideDataType::AV_FRAME_DATA_DOVI_RPU_BUFFER as i32 => {
1436 AVFrameSideDataType::AV_FRAME_DATA_DOVI_RPU_BUFFER
1437 }
1438 x if x == AVFrameSideDataType::AV_FRAME_DATA_DOVI_METADATA as i32 => {
1439 AVFrameSideDataType::AV_FRAME_DATA_DOVI_METADATA
1440 }
1441 x if x == AVFrameSideDataType::AV_FRAME_DATA_DYNAMIC_HDR_VIVID as i32 => {
1442 AVFrameSideDataType::AV_FRAME_DATA_DYNAMIC_HDR_VIVID
1443 }
1444 x if x == AVFrameSideDataType::AV_FRAME_DATA_AMBIENT_VIEWING_ENVIRONMENT as i32 => {
1445 AVFrameSideDataType::AV_FRAME_DATA_AMBIENT_VIEWING_ENVIRONMENT
1446 }
1447 _ => return None,
1448 };
1449 Some(kind)
1450}
1451
1452unsafe fn copy_frame_props_minimal(
1453 dst: *mut AVFrame,
1454 src: *const AVFrame,
1455) -> std::result::Result<(), ffmpeg_next::Error> {
1456 // We deliberately do NOT use `av_frame_copy_props` here, despite
1457 // its convenience. Upstream `av_frame_copy_props` deep-copies
1458 // *every* `AVFrameSideData` entry, the metadata `AVDictionary`,
1459 // and refcounted `opaque_ref` / `private_ref` buffers — all from
1460 // attacker-controlled decoder output. A crafted stream with many
1461 // multi-MiB side-data entries could drive the per-frame
1462 // allocation cost arbitrarily high (one alloc per entry, with the
1463 // entry's bytes copied via `memcpy`). The downstream
1464 // `convert::collect_side_data` cap helps the *Rust* side but the
1465 // FFmpeg-side allocations have already happened.
1466 //
1467 // Instead we copy scalar fields manually (timestamps, color
1468 // metadata, picture type, flags) and copy side-data with a hard
1469 // cap matching the converter's. Metadata dict and opaque_ref /
1470 // private_ref are intentionally NOT copied — they're rarely
1471 // populated on decoded frames and represent unbounded surfaces.
1472 use core::ptr::{addr_of, addr_of_mut, read_unaligned, write_unaligned};
1473 use ffmpeg_next::ffi::av_frame_new_side_data;
1474 unsafe {
1475 // Scalar timestamps / flags / color / SAR / crop. None of
1476 // these allocate.
1477 (*dst).pts = (*src).pts;
1478 (*dst).pkt_dts = (*src).pkt_dts;
1479 (*dst).duration = (*src).duration;
1480 (*dst).best_effort_timestamp = (*src).best_effort_timestamp;
1481 (*dst).quality = (*src).quality;
1482 (*dst).repeat_pict = (*src).repeat_pict;
1483 (*dst).flags = (*src).flags;
1484 (*dst).sample_aspect_ratio = (*src).sample_aspect_ratio;
1485 (*dst).crop_left = (*src).crop_left;
1486 (*dst).crop_top = (*src).crop_top;
1487 (*dst).crop_right = (*src).crop_right;
1488 (*dst).crop_bottom = (*src).crop_bottom;
1489 (*dst).time_base = (*src).time_base;
1490
1491 // Enum-typed fields: bit-copy raw to avoid materializing an
1492 // invalid `AVColorPrimaries` etc. on either side. `read_unaligned`
1493 // / `write_unaligned` on `i32` projections sidestep the bindgen
1494 // enum's discriminant-validity invariant.
1495 let pict_type_raw = read_unaligned(addr_of!((*src).pict_type) as *const i32);
1496 write_unaligned(addr_of_mut!((*dst).pict_type) as *mut i32, pict_type_raw);
1497 let cp_raw = read_unaligned(addr_of!((*src).color_primaries) as *const i32);
1498 write_unaligned(addr_of_mut!((*dst).color_primaries) as *mut i32, cp_raw);
1499 let trc_raw = read_unaligned(addr_of!((*src).color_trc) as *const i32);
1500 write_unaligned(addr_of_mut!((*dst).color_trc) as *mut i32, trc_raw);
1501 let cs_raw = read_unaligned(addr_of!((*src).colorspace) as *const i32);
1502 write_unaligned(addr_of_mut!((*dst).colorspace) as *mut i32, cs_raw);
1503 let cr_raw = read_unaligned(addr_of!((*src).color_range) as *const i32);
1504 write_unaligned(addr_of_mut!((*dst).color_range) as *mut i32, cr_raw);
1505 let cl_raw = read_unaligned(addr_of!((*src).chroma_location) as *const i32);
1506 write_unaligned(addr_of_mut!((*dst).chroma_location) as *mut i32, cl_raw);
1507
1508 // Side-data: bounded copy. `av_frame_new_side_data(dst, type,
1509 // size)` allocates the entry and returns a pointer to write
1510 // the payload bytes into; a null return is the OOM signal.
1511 // Callers (`transfer_hw_frame`, `drain_into_pending`) hand us
1512 // freshly-unref'd `dst` frames, so any prior side-data has
1513 // already been freed by `av_frame_unref` — we don't need to
1514 // strip dst's existing side-data here.
1515 // Read `nb_side_data` as the bindgen `c_int` and clamp non-
1516 // positive values BEFORE casting to `usize`. A negative value
1517 // (corrupt / version-skew decoder output) cast directly to
1518 // `usize` becomes a huge positive count and would walk OOB
1519 // memory below; pinning to zero up front collapses that to a
1520 // no-op. Same signed-count guard `sum_side_data_bytes` applies.
1521 let nb_side_data_raw = (*src).nb_side_data;
1522 let src_arr = (*src).side_data;
1523 if nb_side_data_raw > 0 && !src_arr.is_null() {
1524 let count_raw = nb_side_data_raw as usize;
1525 let count = count_raw.min(HW_COPY_SIDE_DATA_MAX_ENTRIES);
1526 if count_raw > HW_COPY_SIDE_DATA_MAX_ENTRIES {
1527 tracing::warn!(
1528 cap = HW_COPY_SIDE_DATA_MAX_ENTRIES,
1529 requested = count_raw,
1530 "mediadecode-ffmpeg: HW->CPU transfer side-data entry cap reached; truncating",
1531 );
1532 }
1533 let mut total_bytes: usize = 0;
1534 for i in 0..count {
1535 let entry = *src_arr.add(i);
1536 if entry.is_null() {
1537 continue;
1538 }
1539 let kind_raw = read_unaligned(addr_of!((*entry).type_) as *const i32);
1540 let size = (*entry).size;
1541 let data_ptr = (*entry).data;
1542 if size == 0 || data_ptr.is_null() {
1543 continue;
1544 }
1545 // Whitelist gate: only proceed when `kind_raw` matches a
1546 // known `AV_FRAME_DATA_*` constant the linked FFmpeg's
1547 // bindgen output knows about. Without this gate, a
1548 // version-skew or hostile decoder could write a side-data
1549 // type integer outside our bindgen's discriminant set, and
1550 // constructing the `AVFrameSideDataType` enum value (so
1551 // we could pass it to `av_frame_new_side_data`) would be
1552 // immediate UB before the call. Unknown types are dropped
1553 // with a debug-level log — the public converter's
1554 // `collect_side_data` walks the destination raw and would
1555 // also surface them as bare integers in `SideDataEntry.kind`.
1556 let Some(kind_enum) = whitelisted_side_data_kind(kind_raw) else {
1557 tracing::debug!(
1558 kind_raw,
1559 "mediadecode-ffmpeg: unknown AV_FRAME_DATA type during HW->CPU transfer; dropping",
1560 );
1561 continue;
1562 };
1563 let projected = total_bytes.saturating_add(size);
1564 if projected > HW_COPY_SIDE_DATA_MAX_TOTAL_BYTES {
1565 tracing::warn!(
1566 cap = HW_COPY_SIDE_DATA_MAX_TOTAL_BYTES,
1567 projected,
1568 "mediadecode-ffmpeg: HW->CPU transfer side-data byte cap reached; dropping rest",
1569 );
1570 break;
1571 }
1572 let new_entry = av_frame_new_side_data(dst, kind_enum, size);
1573 if new_entry.is_null() {
1574 // OOM mid-loop: stop copying further entries but don't
1575 // fail the whole transfer — the frames we did copy stay
1576 // attached. The convert path's cap is the final guard.
1577 tracing::warn!("mediadecode-ffmpeg: av_frame_new_side_data OOM during HW->CPU transfer",);
1578 break;
1579 }
1580 // SAFETY: `(*new_entry).data` is allocated for `size` bytes
1581 // per av_frame_new_side_data's contract; `data_ptr` is
1582 // valid for `size` reads per AVFrameSideData's contract.
1583 core::ptr::copy_nonoverlapping(data_ptr, (*new_entry).data, size);
1584 total_bytes = projected;
1585 }
1586 }
1587 }
1588 Ok(())
1589}
1590
1591/// `EAGAIN` and `EOF` are normal flow signals from `avcodec_receive_frame`
1592/// and must not be treated as backend failures.
1593fn is_transient(e: &ffmpeg_next::Error) -> bool {
1594 is_eagain(e) || matches!(e, ffmpeg_next::Error::Eof)
1595}
1596
1597/// Post-commit, a HW-only decoder's non-transient, non-EOF error means the
1598/// committed HW backend can't decode this content → fall back to SW. VT's
1599/// "hardware accelerator failed" surfaces as AVERROR_EXTERNAL; some HW
1600/// backends report unsupported geometry as InvalidData; context loss as
1601/// Bug/Bug2/Unknown. Broad-by-design (decode-all-kinds); fixtures will let us
1602/// narrow if a real backend proves a code should NOT trigger fallback.
1603///
1604/// `EAGAIN`/`EOF` are deliberately excluded by the caller (each call site
1605/// guards on `is_transient` first): `EAGAIN` is backpressure and `EOF` is a
1606/// genuine end-of-stream that must propagate, never be trapped in an infinite
1607/// fallback-retry loop. `Other { errno: EINVAL }` from the HW→CPU transfer
1608/// path is also covered — an unsupported CPU output pix_fmt is a HW-output
1609/// problem, never input corruption.
1610fn is_hw_decode_failure(e: &ffmpeg_next::Error) -> bool {
1611 matches!(
1612 e,
1613 ffmpeg_next::Error::External
1614 | ffmpeg_next::Error::Bug
1615 | ffmpeg_next::Error::Bug2
1616 | ffmpeg_next::Error::Unknown
1617 | ffmpeg_next::Error::InvalidData
1618 | ffmpeg_next::Error::Other {
1619 errno: libc::EINVAL
1620 }
1621 )
1622}
1623
1624/// Reject a `codec::Parameters` whose inner `*mut AVCodecParameters` is
1625/// null. This guards the public trust boundary: ffmpeg-next can produce
1626/// such a `Parameters` under OOM (`Parameters::new()` does not check
1627/// `avcodec_parameters_alloc`), and a safe caller can legally hand one
1628/// in. Without this check, the very next `(*p.as_ptr()).field` read
1629/// would be a null deref.
1630fn ensure_parameters_non_null(parameters: &codec::Parameters) -> Result<()> {
1631 // SAFETY: as_ptr() returns the inner *const AVCodecParameters; we just
1632 // inspect the pointer value (no deref).
1633 if unsafe { parameters.as_ptr() }.is_null() {
1634 return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
1635 errno: libc::ENOMEM,
1636 }));
1637 }
1638 Ok(())
1639}
1640
1641/// Allocate a fresh `frame::Video`, checking that `av_frame_alloc` did not
1642/// return NULL. ffmpeg-next's `frame::Video::empty()` does not surface that
1643/// failure and the resulting null pointer would be UB on the next field
1644/// access; this wrapper catches it and surfaces it as `ENOMEM`.
1645fn alloc_av_frame() -> std::result::Result<frame::Video, ffmpeg_next::Error> {
1646 let inner = frame::Video::empty();
1647 // SAFETY: as_ptr() just exposes the inner pointer for inspection.
1648 if unsafe { inner.as_ptr() }.is_null() {
1649 return Err(ffmpeg_next::Error::Other {
1650 errno: libc::ENOMEM,
1651 });
1652 }
1653 Ok(inner)
1654}
1655
1656/// Build a fresh `Context` from `parameters`, checking the underlying
1657/// `avcodec_alloc_context3` for NULL before passing it to
1658/// `avcodec_parameters_to_context`. ffmpeg-next's `Context::from_parameters`
1659/// skips that check and would feed a null pointer into FFmpeg under OOM —
1660/// undefined behavior. This helper surfaces the failure as `ENOMEM` and
1661/// frees the context if `parameters_to_context` itself errors.
1662pub(crate) fn build_codec_context(parameters: &codec::Parameters) -> Result<Context> {
1663 ensure_parameters_non_null(parameters)?;
1664 // SAFETY: avcodec_alloc_context3(NULL) returns a fresh AVCodecContext
1665 // or NULL on allocation failure.
1666 let ctx_ptr = unsafe { avcodec_alloc_context3(ptr::null()) };
1667 if ctx_ptr.is_null() {
1668 return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
1669 errno: libc::ENOMEM,
1670 }));
1671 }
1672 // SAFETY: ctx_ptr is non-null and freshly allocated; parameters.as_ptr()
1673 // returns a valid AVCodecParameters pointer; the function copies bytes
1674 // out of parameters into the context.
1675 let ret = unsafe { avcodec_parameters_to_context(ctx_ptr, parameters.as_ptr()) };
1676 if ret < 0 {
1677 // SAFETY: ctx_ptr was allocated by us and never handed to anyone else.
1678 let mut p = ctx_ptr;
1679 unsafe { avcodec_free_context(&mut p) };
1680 return Err(Error::Ffmpeg(ffmpeg_next::Error::from(ret)));
1681 }
1682 // SAFETY: ctx_ptr is valid; passing `owner: None` means our wrapper owns
1683 // the allocation and `Context::drop` will run `avcodec_free_context`.
1684 Ok(unsafe { Context::wrap(ctx_ptr, None) })
1685}
1686
1687/// Checked deep-clone of `codec::Parameters`. ffmpeg-next's
1688/// `Parameters::clone` allocates via `avcodec_parameters_alloc` without
1689/// checking for NULL and runs `avcodec_parameters_copy` without checking
1690/// the return code. On `ENOMEM` the result is a `Parameters` with a null
1691/// inner pointer, which becomes UB when later passed to FFmpeg.
1692///
1693/// This helper performs both calls explicitly, frees a partial allocation
1694/// on failure, and surfaces the AVERROR. The returned `Parameters` has
1695/// `owner: None`, severing any Rc link to the caller's demuxer (the
1696/// reason we deep-clone in the first place — see Send safety in
1697/// `VideoDecoder::open`).
1698pub(crate) fn try_clone_parameters(
1699 src: &codec::Parameters,
1700) -> std::result::Result<codec::Parameters, ffmpeg_next::Error> {
1701 // Reject a null inner pointer at the boundary; a deref inside
1702 // avcodec_parameters_copy below would otherwise be UB.
1703 if unsafe { src.as_ptr() }.is_null() {
1704 return Err(ffmpeg_next::Error::Other {
1705 errno: libc::ENOMEM,
1706 });
1707 }
1708 // SAFETY: avcodec_parameters_alloc returns a fresh AVCodecParameters
1709 // pointer or NULL on allocation failure.
1710 let dst_ptr = unsafe { avcodec_parameters_alloc() };
1711 if dst_ptr.is_null() {
1712 return Err(ffmpeg_next::Error::Other {
1713 errno: libc::ENOMEM,
1714 });
1715 }
1716 // SAFETY: dst_ptr is non-null and freshly allocated; src.as_ptr() is
1717 // a valid AVCodecParameters pointer; the function copies bytes from
1718 // src into dst.
1719 let ret = unsafe { avcodec_parameters_copy(dst_ptr, src.as_ptr()) };
1720 if ret < 0 {
1721 // SAFETY: dst_ptr was allocated by us and never handed out.
1722 let mut p = dst_ptr;
1723 unsafe { avcodec_parameters_free(&mut p) };
1724 return Err(ffmpeg_next::Error::from(ret));
1725 }
1726 // SAFETY: dst_ptr is a valid AVCodecParameters; passing `owner: None`
1727 // means our wrapper owns the allocation and `Parameters::drop` will
1728 // call `avcodec_parameters_free`.
1729 Ok(unsafe { codec::Parameters::wrap(dst_ptr, None) })
1730}
1731
1732/// Checked counterpart to `Packet::clone()`. ffmpeg-next's `clone_from`
1733/// calls `av_packet_ref` and ignores the int return value; on `ENOMEM`
1734/// the destination is left empty while the caller assumes the clone
1735/// succeeded — corrupting any later replay history. This helper surfaces
1736/// the AVERROR. The result is a refcounted shallow clone — the payload
1737/// buffer is shared with `src` rather than deep-copied; the probe replay
1738/// only sends packets through `avcodec_send_packet`, which does not
1739/// require a writable buffer.
1740pub(crate) fn try_clone_packet(src: &Packet) -> std::result::Result<Packet, ffmpeg_next::Error> {
1741 let mut dst = Packet::empty();
1742 // SAFETY: dst is a freshly zero-initialized Packet (av_init_packet inside
1743 // Packet::empty); av_packet_ref initializes its data fields from src's
1744 // refcounted buffer or returns AVERROR(ENOMEM) on failure.
1745 let ret = unsafe { av_packet_ref(dst.as_mut_ptr(), src.as_ptr()) };
1746 if ret < 0 {
1747 return Err(ffmpeg_next::Error::from(ret));
1748 }
1749 Ok(dst)
1750}
1751
1752/// Sum of `AVPacket.side_data[i].size` across every entry, plus
1753/// `nb_entries * SIDE_DATA_ENTRY_OVERHEAD` (descriptor + AVBufferRef +
1754/// allocator bookkeeping per entry). `av_packet_ref` performs a deep
1755/// copy of side data via `av_packet_copy_props`, so each probe-buffered
1756/// clone retains every one of these bytes. Charging both keeps
1757/// `MAX_PROBE_PACKET_BYTES` a true upper bound — without the overhead,
1758/// many zero-size entries slip past the cap on pure descriptor cost.
1759///
1760/// Walks at most `max_entries` entries even when `side_data_elems`
1761/// reports a larger count. Defense-in-depth against a corrupt or hostile
1762/// packet whose `side_data_elems` lies about the actual array length:
1763/// the caller is expected to also reject any packet whose count exceeds
1764/// the cap (so the inflated clone is never created), but bounding the
1765/// walk here means a stale or weaponised value can never trigger an
1766/// unbounded raw-pointer scan from the safe API.
1767///
1768/// Reads only the `size` field of each `AVPacketSideData` entry — never
1769/// touches the bindgen `AVPacketSideDataType` enum, so no UB even if a
1770/// future FFmpeg adds a side-data type discriminant our build doesn't
1771/// know.
1772pub(crate) fn packet_side_data_bytes(packet: &Packet, max_entries: usize) -> usize {
1773 // SAFETY: AVPacket.side_data is `*mut AVPacketSideData` and
1774 // side_data_elems is `c_int`; both are raw struct fields safe to read.
1775 // Field projection (`.size`) does not reconstruct the enum-typed `type_`
1776 // field, so the bindgen-enum UB hazard does not apply here.
1777 unsafe {
1778 let raw = packet.as_ptr();
1779 let nel = (*raw).side_data_elems;
1780 let arr = (*raw).side_data;
1781 if arr.is_null() || nel <= 0 || max_entries == 0 {
1782 return 0;
1783 }
1784 let count = (nel as usize).min(max_entries);
1785 let mut total = count.saturating_mul(SIDE_DATA_ENTRY_OVERHEAD);
1786 for i in 0..count {
1787 let entry = arr.add(i);
1788 total = total.saturating_add((*entry).size);
1789 }
1790 total
1791 }
1792}
1793
1794/// Number of `AVPacketSideData` entries on `packet`. The probe buffer
1795/// uses this to enforce [`MAX_PROBE_PACKET_SIDE_DATA_ENTRIES`] before
1796/// cloning, so a packet whose entry count alone would dominate retained
1797/// memory is rejected up front.
1798pub(crate) fn packet_side_data_count(packet: &Packet) -> usize {
1799 // SAFETY: side_data_elems is `c_int`, safe to read; clamp negatives to 0.
1800 let nel = unsafe { (*packet.as_ptr()).side_data_elems };
1801 if nel <= 0 { 0 } else { nel as usize }
1802}
1803
1804/// Just `EAGAIN` (separate from EOF — the FFmpeg send/receive state machine
1805/// distinguishes "drain output and retry" from "stream over").
1806fn is_eagain(e: &ffmpeg_next::Error) -> bool {
1807 matches!(e, ffmpeg_next::Error::Other { errno } if *errno == ffmpeg_next::error::EAGAIN)
1808}
1809
1810/// Look up the decoder for `parameters` without going through the bindgen
1811/// `AVCodecID` Rust enum. Reads the codec_id field as raw `u32` via
1812/// `addr_of!` + `ptr::read` so a value not in our build's discriminant
1813/// set never invokes UB.
1814fn find_decoder(parameters: &codec::Parameters) -> Result<Codec> {
1815 ensure_parameters_non_null(parameters)?;
1816 // SAFETY: parameters' inner pointer is non-null (checked above);
1817 // addr_of! projects to the codec_id field; the *const u32 cast is sound
1818 // because AVCodecID is `#[repr(u32)]` (same size and alignment as u32).
1819 // Reading as u32 cannot be UB regardless of the value FFmpeg wrote.
1820 let raw_id: u32 =
1821 unsafe { ptr::read(ptr::addr_of!((*parameters.as_ptr()).codec_id) as *const u32) };
1822
1823 // Call C `avcodec_find_decoder` via our local `c_int`-typed shim — we
1824 // never construct an `AVCodecID` enum from `raw_id`. The C function
1825 // returns NULL for unknown ids, which we surface as `Error::NoCodec`.
1826 // SAFETY: avcodec_find_decoder is a pure FFmpeg lookup; passing any
1827 // c_int is sound (returns NULL for unknown).
1828 let codec_ptr = unsafe { c_shims::avcodec_find_decoder(raw_id as libc::c_int) };
1829 if codec_ptr.is_null() {
1830 return Err(Error::NoCodec(raw_id));
1831 }
1832 // SAFETY: codec_ptr is a non-null *const AVCodec into FFmpeg's static
1833 // codec table; it lives for the duration of the program.
1834 Ok(unsafe { Codec::wrap(codec_ptr) })
1835}
1836
1837/// Drain output frames from a candidate decoder during probe replay,
1838/// transferring each one from the candidate's HW context to a fresh CPU
1839/// frame and queueing it. Returns `Ok(())` once the candidate signals
1840/// EAGAIN/EOF. The transfer happens while the candidate is still alive
1841/// (its `AVHWFramesContext` is reachable); the resulting CPU frames remain
1842/// valid after the candidate is committed because they hold their own
1843/// buffer references with no dependency on the original device context.
1844fn drain_into_pending(
1845 decoder: &mut ffmpeg_next::decoder::Video,
1846 hw_buf: &mut frame::Video,
1847 pending: &mut VecDeque<frame::Video>,
1848 pending_bytes: &mut usize,
1849 max_bytes: usize,
1850) -> std::result::Result<(), ffmpeg_next::Error> {
1851 loop {
1852 match decoder.receive_frame(hw_buf) {
1853 Ok(()) => {
1854 // Pre-transfer cap check: if we are already at or over either cap,
1855 // the candidate is producing more than we can hold. Treat as an
1856 // explicit candidate failure so `advance_probe` can try the next
1857 // backend instead of committing a stream with silently-dropped
1858 // frames in the middle.
1859 //
1860 // TODO: at very large frame sizes (8K HDR P010, > ~96 MiB each)
1861 // even a single retained frame is significant. Future direction:
1862 // memmap-backed pending frames (write to a temp file or shared
1863 // memory segment) so the resident set stays bounded even when the
1864 // byte cap is raised. Out of scope for now.
1865 if pending.len() >= MAX_PROBE_PENDING_FRAMES || *pending_bytes >= max_bytes {
1866 tracing::warn!(
1867 frames = pending.len(),
1868 bytes = *pending_bytes,
1869 max_frames = MAX_PROBE_PENDING_FRAMES,
1870 max_bytes = max_bytes,
1871 "hwdecode: probe pending cap reached; failing candidate replay"
1872 );
1873 // SAFETY: hw_buf is owned and valid; unref of an empty frame is a no-op.
1874 unsafe { av_frame_unref(hw_buf.as_mut_ptr()) };
1875 return Err(ffmpeg_next::Error::Other {
1876 errno: libc::ENOMEM,
1877 });
1878 }
1879 // Pre-transfer size guard: `av_hwframe_transfer_data` will
1880 // allocate the CPU buffer based on `hw_buf`'s dimensions. If a
1881 // single frame's worst-case footprint already pushes past the
1882 // cap, refuse the candidate **before** allocating so RSS does
1883 // not spike on a frame we'd immediately drop. Uses a width *
1884 // height * `WORST_CASE_BYTES_PER_PIXEL` upper bound; the
1885 // post-transfer accounting via `cpu_frame_bytes` below stays in
1886 // place as a backstop using the actual stride/format.
1887 let estimated_bytes = match estimate_transfer_bytes(hw_buf) {
1888 Some(b) => b,
1889 None => {
1890 // SAFETY: AVFrame.width/height are c_int reads.
1891 let (w, h) = unsafe {
1892 let raw = hw_buf.as_ptr();
1893 ((*raw).width, (*raw).height)
1894 };
1895 tracing::warn!(
1896 width = w,
1897 height = h,
1898 "hwdecode: HW frame dimensions invalid for sizing; failing candidate replay"
1899 );
1900 unsafe { av_frame_unref(hw_buf.as_mut_ptr()) };
1901 return Err(ffmpeg_next::Error::Other {
1902 errno: libc::ENOMEM,
1903 });
1904 }
1905 };
1906 let estimated_total = pending_bytes.saturating_add(estimated_bytes);
1907 if estimated_total > max_bytes {
1908 // SAFETY: AVFrame.width/height are c_int reads.
1909 let (w, h) = unsafe {
1910 let raw = hw_buf.as_ptr();
1911 ((*raw).width, (*raw).height)
1912 };
1913 tracing::warn!(
1914 pending_bytes = *pending_bytes,
1915 estimated_bytes,
1916 width = w,
1917 height = h,
1918 max_bytes = max_bytes,
1919 "hwdecode: pre-transfer size estimate exceeds cap; \
1920 refusing candidate replay before allocating CPU frame"
1921 );
1922 unsafe { av_frame_unref(hw_buf.as_mut_ptr()) };
1923 return Err(ffmpeg_next::Error::Other {
1924 errno: libc::ENOMEM,
1925 });
1926 }
1927 let mut cpu = alloc_av_frame()?;
1928 // SAFETY: hw_buf is a freshly-decoded HW frame;
1929 // `av_hwframe_transfer_data` allocates pixel buffers on `cpu`.
1930 // We use `copy_frame_props_minimal` (only `pts`) instead of
1931 // `av_frame_copy_props` for the same reason as
1932 // `transfer_hw_frame`: the public `Frame` API does not expose
1933 // side data / metadata / opaque refs, so deep-copying them per
1934 // frame is pure cost and an unbounded allocation source on
1935 // attacker-controlled streams.
1936 unsafe {
1937 let r1 = av_hwframe_transfer_data(cpu.as_mut_ptr(), hw_buf.as_ptr(), 0);
1938 if r1 < 0 {
1939 return Err(ffmpeg_next::Error::from(r1));
1940 }
1941 }
1942 // Same post-transfer pix_fmt validation as `transfer_hw_frame`.
1943 // A driver that picks a CPU format outside our supported set
1944 // would queue an unusable frame here; later, when
1945 // `try_pop_pending` hands it to the caller, `Frame::row` /
1946 // `Frame::as_ptr` would return `None`. Refuse the candidate
1947 // before the queue grows so probing advances to the next
1948 // backend instead.
1949 let cpu_raw_fmt: i32 = unsafe { (*cpu.as_ptr()).format };
1950 let cpu_pix_fmt = crate::boundary::from_av_pixel_format(cpu_raw_fmt);
1951 if !crate::frame::is_supported_cpu_pix_fmt(cpu_pix_fmt) {
1952 tracing::warn!(
1953 pix_fmt = cpu_raw_fmt,
1954 "hwdecode: candidate produced unsupported CPU pix_fmt during \
1955 probe replay; failing candidate"
1956 );
1957 return Err(ffmpeg_next::Error::Other {
1958 errno: libc::EINVAL,
1959 });
1960 }
1961 let pixel_bytes = match cpu_frame_bytes(&cpu) {
1962 Some(b) => b,
1963 None => {
1964 // Unknown pix_fmt or vertically-flipped layout — we cannot
1965 // bound this frame's contribution against the byte cap, so up
1966 // to MAX_PROBE_PENDING_FRAMES of them could exhaust memory.
1967 // Fail the candidate so probing tries the next backend
1968 // rather than queueing untracked allocations.
1969 // SAFETY: AVFrame.format is c_int, safe to read.
1970 let pix_fmt: i32 = unsafe { (*cpu.as_ptr()).format };
1971 tracing::warn!(
1972 pix_fmt,
1973 "hwdecode: cannot size unknown CPU pix_fmt during replay; failing candidate"
1974 );
1975 // cpu drops here.
1976 return Err(ffmpeg_next::Error::Other {
1977 errno: libc::ENOMEM,
1978 });
1979 }
1980 };
1981 // Account for side-data bytes that `av_frame_copy_props`
1982 // will deep-copy from the source HW frame. HDR streams
1983 // typically carry mastering display + content light level
1984 // (~50 bytes) and dynamic HDR metadata (~few hundred bytes);
1985 // pathological side-data could otherwise quietly bypass the
1986 // pixel-data byte cap.
1987 // SAFETY: hw_buf is a valid AVFrame; we read scalar fields
1988 // and pointer arrays without forming a `&AVFrame`.
1989 let side_data_bytes = unsafe { sum_side_data_bytes(hw_buf.as_ptr()) };
1990 let new_total = pending_bytes
1991 .saturating_add(pixel_bytes)
1992 .saturating_add(side_data_bytes);
1993 if new_total > max_bytes {
1994 tracing::warn!(
1995 pending_bytes = *pending_bytes,
1996 pixel_bytes,
1997 side_data_bytes,
1998 max_bytes,
1999 "hwdecode: queueing this frame would exceed byte cap; \
2000 failing candidate replay"
2001 );
2002 // cpu drops here without ever paying a metadata deep copy.
2003 return Err(ffmpeg_next::Error::Other {
2004 errno: libc::ENOMEM,
2005 });
2006 }
2007 // Cap check passed — copy AVFrame metadata. SAFETY: cpu and
2008 // hw_buf are both valid AVFrames we own. On failure (OOM
2009 // during side-data alloc) we propagate so the probe candidate
2010 // is treated as failed rather than queueing a frame whose
2011 // metadata silently disappeared.
2012 unsafe { copy_frame_props_minimal(cpu.as_mut_ptr(), hw_buf.as_ptr()) }?;
2013 *pending_bytes = new_total;
2014 pending.push_back(cpu);
2015 }
2016 Err(e) if is_transient(&e) => return Ok(()),
2017 Err(e) => return Err(e),
2018 }
2019 }
2020}
2021
2022/// Allocated frame dimensions according to `hw_buf.hw_frames_ctx`.
2023///
2024/// Per FFmpeg's `libavutil/hwcontext.c::transfer_data_alloc`, the CPU
2025/// destination of `av_hwframe_transfer_data` is allocated using
2026/// `AVHWFramesContext.width / .height` (the *allocated* surface size of
2027/// the HW pool); only afterwards is `dst->width / dst->height` reset to
2028/// `src->width / src->height` (the *display* size). For cropped or
2029/// heavily aligned streams the allocated dims can be much larger than
2030/// the display dims (e.g. coded 8192×8192 surface with a 100×100
2031/// display crop), so any byte-cap accounting that uses display dims
2032/// undercounts by `allocated_height / display_height` and lets the
2033/// real allocation slip past the cap.
2034///
2035/// Returns `None` when no `hw_frames_ctx` is attached or the dimensions
2036/// are non-positive — the caller treats `None` as "cannot prove
2037/// allocation extent, fail the candidate."
2038fn hw_frames_ctx_dimensions(frame: &frame::Video) -> Option<(i32, i32)> {
2039 // SAFETY: AVFrame.hw_frames_ctx is `*mut AVBufferRef`. When non-null,
2040 // its `data` field points to an `AVHWFramesContext`. We read `.width`
2041 // and `.height` (both `c_int`) via field projection — neither field is
2042 // enum-typed, so no bindgen-enum UB hazard.
2043 unsafe {
2044 let raw = frame.as_ptr();
2045 let hw_ctx_ref = (*raw).hw_frames_ctx;
2046 if hw_ctx_ref.is_null() {
2047 return None;
2048 }
2049 let data = (*hw_ctx_ref).data;
2050 if data.is_null() {
2051 return None;
2052 }
2053 let frames_ctx = data as *const AVHWFramesContext;
2054 let w: i32 = ptr::read(ptr::addr_of!((*frames_ctx).width));
2055 let h: i32 = ptr::read(ptr::addr_of!((*frames_ctx).height));
2056 if w <= 0 || h <= 0 {
2057 return None;
2058 }
2059 Some((w, h))
2060 }
2061}
2062
2063/// Conservative upper-bound estimate of the bytes
2064/// `av_hwframe_transfer_data` will allocate when downloading `hw_buf` to
2065/// a CPU frame. Used by [`drain_into_pending`] as a pre-transfer guard
2066/// so a candidate replay can refuse a frame whose footprint would
2067/// exceed the byte budget *without* first paying the allocation.
2068///
2069/// Sizes from `hw_buf.hw_frames_ctx` (the allocated dims used by the
2070/// FFmpeg transfer path) rather than `AVFrame.width / .height` (display
2071/// dims). On a cropped stream the two can differ by orders of magnitude
2072/// and using display dims would let the real allocation slip past the
2073/// cap.
2074///
2075/// Returns `None` when `hw_frames_ctx` is missing or its width/height
2076/// are non-positive — caller treats as candidate failure since we
2077/// cannot prove the allocation extent. (A SW source frame on the probe
2078/// replay path is not expected; we don't fall back to display dims
2079/// because that's the exact attack the cap is meant to prevent.)
2080fn estimate_transfer_bytes(hw_buf: &frame::Video) -> Option<usize> {
2081 let (w, h) = hw_frames_ctx_dimensions(hw_buf)?;
2082 Some(
2083 (w as usize)
2084 .saturating_mul(h as usize)
2085 .saturating_mul(WORST_CASE_BYTES_PER_PIXEL),
2086 )
2087}
2088
2089/// Exact resident size of a CPU frame: sum of `AVFrame.buf[i].size`
2090/// across every populated buffer.
2091///
2092/// `AVBufferRef.size` is documented as "Size of data in bytes" — the
2093/// real allocated extent FFmpeg used. Reading it directly handles the
2094/// cropped/aligned case where `AVFrame.height` (display) is smaller
2095/// than the underlying allocation height (the `AVHWFramesContext`
2096/// surface size FFmpeg sized the buffer for); a `linesize *
2097/// plane_height_for(display_height)` formula would undercount in that
2098/// case.
2099///
2100/// Returns `None` only when `linesize[0]` is negative — FFmpeg's
2101/// vertically-flipped layout. The crate's safe row accessors
2102/// ([`crate::Frame::row`] / [`crate::Frame::rows`]) already reject
2103/// negative-stride frames, so queueing one during probe replay would
2104/// just delay the failure to the consumer; refusing here lets the
2105/// probe loop advance to the next backend instead.
2106fn cpu_frame_bytes(frame: &frame::Video) -> Option<usize> {
2107 // SAFETY: AVFrame.linesize is `[c_int; 8]`; AVFrame.buf is
2108 // `[*mut AVBufferRef; 8]`; AVBufferRef.size is `usize`. All are
2109 // primitive reads / pointer dereferences with no enum interpretation.
2110 unsafe {
2111 let raw = frame.as_ptr();
2112 let first_linesize = (*raw).linesize[0];
2113 // Vertically-flipped (negative linesize) is the only "unsizeable"
2114 // case we still surface as `None`; everything else can be exactly
2115 // measured from buf[i].size.
2116 if first_linesize < 0 {
2117 return None;
2118 }
2119 let mut total: usize = 0;
2120 for i in 0..(*raw).buf.len() {
2121 let buf = (*raw).buf[i];
2122 if buf.is_null() {
2123 continue;
2124 }
2125 total = total.saturating_add((*buf).size);
2126 }
2127 Some(total)
2128 }
2129}
2130
2131#[allow(dead_code)]
2132fn _assert_send() {
2133 fn check<T: Send>() {}
2134 check::<VideoDecoder>();
2135}
2136
2137#[cfg(test)]
2138mod tests;