Skip to main content

mediadecode_ffmpeg/
frame.rs

1//! CPU-side decoded video frame.
2//!
3//! Wraps `ffmpeg_next::frame::Video`. All accessors read from raw `AVFrame`
4//! fields (`format`, `linesize`, `data`, `width`, `height`, `pts`) directly
5//! and never go through ffmpeg-next's `Video::format()` / `plane_height()`
6//! / `plane_width()` / `data()` — those construct `AVPixelFormat` from the
7//! frame's raw `format` integer via `transmute`, which is undefined behavior
8//! when the value isn't in the build's bindgen-generated discriminant set
9//! (the exact failure mode this crate is designed to survive).
10//!
11//! Per-row sizes for [`Frame::row`] / [`Frame::rows`] are computed from
12//! hardcoded chroma-subsampling and bit-depth tables keyed on the safe
13//! `pix_fmt()` integer, covering only the formats `hwdecode` produces (the
14//! NV* and P0xx/P2xx/P4xx families after `av_hwframe_transfer_data`). For
15//! any other format, the row accessors return `None` rather than guessing
16//! at a slice length.
17//!
18//! Why per-row, not whole-plane: FFmpeg allocates each row at
19//! `linesize[plane]` ([`Frame::stride`]) bytes for SIMD alignment, but
20//! hardware transfer paths only initialize the first
21//! [`Frame::row_bytes`]`(plane)` of every row. Exposing a stride-inclusive
22//! `&[u8]` over an entire plane would let safe code observe those
23//! uninitialized padding bytes, which violates `slice::from_raw_parts`.
24//! Per-row slices are tightly clipped to the visible byte width so the
25//! safe API never hands out an uninitialized byte. Callers that need a
26//! single base pointer (e.g. SIMD pixel converters keyed off stride) can
27//! reach for [`Frame::as_ptr`] and consume `stride * plane_h` bytes
28//! themselves under their own `unsafe` contract.
29//!
30//! Compare formats against the variants of
31//! [`mediadecode::PixelFormat`].
32
33use std::slice;
34
35use ffmpeg_next::frame;
36use mediadecode::PixelFormat;
37
38use crate::{
39  boundary,
40  error::{Error, Result},
41};
42
43/// Checked allocator for `ffmpeg_next::frame::Video`. ffmpeg-next's
44/// `Video::empty` is built on `av_frame_alloc()` and ignores its
45/// NULL-on-OOM return; the resulting `Video` would have a null inner
46/// `*mut AVFrame` and the next FFmpeg call against it would be UB.
47/// Use this helper anywhere a SW video scratch frame is constructed
48/// in production code.
49pub(crate) fn alloc_av_video_frame() -> Result<frame::Video> {
50  let f = frame::Video::empty();
51  // SAFETY: `as_ptr()` reads the inner pointer without dereferencing.
52  if unsafe { f.as_ptr() }.is_null() {
53    return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
54      errno: libc::ENOMEM,
55    }));
56  }
57  Ok(f)
58}
59
60/// Checked allocator for `ffmpeg_next::frame::Audio`. Same rationale
61/// as [`alloc_av_video_frame`].
62pub(crate) fn alloc_av_audio_frame() -> Result<frame::Audio> {
63  let f = frame::Audio::empty();
64  // SAFETY: `as_ptr()` reads the inner pointer without dereferencing.
65  if unsafe { f.as_ptr() }.is_null() {
66    return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
67      errno: libc::ENOMEM,
68    }));
69  }
70  Ok(f)
71}
72
73/// CPU-side decoded video frame produced by [`crate::VideoDecoder`].
74pub struct Frame {
75  inner: frame::Video,
76}
77
78impl core::fmt::Debug for Frame {
79  /// `frame::Video` (from `ffmpeg_next`) doesn't itself implement
80  /// `Debug`, so route through the public accessors. Shows the
81  /// dimensions, pixel format, plane count, and PTS — enough to
82  /// distinguish frames at debug-print sites without surfacing
83  /// raw FFI internals.
84  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
85    f.debug_struct("Frame")
86      .field("width", &self.width())
87      .field("height", &self.height())
88      .field("pix_fmt", &self.pix_fmt())
89      .field("planes", &self.planes())
90      .field("pts", &self.pts())
91      .finish()
92  }
93}
94
95impl Frame {
96  /// Construct an empty frame, suitable as the destination passed to
97  /// [`crate::VideoDecoder::receive_frame`].
98  ///
99  /// Returns `Err(Error::Ffmpeg(Other { errno: ENOMEM }))` when the
100  /// underlying `av_frame_alloc()` returns NULL — `ffmpeg_next` does not
101  /// surface that failure, so we check it here rather than letting a null
102  /// pointer flow into the safe accessors and become UB on first read.
103  pub fn empty() -> Result<Self> {
104    // SAFETY: as_ptr() is safe; we just inspect the value (potentially null).
105    let inner = frame::Video::empty();
106    if unsafe { inner.as_ptr() }.is_null() {
107      return Err(Error::Ffmpeg(ffmpeg_next::Error::Other {
108        errno: libc::ENOMEM,
109      }));
110    }
111    Ok(Self { inner })
112  }
113
114  /// Width in pixels.
115  pub fn width(&self) -> u32 {
116    // SAFETY: AVFrame.width is c_int; safe to read regardless of value.
117    unsafe { (*self.inner.as_ptr()).width as u32 }
118  }
119
120  /// Height in pixels.
121  pub fn height(&self) -> u32 {
122    // SAFETY: AVFrame.height is c_int.
123    unsafe { (*self.inner.as_ptr()).height as u32 }
124  }
125
126  /// Pixel format, returned as a [`PixelFormat`] (the unified
127  /// mediadecode enum). The mapping is via [`boundary::from_av_pixel_format`]
128  /// — sound regardless of the linked FFmpeg version, no
129  /// `AVPixelFormat` enum is constructed from a runtime integer.
130  pub fn pix_fmt(&self) -> PixelFormat {
131    // SAFETY: AVFrame.format is bound as c_int.
132    boundary::from_av_pixel_format(unsafe { (*self.inner.as_ptr()).format })
133  }
134
135  /// Presentation timestamp in stream time base, or `None` for
136  /// `AV_NOPTS_VALUE`.
137  pub fn pts(&self) -> Option<i64> {
138    // ffmpeg-next's Frame::pts performs no enum conversion; safe to use.
139    self.inner.pts()
140  }
141
142  /// Number of populated planes (1 for packed formats, 2 for NV12/P010,
143  /// 3 for planar YUV, etc.). Computed by scanning `linesize` for the
144  /// first zero entry — no enum reads.
145  pub fn planes(&self) -> usize {
146    // SAFETY: AVFrame.linesize is `[c_int; 8]`; reads are sound.
147    unsafe {
148      let linesize = &(*self.inner.as_ptr()).linesize;
149      for (i, ls) in linesize.iter().enumerate() {
150        if *ls == 0 {
151          return i;
152        }
153      }
154      linesize.len()
155    }
156  }
157
158  /// Bytes per row for `plane`. Reads `AVFrame.linesize[plane]` directly.
159  ///
160  /// # Panics
161  ///
162  /// Panics if `plane >= planes()` or the linesize is non-positive
163  /// (FFmpeg allows negative linesize for vertically-flipped formats;
164  /// this crate does not surface those). Callers who need to handle
165  /// either case without panicking should use [`Self::try_stride`],
166  /// or the non-panicking pixel accessors [`Self::row`] / [`Self::rows`]
167  /// / [`Self::row_bytes`] / [`Self::as_ptr`].
168  pub fn stride(&self, plane: usize) -> usize {
169    let n = self.planes();
170    assert!(
171      plane < n,
172      "stride: plane {plane} out of bounds (planes={n})"
173    );
174    // SAFETY: bounds-checked above; linesize is `[c_int; 8]`.
175    let linesize: i32 = unsafe { (*self.inner.as_ptr()).linesize[plane] };
176    assert!(
177      linesize > 0,
178      "stride: non-positive linesize {linesize} for plane {plane} \
179       (negative linesize means vertically-flipped — not supported)"
180    );
181    linesize as usize
182  }
183
184  /// Fallible counterpart to [`Self::stride`]. Returns `None` when
185  /// `plane` is out of bounds *or* the linesize is non-positive (the
186  /// two conditions [`Self::stride`] panics on). Use this when the
187  /// frame's plane count or layout is caller-controlled / data-driven
188  /// and either case should be handled rather than aborting.
189  pub fn try_stride(&self, plane: usize) -> Option<usize> {
190    if plane >= self.planes() {
191      return None;
192    }
193    // SAFETY: bounds-checked above; linesize is `[c_int; 8]`.
194    let linesize: i32 = unsafe { (*self.inner.as_ptr()).linesize[plane] };
195    if linesize <= 0 {
196      return None;
197    }
198    Some(linesize as usize)
199  }
200
201  /// Visible byte width of `plane` — the number of initialized bytes at
202  /// the start of every row in that plane.
203  ///
204  /// Distinct from [`Self::stride`], which returns the FFmpeg `linesize`.
205  /// `linesize` is `>= row_bytes` and may include trailing alignment
206  /// padding bytes that FFmpeg's hardware transfer paths do not
207  /// initialize. `row_bytes` is what `slice::from_raw_parts` can safely
208  /// see.
209  ///
210  /// Returns `None` when the format is not in the supported HW-output set
211  /// (see crate `pix_fmt`) or the plane is out of range.
212  pub fn row_bytes(&self, plane: usize) -> Option<usize> {
213    if plane >= self.planes() {
214      return None;
215    }
216    plane_row_bytes_for(self.pix_fmt(), plane, self.width() as usize)
217  }
218
219  /// Pixel data for one row of `plane`, tightly clipped to the visible
220  /// byte width ([`Self::row_bytes`]).
221  ///
222  /// Excludes the trailing alignment padding that [`Self::stride`]
223  /// includes — those bytes are not guaranteed to be initialized by
224  /// FFmpeg's hardware transfer paths and must not be exposed through a
225  /// safe `&[u8]`.
226  ///
227  /// Returns `None` for any of the following — never panics:
228  /// - The frame's pixel format is not one of the supported hardware-
229  ///   output formats listed in [`crate::pix_fmt`].
230  /// - The plane index is out of range.
231  /// - `y` is past the plane's row count.
232  /// - `AVFrame.linesize[plane]` is `<= 0` or `AVFrame.height` is `<= 0`.
233  /// - The plane's data pointer is null.
234  /// - The plane size would overflow `isize::MAX`.
235  pub fn row(&self, plane: usize, y: usize) -> Option<&[u8]> {
236    let info = self.plane_info(plane)?;
237    if y >= info.plane_h {
238      return None;
239    }
240    // y < plane_h and plane_h * stride ≤ isize::MAX (verified in plane_info),
241    // so y * stride is bounded by (plane_h - 1) * stride ≤ isize::MAX.
242    let offset = y * info.stride;
243    // SAFETY:
244    // - `info.plane_ptr` is non-null (verified in plane_info).
245    // - `offset + row_bytes ≤ plane_h * stride`, which is the size of the
246    //   FFmpeg allocation for this plane.
247    // - Bytes 0..row_bytes of every row are written by FFmpeg's HW
248    //   transfer; the slice is fully initialized.
249    // - `row_bytes ≤ stride ≤ isize::MAX` per plane_info.
250    unsafe {
251      let row_ptr = info.plane_ptr.add(offset);
252      Some(slice::from_raw_parts(row_ptr, info.row_bytes))
253    }
254  }
255
256  /// Iterator over every row of `plane`. Each yielded slice has length
257  /// [`Self::row_bytes`]`(plane)` — never includes the trailing alignment
258  /// padding that lives within [`Self::stride`].
259  ///
260  /// Returns `None` under the same conditions as [`Self::row`].
261  pub fn rows(&self, plane: usize) -> Option<impl Iterator<Item = &[u8]> + '_> {
262    let info = self.plane_info(plane)?;
263    Some((0..info.plane_h).map(move |y| {
264      // Same bounds argument as `row()`.
265      let offset = y * info.stride;
266      // SAFETY: see `row()` — the same invariants hold here, and the
267      // iterator's lifetime is tied to `&self` so the pointer remains
268      // valid for every yielded slice.
269      unsafe { slice::from_raw_parts(info.plane_ptr.add(offset), info.row_bytes) }
270    }))
271  }
272
273  /// Raw base pointer to `plane`'s allocation, or `None` if the plane
274  /// fails the same layout validation [`Self::row`] applies.
275  ///
276  /// Returns `None` whenever any of the following is true:
277  /// - The plane index is out of range (`plane >= planes()`).
278  /// - The frame's pixel format is not in the supported HW-output set.
279  /// - `linesize[plane] <= 0`. **In particular, FFmpeg permits negative
280  ///   linesizes for vertically-flipped frames with `data[n]` pointing
281  ///   at the *end* of the image. Returning that pointer with the
282  ///   advertised "valid for `stride * plane_h` bytes forward" contract
283  ///   would let a downstream converter walk past the buffer.** This
284  ///   accessor refuses the layout instead of handing back a pointer the
285  ///   caller cannot safely interpret as forward-addressable.
286  /// - `height <= 0`, the data pointer is null, `row_bytes > stride`, or
287  ///   the total plane size would overflow `isize::MAX`.
288  ///
289  /// On `Some(ptr)` the pointer is valid for
290  /// `stride(plane) * plane_height` *forward-addressable* bytes, and
291  /// only the first [`Self::row_bytes`]`(plane)` bytes of each row are
292  /// guaranteed to be initialized. The trailing per-row alignment padding
293  /// is uninitialized; callers performing wide SIMD loads that read past
294  /// `row_bytes` must mask the result and never surface those bytes
295  /// through a safe `&[u8]`.
296  ///
297  /// This accessor exists for downstream pixel-format converters
298  /// (`colconv`) that work in `(ptr, stride, width, height)` quadruples;
299  /// safe code should prefer [`Self::row`] / [`Self::rows`].
300  pub fn as_ptr(&self, plane: usize) -> Option<*const u8> {
301    // Share the full plane-layout validation so the unsafe escape hatch
302    // never escapes a layout that `row()` / `rows()` reject. Returning a
303    // pointer for a negative-stride frame (FFmpeg's vertical-flip
304    // convention, where `data[n]` points at the *end* of the image)
305    // would invite forward-walking out-of-bounds reads from a caller
306    // that trusts the documented "valid for stride × plane_h bytes"
307    // contract.
308    self.plane_info(plane).map(|info| info.plane_ptr)
309  }
310
311  /// Read every per-plane field needed by the row accessors with the
312  /// safety preconditions enforced once.
313  fn plane_info(&self, plane: usize) -> Option<PlaneInfo> {
314    if plane >= self.planes() {
315      return None;
316    }
317    // SAFETY: bounds-checked plane index; linesize/height/data are raw
318    // c_int / pointer reads that cannot themselves be UB.
319    let (stride_int, height_int, plane_ptr) = unsafe {
320      let raw = self.inner.as_ptr();
321      ((*raw).linesize[plane], (*raw).height, (*raw).data[plane])
322    };
323    if stride_int <= 0 || height_int <= 0 || plane_ptr.is_null() {
324      return None;
325    }
326    let stride = stride_int as usize;
327    let plane_h = plane_height_for(self.pix_fmt(), plane, height_int as usize)?;
328    let row_bytes = plane_row_bytes_for(self.pix_fmt(), plane, self.width() as usize)?;
329    if row_bytes > stride {
330      return None;
331    }
332    // Bound the entire plane allocation to isize::MAX so any byte offset
333    // computed as `y * stride` (y < plane_h) stays representable, satisfying
334    // the safety contract of `pointer::add` and `slice::from_raw_parts`.
335    let plane_size = stride.checked_mul(plane_h)?;
336    if plane_size > isize::MAX as usize {
337      return None;
338    }
339    Some(PlaneInfo {
340      plane_ptr,
341      stride,
342      plane_h,
343      row_bytes,
344    })
345  }
346
347  /// Crate-internal: hand the wrapped frame to FFmpeg / our decoder code.
348  pub(crate) fn as_inner_mut(&mut self) -> &mut frame::Video {
349    &mut self.inner
350  }
351}
352
353#[derive(Clone, Copy)]
354struct PlaneInfo {
355  plane_ptr: *const u8,
356  stride: usize,
357  plane_h: usize,
358  row_bytes: usize,
359}
360
361// `Default` intentionally omitted: constructing a frame can fail (OOM
362// in `av_frame_alloc`), and a panicking `default()` would defeat the
363// safety stance of [`Frame::empty`]. Use `Frame::empty()?` directly.
364
365/// Whether `pix_fmt_int` is a CPU pixel format the safe `Frame::row` /
366/// `Frame::rows` / `Frame::row_bytes` / `Frame::as_ptr` accessors
367/// support — i.e. one of the NV*/P0xx/P2xx/P4xx semi-planar families
368/// this crate expects HW backends to produce after
369/// `av_hwframe_transfer_data`.
370///
371/// Single source of truth for "supported CPU pix_fmt." Used by:
372/// - the safe `Frame::*` row accessors (via `plane_row_bytes_for` /
373///   `plane_height_for`, which agree with this helper — every format
374///   that returns `Some` from those functions is also accepted here).
375/// - [`crate::decoder::transfer_hw_frame`] post-transfer validation —
376///   if FFmpeg's auto-pick produces a format outside this set, treat
377///   as a backend failure so probe advances rather than collapsing on
378///   an unusable frame.
379/// - the probe-replay drain path in `drain_into_pending`, which
380///   refuses to queue an unusable candidate frame.
381pub(crate) fn is_supported_cpu_pix_fmt(pix_fmt: PixelFormat) -> bool {
382  matches!(
383    pix_fmt,
384    // --- HW download outputs (NV* + P0xx/P2xx/P4xx) ---
385    PixelFormat::Nv12
386      | PixelFormat::Nv21
387      | PixelFormat::Nv16
388      | PixelFormat::Nv24
389      | PixelFormat::P010Le
390      | PixelFormat::P012Le
391      | PixelFormat::P016Le
392      | PixelFormat::P210Le
393      | PixelFormat::P212Le
394      | PixelFormat::P216Le
395      | PixelFormat::P410Le
396      | PixelFormat::P412Le
397      | PixelFormat::P416Le
398      // --- SW decoder outputs: planar YUV ---
399      | PixelFormat::Yuv420p
400      | PixelFormat::Yuv422p
401      | PixelFormat::Yuv444p
402      | PixelFormat::Yuv420p10Le
403      | PixelFormat::Yuv420p12Le
404      | PixelFormat::Yuv420p16Le
405      | PixelFormat::Yuv422p10Le
406      | PixelFormat::Yuv422p12Le
407      | PixelFormat::Yuv422p16Le
408      | PixelFormat::Yuv444p10Le
409      | PixelFormat::Yuv444p12Le
410      | PixelFormat::Yuv444p16Le
411      // --- SW decoder outputs: packed RGB ---
412      | PixelFormat::Rgb24
413      | PixelFormat::Bgr24
414      | PixelFormat::Rgba
415      | PixelFormat::Bgra
416      | PixelFormat::Argb
417      | PixelFormat::Abgr
418      // --- SW decoder outputs: greyscale ---
419      | PixelFormat::Gray8
420      | PixelFormat::Gray16Le
421  )
422}
423
424/// Visible byte width of `plane`'s rows for a frame of `frame_width` and
425/// the given pixel format. `None` for formats not in the supported HW-
426/// output set.
427///
428/// Distinct from `linesize` (FFmpeg's per-row stride, which may include
429/// alignment padding). HW transfer paths only initialize bytes
430/// `0..plane_row_bytes_for(...)` of each row; everything from there to
431/// `stride` is uninitialized padding and must not be exposed via
432/// `slice::from_raw_parts`.
433pub(crate) fn plane_row_bytes_for(
434  pix_fmt: PixelFormat,
435  plane: usize,
436  frame_width: usize,
437) -> Option<usize> {
438  match pix_fmt {
439    // 8-bit semi-planar 4:2:0 / 4:2:2: Y at full width (1 byte/sample);
440    // UV interleaved at horizontally-subsampled chroma with `ceil(W/2)`
441    // U+V pairs at 2 bytes per pair. For even W the chroma row equals
442    // `W` bytes (the simple case); for odd W it must round *up* to the
443    // next even byte so the trailing chroma sample is not silently
444    // dropped on width = 2k+1 frames.
445    PixelFormat::Nv12 | PixelFormat::Nv21 | PixelFormat::Nv16 => match plane {
446      0 => Some(frame_width),
447      1 => Some(frame_width.div_ceil(2).checked_mul(2)?),
448      _ => None,
449    },
450    // 8-bit 4:4:4 semi-planar: chroma at full horizontal resolution,
451    // 2 bytes per pixel (1 byte U + 1 byte V) — no rounding required.
452    PixelFormat::Nv24 => match plane {
453      0 => Some(frame_width),
454      1 => Some(frame_width.checked_mul(2)?),
455      _ => None,
456    },
457    // 10/12/16-bit semi-planar 4:2:0 / 4:2:2: Y is 2 bytes/sample
458    // (high-bit-depth packed in 16-bit). UV interleaved at horizontally-
459    // subsampled chroma with `ceil(W/2)` U+V pairs at 4 bytes per pair
460    // (2 bytes U + 2 bytes V). Same odd-width rounding as the 8-bit
461    // chroma path, scaled by 2 bytes per sample.
462    PixelFormat::P010Le
463    | PixelFormat::P012Le
464    | PixelFormat::P016Le
465    | PixelFormat::P210Le
466    | PixelFormat::P212Le
467    | PixelFormat::P216Le => match plane {
468      0 => Some(frame_width.checked_mul(2)?),
469      1 => Some(frame_width.div_ceil(2).checked_mul(4)?),
470      _ => None,
471    },
472    // 10/12/16-bit 4:4:4 semi-planar: Y is 2 bytes/sample; UV at full
473    // horizontal resolution with 4 bytes per pixel (2 bytes U + 2 bytes V).
474    PixelFormat::P410Le | PixelFormat::P412Le | PixelFormat::P416Le => match plane {
475      0 => Some(frame_width.checked_mul(2)?),
476      1 => Some(frame_width.checked_mul(4)?),
477      _ => None,
478    },
479    // --- SW planar YUV 4:2:0 8-bit ---
480    PixelFormat::Yuv420p => match plane {
481      0 => Some(frame_width),
482      1 | 2 => Some(frame_width.div_ceil(2)),
483      _ => None,
484    },
485    // --- SW planar YUV 4:2:2 8-bit ---
486    PixelFormat::Yuv422p => match plane {
487      0 => Some(frame_width),
488      1 | 2 => Some(frame_width.div_ceil(2)),
489      _ => None,
490    },
491    // --- SW planar YUV 4:4:4 8-bit ---
492    PixelFormat::Yuv444p => match plane {
493      0..=2 => Some(frame_width),
494      _ => None,
495    },
496    // --- SW planar YUV 4:2:0 10/12/16-bit (low-packed in u16) ---
497    PixelFormat::Yuv420p10Le | PixelFormat::Yuv420p12Le | PixelFormat::Yuv420p16Le => match plane {
498      0 => Some(frame_width.checked_mul(2)?),
499      1 | 2 => Some(frame_width.div_ceil(2).checked_mul(2)?),
500      _ => None,
501    },
502    // --- SW planar YUV 4:2:2 10/12/16-bit ---
503    PixelFormat::Yuv422p10Le | PixelFormat::Yuv422p12Le | PixelFormat::Yuv422p16Le => match plane {
504      0 => Some(frame_width.checked_mul(2)?),
505      1 | 2 => Some(frame_width.div_ceil(2).checked_mul(2)?),
506      _ => None,
507    },
508    // --- SW planar YUV 4:4:4 10/12/16-bit ---
509    PixelFormat::Yuv444p10Le | PixelFormat::Yuv444p12Le | PixelFormat::Yuv444p16Le => match plane {
510      0..=2 => Some(frame_width.checked_mul(2)?),
511      _ => None,
512    },
513    // --- SW packed RGB 8-bit (3 bytes/pixel for RGB24/BGR24,
514    //     4 bytes/pixel for RGBA/BGRA/ARGB/ABGR). Single plane. ---
515    PixelFormat::Rgb24 | PixelFormat::Bgr24 => match plane {
516      0 => Some(frame_width.checked_mul(3)?),
517      _ => None,
518    },
519    PixelFormat::Rgba | PixelFormat::Bgra | PixelFormat::Argb | PixelFormat::Abgr => match plane {
520      0 => Some(frame_width.checked_mul(4)?),
521      _ => None,
522    },
523    // --- SW greyscale ---
524    PixelFormat::Gray8 => match plane {
525      0 => Some(frame_width),
526      _ => None,
527    },
528    PixelFormat::Gray16Le => match plane {
529      0 => Some(frame_width.checked_mul(2)?),
530      _ => None,
531    },
532    _ => None,
533  }
534}
535
536/// Number of rows in `plane` for a frame of `frame_height` and the given
537/// pixel format. `None` for formats not in the supported HW-output set.
538///
539/// Crate-internal so the decoder's probe-replay accountant can compute
540/// per-frame byte sizes without re-implementing the chroma-subsampling
541/// table.
542pub(crate) fn plane_height_for(
543  pix_fmt: PixelFormat,
544  plane: usize,
545  frame_height: usize,
546) -> Option<usize> {
547  match pix_fmt {
548    // 4:2:0 semi-planar — Y full height, chroma half height.
549    PixelFormat::Nv12
550    | PixelFormat::Nv21
551    | PixelFormat::P010Le
552    | PixelFormat::P012Le
553    | PixelFormat::P016Le => match plane {
554      0 => Some(frame_height),
555      1 => Some(frame_height.div_ceil(2)),
556      _ => None,
557    },
558    // 4:2:2 / 4:4:4 semi-planar — both planes full height.
559    PixelFormat::Nv16
560    | PixelFormat::Nv24
561    | PixelFormat::P210Le
562    | PixelFormat::P212Le
563    | PixelFormat::P216Le
564    | PixelFormat::P410Le
565    | PixelFormat::P412Le
566    | PixelFormat::P416Le => match plane {
567      0 | 1 => Some(frame_height),
568      _ => None,
569    },
570    // --- SW planar YUV 4:2:0: Y full, U/V half-height ---
571    PixelFormat::Yuv420p
572    | PixelFormat::Yuv420p10Le
573    | PixelFormat::Yuv420p12Le
574    | PixelFormat::Yuv420p16Le => match plane {
575      0 => Some(frame_height),
576      1 | 2 => Some(frame_height.div_ceil(2)),
577      _ => None,
578    },
579    // --- SW planar YUV 4:2:2 / 4:4:4: all planes full height ---
580    PixelFormat::Yuv422p
581    | PixelFormat::Yuv422p10Le
582    | PixelFormat::Yuv422p12Le
583    | PixelFormat::Yuv422p16Le
584    | PixelFormat::Yuv444p
585    | PixelFormat::Yuv444p10Le
586    | PixelFormat::Yuv444p12Le
587    | PixelFormat::Yuv444p16Le => match plane {
588      0..=2 => Some(frame_height),
589      _ => None,
590    },
591    // --- SW packed RGB / greyscale: single plane, full height ---
592    PixelFormat::Rgb24
593    | PixelFormat::Bgr24
594    | PixelFormat::Rgba
595    | PixelFormat::Bgra
596    | PixelFormat::Argb
597    | PixelFormat::Abgr
598    | PixelFormat::Gray8
599    | PixelFormat::Gray16Le => match plane {
600      0 => Some(frame_height),
601      _ => None,
602    },
603    _ => None,
604  }
605}
606
607#[cfg(test)]
608mod tests {
609  use super::*;
610  use ffmpeg_next::ffi::AVPixelFormat;
611
612  #[test]
613  fn empty_frame_has_zero_dimensions_and_no_pts() {
614    let f = Frame::empty().expect("alloc");
615    assert_eq!(f.width(), 0);
616    assert_eq!(f.height(), 0);
617    assert_eq!(f.pts(), None);
618    // AVFrame.format defaults to -1 (AV_PIX_FMT_NONE) for an empty frame.
619    assert!(matches!(f.pix_fmt(), PixelFormat::Unknown(_)));
620    // No active planes for an empty frame (all linesize entries are 0).
621    assert_eq!(f.planes(), 0);
622  }
623
624  #[test]
625  fn row_returns_none_for_unknown_format() {
626    let f = Frame::empty().expect("alloc");
627    // pix_fmt is NONE (-1), not in the supported set.
628    assert!(f.row(0, 0).is_none());
629    assert!(f.rows(0).is_none());
630    assert!(f.row_bytes(0).is_none());
631  }
632
633  /// Synthesize a frame with a negative linesize (FFmpeg's vertical-flip
634  /// convention) and assert the row accessors refuse to construct a slice.
635  /// Without the linesize > 0 check, the negative `i32 as usize` would
636  /// produce a huge positive length and `from_raw_parts` would be UB.
637  ///
638  /// `as_ptr` shares the same validation — handing back the data pointer
639  /// for a negative-stride frame would let a downstream converter
640  /// following the "valid for stride × plane_h bytes forward" contract
641  /// walk past the buffer.
642  #[test]
643  fn row_returns_none_for_negative_linesize() {
644    let mut f = Frame::empty().expect("alloc");
645    unsafe {
646      let raw = f.inner.as_mut_ptr();
647      (*raw).format = AVPixelFormat::AV_PIX_FMT_NV12 as i32;
648      (*raw).width = 1920;
649      (*raw).height = 1080;
650      (*raw).linesize[0] = -1920; // vertically-flipped
651      (*raw).linesize[1] = -1920;
652      // data pointers stay null; the accessors would also reject on null,
653      // but should bail earlier on the linesize sign.
654    }
655    assert!(f.row(0, 0).is_none());
656    assert!(f.row(1, 0).is_none());
657    assert!(f.rows(0).is_none());
658    assert!(
659      f.as_ptr(0).is_none(),
660      "as_ptr must share row()/rows() validation — a negative-stride \
661       frame must not leak a forward-readable plane pointer"
662    );
663    assert!(f.as_ptr(1).is_none());
664  }
665
666  #[test]
667  fn row_returns_none_for_non_positive_height() {
668    let mut f = Frame::empty().expect("alloc");
669    unsafe {
670      let raw = f.inner.as_mut_ptr();
671      (*raw).format = AVPixelFormat::AV_PIX_FMT_NV12 as i32;
672      (*raw).width = 1920;
673      (*raw).height = 0;
674      (*raw).linesize[0] = 1920;
675      (*raw).linesize[1] = 1920;
676    }
677    assert!(f.row(0, 0).is_none());
678  }
679
680  /// Synthesize a frame backed by a manually-allocated buffer with stride
681  /// strictly larger than visible row bytes (the exact case where
682  /// FFmpeg's HW transfer leaves trailing padding uninitialized) and
683  /// confirm the safe row accessor returns slices clipped to the visible
684  /// width.
685  #[test]
686  fn row_clips_to_visible_width_not_stride() {
687    use std::alloc::{Layout, alloc, dealloc};
688    let width = 64usize;
689    let height = 4usize;
690    // Stride > width: 16 bytes of padding per row in the Y plane.
691    let stride = 80usize;
692    let plane_size = stride * height;
693    // Allocate ourselves so we can fully control initialization. Fill
694    // bytes 0..width with 0xAA per row (the "valid pixel" range) and
695    // bytes width..stride with 0xFF (the simulated alignment padding —
696    // FFmpeg would leave these uninitialized; we set them to a sentinel
697    // that the test can detect if the safe slice ever exposes them).
698    let layout = Layout::from_size_align(plane_size, 32).unwrap();
699    let buf = unsafe { alloc(layout) };
700    assert!(!buf.is_null());
701    for y in 0..height {
702      let row = unsafe { buf.add(y * stride) };
703      for x in 0..width {
704        unsafe { *row.add(x) = 0xAA };
705      }
706      for x in width..stride {
707        unsafe { *row.add(x) = 0xFF };
708      }
709    }
710
711    let mut f = Frame::empty().expect("alloc");
712    unsafe {
713      let raw = f.inner.as_mut_ptr();
714      (*raw).format = AVPixelFormat::AV_PIX_FMT_NV12 as i32;
715      (*raw).width = width as i32;
716      (*raw).height = height as i32;
717      (*raw).linesize[0] = stride as i32;
718      // linesize[1] = 0 keeps planes() at 1 so the test stays focused on
719      // plane 0 without owning a second allocation.
720      (*raw).data[0] = buf;
721    }
722
723    assert_eq!(f.row_bytes(0), Some(width));
724    assert_eq!(f.stride(0), stride);
725    let row0 = f.row(0, 0).expect("row 0");
726    assert_eq!(
727      row0.len(),
728      width,
729      "safe row must be clipped to visible width"
730    );
731    assert!(
732      row0.iter().all(|&b| b == 0xAA),
733      "row must not include padding sentinel 0xFF"
734    );
735
736    let collected: Vec<&[u8]> = f.rows(0).expect("rows iterator").collect();
737    assert_eq!(collected.len(), height);
738    for r in &collected {
739      assert_eq!(r.len(), width);
740      assert!(r.iter().all(|&b| b == 0xAA));
741    }
742
743    // `as_ptr` accepts the valid layout and returns the same base pointer
744    // FFmpeg wrote into `data[0]`, so SIMD callers can reach the plane
745    // through the documented unsafe contract.
746    assert_eq!(
747      f.as_ptr(0),
748      Some(buf as *const u8),
749      "as_ptr must surface the plane base for a valid forward-stride frame"
750    );
751
752    // Out-of-range row index returns None instead of panicking.
753    assert!(f.row(0, height).is_none());
754
755    // Detach the buffer before drop so AVFrame's own free path doesn't
756    // touch our manual allocation.
757    unsafe {
758      (*f.inner.as_mut_ptr()).data[0] = std::ptr::null_mut();
759      dealloc(buf, layout);
760    }
761  }
762
763  #[test]
764  #[should_panic(expected = "non-positive linesize")]
765  fn stride_panics_on_negative_linesize() {
766    let mut f = Frame::empty().expect("alloc");
767    unsafe {
768      let raw = f.inner.as_mut_ptr();
769      (*raw).linesize[0] = -1920;
770    }
771    let _ = f.stride(0);
772  }
773
774  #[test]
775  fn frame_is_send() {
776    fn check<T: Send>() {}
777    check::<Frame>();
778  }
779
780  #[test]
781  fn plane_height_table_covers_supported_formats() {
782    // Spot-check the chroma subsampling table.
783    assert_eq!(plane_height_for(PixelFormat::Nv12, 0, 1080), Some(1080));
784    assert_eq!(plane_height_for(PixelFormat::Nv12, 1, 1080), Some(540));
785    assert_eq!(plane_height_for(PixelFormat::Nv12, 1, 1081), Some(541));
786    assert_eq!(plane_height_for(PixelFormat::P010Le, 1, 1080), Some(540));
787    assert_eq!(plane_height_for(PixelFormat::Nv16, 1, 1080), Some(1080));
788    assert_eq!(plane_height_for(PixelFormat::Nv24, 1, 1080), Some(1080));
789    assert_eq!(plane_height_for(PixelFormat::P416Le, 1, 1080), Some(1080));
790    assert_eq!(plane_height_for(PixelFormat::Unknown(0), 0, 1080), None);
791    assert_eq!(plane_height_for(PixelFormat::Nv12, 2, 1080), None);
792  }
793
794  /// 4:2:0 / 4:2:2 chroma planes carry `ceil(W/2)` U+V pairs per row.
795  /// For odd `W`, dropping the round-up silently truncates the last chroma
796  /// sample — and the safe row slice would expose a buffer one byte (8-bit)
797  /// or two bytes (high-bit-depth) shorter than the data FFmpeg actually
798  /// wrote. Y planes and 4:4:4 chroma planes are unaffected because their
799  /// row count is just `W` or a fixed multiple of `W`.
800  #[test]
801  fn plane_row_bytes_rounds_up_chroma_for_odd_widths() {
802    // 8-bit subsampled chroma — odd W gains one byte (the missing sample
803    // pair).
804    assert_eq!(plane_row_bytes_for(PixelFormat::Nv12, 1, 1921), Some(1922));
805    assert_eq!(plane_row_bytes_for(PixelFormat::Nv21, 1, 1921), Some(1922));
806    assert_eq!(plane_row_bytes_for(PixelFormat::Nv16, 1, 1921), Some(1922));
807    // High-bit-depth subsampled chroma — odd W gains two bytes.
808    assert_eq!(
809      plane_row_bytes_for(PixelFormat::P010Le, 1, 1921),
810      Some(3844)
811    );
812    assert_eq!(
813      plane_row_bytes_for(PixelFormat::P010Le, 1, 1921),
814      Some(3844)
815    );
816    assert_eq!(
817      plane_row_bytes_for(PixelFormat::P012Le, 1, 1921),
818      Some(3844)
819    );
820    assert_eq!(
821      plane_row_bytes_for(PixelFormat::P016Le, 1, 1921),
822      Some(3844)
823    );
824    assert_eq!(
825      plane_row_bytes_for(PixelFormat::P210Le, 1, 1921),
826      Some(3844)
827    );
828    assert_eq!(
829      plane_row_bytes_for(PixelFormat::P212Le, 1, 1921),
830      Some(3844)
831    );
832    assert_eq!(
833      plane_row_bytes_for(PixelFormat::P216Le, 1, 1921),
834      Some(3844)
835    );
836    // Y planes always at full width regardless of subsampling.
837    assert_eq!(plane_row_bytes_for(PixelFormat::Nv12, 0, 1921), Some(1921));
838    assert_eq!(
839      plane_row_bytes_for(PixelFormat::P010Le, 0, 1921),
840      Some(3842)
841    );
842    // 4:4:4 chroma is at full horizontal resolution — no rounding.
843    assert_eq!(plane_row_bytes_for(PixelFormat::Nv24, 1, 1921), Some(3842));
844    assert_eq!(
845      plane_row_bytes_for(PixelFormat::P410Le, 1, 1921),
846      Some(7684)
847    );
848    // Even widths must still match the original (pre-fix) values so the
849    // change is purely additive on the dominant code path.
850    assert_eq!(plane_row_bytes_for(PixelFormat::Nv12, 1, 1920), Some(1920));
851    assert_eq!(
852      plane_row_bytes_for(PixelFormat::P010Le, 1, 1920),
853      Some(3840)
854    );
855  }
856
857  #[test]
858  fn plane_row_bytes_table_covers_supported_formats() {
859    // 8-bit 4:2:0 / 4:2:2 — both planes at width.
860    assert_eq!(plane_row_bytes_for(PixelFormat::Nv12, 0, 1920), Some(1920));
861    assert_eq!(plane_row_bytes_for(PixelFormat::Nv12, 1, 1920), Some(1920));
862    assert_eq!(plane_row_bytes_for(PixelFormat::Nv21, 1, 1920), Some(1920));
863    assert_eq!(plane_row_bytes_for(PixelFormat::Nv16, 1, 1920), Some(1920));
864    // 8-bit 4:4:4 — chroma plane is 2 * width.
865    assert_eq!(plane_row_bytes_for(PixelFormat::Nv24, 0, 1920), Some(1920));
866    assert_eq!(plane_row_bytes_for(PixelFormat::Nv24, 1, 1920), Some(3840));
867    // 10/12/16-bit 4:2:0 / 4:2:2 — both planes at 2 * width.
868    assert_eq!(
869      plane_row_bytes_for(PixelFormat::P010Le, 0, 1920),
870      Some(3840)
871    );
872    assert_eq!(
873      plane_row_bytes_for(PixelFormat::P010Le, 1, 1920),
874      Some(3840)
875    );
876    assert_eq!(
877      plane_row_bytes_for(PixelFormat::P210Le, 1, 1920),
878      Some(3840)
879    );
880    // 10/12/16-bit 4:4:4 — Y is 2 * width, chroma is 4 * width.
881    assert_eq!(
882      plane_row_bytes_for(PixelFormat::P410Le, 0, 1920),
883      Some(3840)
884    );
885    assert_eq!(
886      plane_row_bytes_for(PixelFormat::P410Le, 1, 1920),
887      Some(7680)
888    );
889    assert_eq!(
890      plane_row_bytes_for(PixelFormat::P416Le, 1, 1920),
891      Some(7680)
892    );
893    // Unsupported / out-of-range.
894    assert_eq!(plane_row_bytes_for(PixelFormat::Unknown(0), 0, 1920), None);
895    assert_eq!(plane_row_bytes_for(PixelFormat::Nv12, 2, 1920), None);
896  }
897
898  /// Every format `is_supported_cpu_pix_fmt` accepts must also have a
899  /// row-byte table entry (otherwise `Frame::row_bytes` would return
900  /// `None` for a "supported" format), and every format the table
901  /// accepts must be in `is_supported_cpu_pix_fmt`. The
902  /// post-transfer validator and the safe row accessor must agree
903  /// on what's usable.
904  #[test]
905  fn is_supported_cpu_pix_fmt_agrees_with_row_byte_table() {
906    let supported = [
907      PixelFormat::Nv12,
908      PixelFormat::Nv21,
909      PixelFormat::Nv16,
910      PixelFormat::Nv24,
911      PixelFormat::P010Le,
912      PixelFormat::P010Le,
913      PixelFormat::P012Le,
914      PixelFormat::P016Le,
915      PixelFormat::P210Le,
916      PixelFormat::P212Le,
917      PixelFormat::P216Le,
918      PixelFormat::P410Le,
919      PixelFormat::P412Le,
920      PixelFormat::P416Le,
921    ];
922    for fmt in supported {
923      assert!(
924        is_supported_cpu_pix_fmt(fmt),
925        "is_supported_cpu_pix_fmt rejected pix_fmt {fmt:?}, but the row-byte \
926         table accepts it — the two are out of sync"
927      );
928      assert!(
929        plane_row_bytes_for(fmt, 0, 1920).is_some(),
930        "plane_row_bytes_for rejected pix_fmt {fmt:?}, but \
931         is_supported_cpu_pix_fmt accepts it — out of sync"
932      );
933      assert!(
934        plane_height_for(fmt, 0, 1080).is_some(),
935        "plane_height_for rejected pix_fmt {fmt:?} — out of sync"
936      );
937    }
938  }
939
940  /// Common CPU formats outside the supported HW-output set must be
941  /// rejected. These are the formats a misbehaving driver might pick
942  /// for `av_hwframe_transfer_data`'s auto-format selection that the
943  /// safe `Frame` accessors would silently fail on.
944  #[test]
945  fn is_supported_cpu_pix_fmt_rejects_common_unsupported_formats() {
946    use ffmpeg_next::ffi::AVPixelFormat;
947
948    // AV_PIX_FMT_NONE sentinel and HW pix_fmts (those should never
949    // surface post-transfer).
950    assert!(!is_supported_cpu_pix_fmt(PixelFormat::Unknown(0)));
951    assert!(!is_supported_cpu_pix_fmt(boundary::from_av_pixel_format(
952      AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32
953    )));
954    assert!(!is_supported_cpu_pix_fmt(boundary::from_av_pixel_format(
955      AVPixelFormat::AV_PIX_FMT_VAAPI as i32
956    )));
957    assert!(!is_supported_cpu_pix_fmt(boundary::from_av_pixel_format(
958      AVPixelFormat::AV_PIX_FMT_CUDA as i32
959    )));
960    assert!(!is_supported_cpu_pix_fmt(boundary::from_av_pixel_format(
961      AVPixelFormat::AV_PIX_FMT_D3D11 as i32
962    )));
963
964    // YUVJ420P (deprecated full-range marker) maps to PixelFormat::Unknown
965    // — we don't surface the J variants since the range info now lives
966    // on `ColorInfo::range`.
967    assert!(!is_supported_cpu_pix_fmt(boundary::from_av_pixel_format(
968      AVPixelFormat::AV_PIX_FMT_YUVJ420P as i32
969    )));
970
971    // Note: YUV420P / YUV422P / YUV444P / RGB24 / BGR24 / RGBA / BGRA
972    // are now intentionally **supported** (added when SW fallback
973    // landed in the FfmpegVideoStreamDecoder). They previously appeared
974    // here as "unsupported" when this crate was HW-only.
975
976    // A future / unknown format value FFmpeg might invent — the helper
977    // is closed-set so unknown integers are always rejected without
978    // constructing the bindgen enum.
979    assert!(!is_supported_cpu_pix_fmt(boundary::from_av_pixel_format(
980      99_999_999
981    )));
982  }
983}