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}