Skip to main content

mediadecode_ffmpeg/
boundary.rs

1//! Boundary conversions between FFmpeg's bindgen integers and the
2//! unified [`mediadecode`] vocabulary.
3//!
4//! Centralised so the rest of the crate never compares raw
5//! `AVPixelFormat` integers against literals or transmutes back into
6//! the bindgen enum (UB hazard when the value isn't in the enum's
7//! discriminant set).
8
9use core::ffi::c_int;
10
11use ffmpeg_next::{Packet, ffi::AVPixelFormat};
12use mediadecode::{
13  PixelFormat, Timestamp,
14  channel::AudioChannelLayout,
15  frame::{AudioFrame, Dimensions, Plane, SubtitleFrame, VideoFrame},
16  packet::{AudioPacket, PacketFlags as MdPacketFlags, SubtitlePacket, VideoPacket},
17  subtitle::SubtitlePayload,
18};
19
20use crate::{
21  FfmpegBuffer,
22  extras::{
23    AudioFrameExtra, AudioPacketExtra, SubtitleFrameExtra, SubtitlePacketExtra, VideoFrameExtra,
24    VideoPacketExtra,
25  },
26  sample_format::SampleFormat,
27};
28
29/// Maps a raw `AVFrame.format` integer (i.e. the value of an
30/// `AVPixelFormat` enum variant) onto [`mediadecode::PixelFormat`].
31///
32/// Returns [`PixelFormat::Unknown`] for raw integers we don't have a
33/// mapping for — including hardware-frame markers
34/// (`AV_PIX_FMT_VIDEOTOOLBOX` / `_VAAPI` / `_CUDA` / `_D3D11` / …)
35/// since those never describe CPU-side pixel data and the unified
36/// enum intentionally doesn't carry them. Use [`is_hardware_pix_fmt`]
37/// to identify HW frames before transferring to a CPU format.
38///
39/// The match never constructs an `AVPixelFormat` from a runtime
40/// value; it compares the input against `AVPixelFormat::AV_PIX_FMT_X
41/// as i32` constants. Sound regardless of which discriminant set the
42/// linked FFmpeg version exposes.
43pub const fn from_av_pixel_format(raw: i32) -> PixelFormat {
44  match raw {
45    // Semi-planar YUV 8-bit.
46    x if x == AVPixelFormat::AV_PIX_FMT_NV12 as i32 => PixelFormat::Nv12,
47    x if x == AVPixelFormat::AV_PIX_FMT_NV21 as i32 => PixelFormat::Nv21,
48    x if x == AVPixelFormat::AV_PIX_FMT_NV16 as i32 => PixelFormat::Nv16,
49    x if x == AVPixelFormat::AV_PIX_FMT_NV24 as i32 => PixelFormat::Nv24,
50    x if x == AVPixelFormat::AV_PIX_FMT_NV42 as i32 => PixelFormat::Nv42,
51    // Semi-planar YUV high-bit-depth.
52    x if x == AVPixelFormat::AV_PIX_FMT_P010LE as i32 => PixelFormat::P010Le,
53    // BE-tagged FFmpeg formats map to mediadecode's distinct BE
54    // variants. Folding BE onto the LE canonical enum was a previous
55    // shortcut that silently corrupted pixel data: each 16-bit sample
56    // is byte-swapped between BE and LE, and the convert path
57    // exports the AVBufferRef bytes verbatim without endian
58    // conversion. Consumers reading the planes as LE samples on a
59    // BE-tagged frame would interpret every Y/UV sample with its
60    // bytes reversed. By mapping to the BE variant we let
61    // `is_supported_cpu_pix_fmt` correctly reject the format until
62    // proper BE support (or a byte-swap) is wired in.
63    x if x == AVPixelFormat::AV_PIX_FMT_P010BE as i32 => PixelFormat::P010Be,
64    x if x == AVPixelFormat::AV_PIX_FMT_P012LE as i32 => PixelFormat::P012Le,
65    x if x == AVPixelFormat::AV_PIX_FMT_P016LE as i32 => PixelFormat::P016Le,
66    x if x == AVPixelFormat::AV_PIX_FMT_P210LE as i32 => PixelFormat::P210Le,
67    x if x == AVPixelFormat::AV_PIX_FMT_P212LE as i32 => PixelFormat::P212Le,
68    x if x == AVPixelFormat::AV_PIX_FMT_P216LE as i32 => PixelFormat::P216Le,
69    x if x == AVPixelFormat::AV_PIX_FMT_P410LE as i32 => PixelFormat::P410Le,
70    x if x == AVPixelFormat::AV_PIX_FMT_P412LE as i32 => PixelFormat::P412Le,
71    x if x == AVPixelFormat::AV_PIX_FMT_P416LE as i32 => PixelFormat::P416Le,
72    // Planar YUV 8-bit.
73    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P as i32 => PixelFormat::Yuv420p,
74    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P as i32 => PixelFormat::Yuv422p,
75    x if x == AVPixelFormat::AV_PIX_FMT_YUV440P as i32 => PixelFormat::Yuv440p,
76    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P as i32 => PixelFormat::Yuv444p,
77    x if x == AVPixelFormat::AV_PIX_FMT_YUV411P as i32 => PixelFormat::Yuv411p,
78    x if x == AVPixelFormat::AV_PIX_FMT_YUV410P as i32 => PixelFormat::Yuv410p,
79    // Planar YUV 4:2:0 high-bit.
80    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P9LE as i32 => PixelFormat::Yuv420p9Le,
81    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P10LE as i32 => PixelFormat::Yuv420p10Le,
82    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P12LE as i32 => PixelFormat::Yuv420p12Le,
83    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P14LE as i32 => PixelFormat::Yuv420p14Le,
84    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P16LE as i32 => PixelFormat::Yuv420p16Le,
85    // Planar YUV 4:2:2 high-bit.
86    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P9LE as i32 => PixelFormat::Yuv422p9Le,
87    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P10LE as i32 => PixelFormat::Yuv422p10Le,
88    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P12LE as i32 => PixelFormat::Yuv422p12Le,
89    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P14LE as i32 => PixelFormat::Yuv422p14Le,
90    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P16LE as i32 => PixelFormat::Yuv422p16Le,
91    // Planar YUV 4:4:4 high-bit.
92    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P9LE as i32 => PixelFormat::Yuv444p9Le,
93    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P10LE as i32 => PixelFormat::Yuv444p10Le,
94    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P12LE as i32 => PixelFormat::Yuv444p12Le,
95    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P14LE as i32 => PixelFormat::Yuv444p14Le,
96    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P16LE as i32 => PixelFormat::Yuv444p16Le,
97    // Planar YUVA 8-bit.
98    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P as i32 => PixelFormat::Yuva420p,
99    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P as i32 => PixelFormat::Yuva422p,
100    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P as i32 => PixelFormat::Yuva444p,
101    // Packed YUV 8-bit.
102    x if x == AVPixelFormat::AV_PIX_FMT_YUYV422 as i32 => PixelFormat::Yuyv422,
103    x if x == AVPixelFormat::AV_PIX_FMT_UYVY422 as i32 => PixelFormat::Uyvy422,
104    x if x == AVPixelFormat::AV_PIX_FMT_YVYU422 as i32 => PixelFormat::Yvyu422,
105    // Packed RGB 8-bit.
106    x if x == AVPixelFormat::AV_PIX_FMT_RGB24 as i32 => PixelFormat::Rgb24,
107    x if x == AVPixelFormat::AV_PIX_FMT_BGR24 as i32 => PixelFormat::Bgr24,
108    x if x == AVPixelFormat::AV_PIX_FMT_RGBA as i32 => PixelFormat::Rgba,
109    x if x == AVPixelFormat::AV_PIX_FMT_BGRA as i32 => PixelFormat::Bgra,
110    x if x == AVPixelFormat::AV_PIX_FMT_ARGB as i32 => PixelFormat::Argb,
111    x if x == AVPixelFormat::AV_PIX_FMT_ABGR as i32 => PixelFormat::Abgr,
112    // Packed RGB high-bit.
113    x if x == AVPixelFormat::AV_PIX_FMT_RGB48LE as i32 => PixelFormat::Rgb48Le,
114    x if x == AVPixelFormat::AV_PIX_FMT_BGR48LE as i32 => PixelFormat::Bgr48Le,
115    x if x == AVPixelFormat::AV_PIX_FMT_RGBA64LE as i32 => PixelFormat::Rgba64Le,
116    x if x == AVPixelFormat::AV_PIX_FMT_BGRA64LE as i32 => PixelFormat::Bgra64Le,
117    // Greyscale.
118    x if x == AVPixelFormat::AV_PIX_FMT_GRAY8 as i32 => PixelFormat::Gray8,
119    x if x == AVPixelFormat::AV_PIX_FMT_GRAY16LE as i32 => PixelFormat::Gray16Le,
120    // Bayer.
121    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_BGGR8 as i32 => PixelFormat::BayerBggr8,
122    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_RGGB8 as i32 => PixelFormat::BayerRggb8,
123    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GBRG8 as i32 => PixelFormat::BayerGbrg8,
124    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GRBG8 as i32 => PixelFormat::BayerGrbg8,
125    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_BGGR16LE as i32 => PixelFormat::BayerBggr16Le,
126    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_RGGB16LE as i32 => PixelFormat::BayerRggb16Le,
127    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GBRG16LE as i32 => PixelFormat::BayerGbrg16Le,
128    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GRBG16LE as i32 => PixelFormat::BayerGrbg16Le,
129    _ => PixelFormat::Unknown(raw as u32),
130  }
131}
132
133/// Returns `true` when `raw` is one of FFmpeg's hardware-frame markers
134/// (`AV_PIX_FMT_VIDEOTOOLBOX` / `_VAAPI` / `_CUDA` / `_D3D11` /
135/// `_DRM_PRIME` / `_MEDIACODEC` / `_VULKAN`). Used by the HW probe to
136/// identify GPU-resident frames before triggering
137/// `av_hwframe_transfer_data`.
138pub const fn is_hardware_pix_fmt(raw: i32) -> bool {
139  matches!(
140    raw,
141    x if x == AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32
142      || x == AVPixelFormat::AV_PIX_FMT_VAAPI as i32
143      || x == AVPixelFormat::AV_PIX_FMT_CUDA as i32
144      || x == AVPixelFormat::AV_PIX_FMT_D3D11 as i32
145      || x == AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32
146      || x == AVPixelFormat::AV_PIX_FMT_MEDIACODEC as i32
147      || x == AVPixelFormat::AV_PIX_FMT_VULKAN as i32
148  )
149}
150
151/// Fallible counterpart to ffmpeg-next's `Packet::copy`.
152///
153/// The upstream helper calls `Packet::new(size)` (which silently
154/// truncates `size` to `c_int` and ignores `av_new_packet`'s return
155/// code) and then panics via `data_mut().unwrap().write_all(...).unwrap()`
156/// if the allocation failed. From a safe public decoder API we want
157/// the OOM / oversized-payload paths to surface as
158/// `ffmpeg_next::Error` rather than aborting the process — every
159/// `send_packet` path goes through this helper.
160///
161/// Failure modes:
162/// * payload larger than `c_int::MAX` (would overflow `AVPacket.size`)
163///   → `ffmpeg_next::Error::Other { errno: libc::EINVAL }`.
164/// * `av_new_packet` allocation failure (signalled by `data_mut()`
165///   returning `None`) → `ffmpeg_next::Error::Other { errno:
166///   libc::ENOMEM }`.
167fn try_packet_copy(data: &[u8]) -> std::result::Result<Packet, ffmpeg_next::Error> {
168  // FFmpeg's `AVPacket.size` is `c_int`. A payload larger than that
169  // can't fit in a single packet — refuse rather than truncate via
170  // `as c_int` inside `Packet::new`.
171  if data.len() > c_int::MAX as usize {
172    return Err(ffmpeg_next::Error::Other {
173      errno: libc::EINVAL,
174    });
175  }
176  // `Packet::new(size)` calls `av_new_packet(&mut pkt, size as
177  // c_int)` and ignores the return code; on OOM it returns a
178  // `Packet` whose `.data` is null. We detect that via
179  // `data_mut()` (returns `None` on null) and copy via
180  // `copy_nonoverlapping` so we never go through `data_mut()
181  // .unwrap().write_all().unwrap()` — the upstream `Packet::copy`'s
182  // double panic.
183  let mut pkt = Packet::new(data.len());
184  match pkt.data_mut() {
185    Some(slot) if slot.len() == data.len() => {
186      // SAFETY: `slot` is a `&mut [u8]` of `data.len()` bytes;
187      // `data` is a `&[u8]` of the same length. Non-overlapping
188      // because `slot` is a fresh allocation.
189      if !data.is_empty() {
190        unsafe {
191          core::ptr::copy_nonoverlapping(data.as_ptr(), slot.as_mut_ptr(), data.len());
192        }
193      }
194      Ok(pkt)
195    }
196    _ => Err(ffmpeg_next::Error::Other {
197      errno: libc::ENOMEM,
198    }),
199  }
200}
201
202/// Centralised mediadecode→AV packet flag mapping so the three
203/// packet-conversion helpers stay aligned.
204fn map_md_flags_to_av(flags: MdPacketFlags) -> ffmpeg_next::packet::Flags {
205  let mut av_flags = ffmpeg_next::packet::Flags::empty();
206  if flags.contains(MdPacketFlags::KEY) {
207    av_flags |= ffmpeg_next::packet::Flags::KEY;
208  }
209  if flags.contains(MdPacketFlags::CORRUPT) {
210    av_flags |= ffmpeg_next::packet::Flags::CORRUPT;
211  }
212  // ffmpeg-next 8.x doesn't expose a DISCARD flag constant on
213  // `packet::Flags`; the upstream `AV_PKT_FLAG_DISCARD` bit is
214  // documented as a demuxer hint and rarely set on packets passed
215  // to a decoder. We forward KEY and CORRUPT (the meaningful subset)
216  // and silently drop DISCARD until ffmpeg-next adds it.
217  av_flags
218}
219
220/// Builds an `ffmpeg::Packet` from a [`mediadecode::VideoPacket`]
221/// parameterized by [`crate::extras::VideoPacketExtra`] and
222/// [`crate::FfmpegBuffer`].
223///
224/// The compressed bytes are **copied** into a new packet allocation —
225/// zero-copy passthrough of the FfmpegBuffer's underlying AVBufferRef
226/// is a future optimization (would need to wire an `AVBufferRef` into
227/// `AVPacket.buf` directly via `av_packet_alloc` + manual buffer set).
228/// PTS / DTS / duration / flags / stream_index are propagated.
229///
230/// Returns `Err(ffmpeg_next::Error)` on:
231/// * payload larger than `c_int::MAX` (would overflow `AVPacket.size`);
232/// * `av_new_packet` allocation failure (OOM).
233pub fn ffmpeg_packet_from_video_packet(
234  packet: &mediadecode::packet::VideoPacket<VideoPacketExtra, FfmpegBuffer>,
235) -> std::result::Result<Packet, ffmpeg_next::Error> {
236  let mut out = try_packet_copy(packet.data().as_ref())?;
237  if let Some(ts) = packet.pts() {
238    out.set_pts(Some(ts.pts()));
239  }
240  if let Some(ts) = packet.dts() {
241    out.set_dts(Some(ts.pts()));
242  }
243  if let Some(d) = packet.duration() {
244    out.set_duration(d.pts());
245  }
246  out.set_flags(map_md_flags_to_av(packet.flags()));
247  out.set_stream(packet.extra().stream_index() as usize);
248  Ok(out)
249}
250
251/// Builds an `ffmpeg::Packet` from a [`mediadecode::AudioPacket`].
252/// Same shape as [`ffmpeg_packet_from_video_packet`] — bytes are
253/// copied; pts/dts/duration/flags/stream_index are forwarded. Same
254/// failure modes.
255pub fn ffmpeg_packet_from_audio_packet(
256  packet: &mediadecode::packet::AudioPacket<AudioPacketExtra, FfmpegBuffer>,
257) -> std::result::Result<Packet, ffmpeg_next::Error> {
258  let mut out = try_packet_copy(packet.data().as_ref())?;
259  if let Some(ts) = packet.pts() {
260    out.set_pts(Some(ts.pts()));
261  }
262  if let Some(ts) = packet.dts() {
263    out.set_dts(Some(ts.pts()));
264  }
265  if let Some(d) = packet.duration() {
266    out.set_duration(d.pts());
267  }
268  out.set_flags(map_md_flags_to_av(packet.flags()));
269  out.set_stream(packet.extra().stream_index() as usize);
270  Ok(out)
271}
272
273/// Builds an `ffmpeg::Packet` from a [`mediadecode::SubtitlePacket`].
274/// Bytes copied; pts/duration/flags/stream_index forwarded. Subtitle
275/// packets have no `dts` in the mediadecode model. Same failure
276/// modes as [`ffmpeg_packet_from_video_packet`].
277pub fn ffmpeg_packet_from_subtitle_packet(
278  packet: &mediadecode::packet::SubtitlePacket<SubtitlePacketExtra, FfmpegBuffer>,
279) -> std::result::Result<Packet, ffmpeg_next::Error> {
280  let mut out = try_packet_copy(packet.data().as_ref())?;
281  if let Some(ts) = packet.pts() {
282    out.set_pts(Some(ts.pts()));
283  }
284  if let Some(d) = packet.duration() {
285    out.set_duration(d.pts());
286  }
287  out.set_flags(map_md_flags_to_av(packet.flags()));
288  out.set_stream(packet.extra().stream_index() as usize);
289  Ok(out)
290}
291
292// ---------------------------------------------------------------------------
293//  Safe wrappers — `&ffmpeg::Packet` → `mediadecode::*Packet`.
294// ---------------------------------------------------------------------------
295
296/// Wraps a borrowed [`ffmpeg::Packet`] as a
297/// [`mediadecode::packet::VideoPacket`]. The compressed payload is
298/// shared with the source `AVPacket` via refcount bump (no copy).
299/// Timestamps, duration, key/corrupt flags, and the source stream
300/// index are forwarded to the produced packet.
301///
302/// Returns `None` when the source packet has no buffer attached
303/// (empty packet — typical after EOF). Caller can also fill in
304/// [`VideoPacketExtra::byte_pos`] / `side_data` post-construction
305/// if they need those.
306pub fn video_packet_from_ffmpeg(
307  packet: &Packet,
308) -> Option<VideoPacket<VideoPacketExtra, FfmpegBuffer>> {
309  let buf = FfmpegBuffer::from_packet(packet)?;
310  let mut out = VideoPacket::new(buf, VideoPacketExtra::new(packet.stream() as i32))
311    .with_flags(md_flags_from_av(packet.flags()));
312  if let Some(p) = packet.pts() {
313    out = out.with_pts(Some(Timestamp::new(p, mediadecode::Timebase::default())));
314  }
315  if let Some(d) = packet.dts() {
316    out = out.with_dts(Some(Timestamp::new(d, mediadecode::Timebase::default())));
317  }
318  let dur = packet.duration();
319  if dur > 0 {
320    out = out.with_duration(Some(Timestamp::new(dur, mediadecode::Timebase::default())));
321  }
322  Some(out)
323}
324
325/// Wraps a borrowed [`ffmpeg::Packet`] as a
326/// [`mediadecode::packet::AudioPacket`]. Same shape as
327/// [`video_packet_from_ffmpeg`] — refcounted payload, forwarded
328/// metadata.
329pub fn audio_packet_from_ffmpeg(
330  packet: &Packet,
331) -> Option<AudioPacket<AudioPacketExtra, FfmpegBuffer>> {
332  let buf = FfmpegBuffer::from_packet(packet)?;
333  let mut out = AudioPacket::new(buf, AudioPacketExtra::new(packet.stream() as i32))
334    .with_flags(md_flags_from_av(packet.flags()));
335  if let Some(p) = packet.pts() {
336    out = out.with_pts(Some(Timestamp::new(p, mediadecode::Timebase::default())));
337  }
338  if let Some(d) = packet.dts() {
339    out = out.with_dts(Some(Timestamp::new(d, mediadecode::Timebase::default())));
340  }
341  let dur = packet.duration();
342  if dur > 0 {
343    out = out.with_duration(Some(Timestamp::new(dur, mediadecode::Timebase::default())));
344  }
345  Some(out)
346}
347
348/// Wraps a borrowed [`ffmpeg::Packet`] as a
349/// [`mediadecode::packet::SubtitlePacket`]. Subtitle packets have no
350/// `dts` in the mediadecode model; everything else mirrors
351/// [`video_packet_from_ffmpeg`].
352pub fn subtitle_packet_from_ffmpeg(
353  packet: &Packet,
354) -> Option<SubtitlePacket<SubtitlePacketExtra, FfmpegBuffer>> {
355  let buf = FfmpegBuffer::from_packet(packet)?;
356  let mut out = SubtitlePacket::new(buf, SubtitlePacketExtra::new(packet.stream() as i32))
357    .with_flags(md_flags_from_av(packet.flags()));
358  if let Some(p) = packet.pts() {
359    out = out.with_pts(Some(Timestamp::new(p, mediadecode::Timebase::default())));
360  }
361  let dur = packet.duration();
362  if dur > 0 {
363    out = out.with_duration(Some(Timestamp::new(dur, mediadecode::Timebase::default())));
364  }
365  Some(out)
366}
367
368fn md_flags_from_av(flags: ffmpeg_next::packet::Flags) -> MdPacketFlags {
369  let mut out = MdPacketFlags::empty();
370  if flags.contains(ffmpeg_next::packet::Flags::KEY) {
371    out |= MdPacketFlags::KEY;
372  }
373  if flags.contains(ffmpeg_next::packet::Flags::CORRUPT) {
374    out |= MdPacketFlags::CORRUPT;
375  }
376  out
377}
378
379// ---------------------------------------------------------------------------
380//  Empty-frame placeholders for `receive_frame` destinations.
381// ---------------------------------------------------------------------------
382
383/// Constructs an empty [`mediadecode::frame::VideoFrame`] suitable as
384/// the destination argument to
385/// [`mediadecode::decoder::VideoStreamDecoder::receive_frame`]. The
386/// decoder overwrites the frame on success; this just provides a
387/// well-formed slot.
388///
389/// All four plane slots get a 1-byte `FfmpegBuffer` placeholder
390/// (the array shape requires a buffer in every slot, but
391/// `plane_count = 0` reports them as inactive).
392///
393/// # Panics
394///
395/// Panics on FFmpeg-side OOM (the per-plane 1-byte allocation
396/// failed). Callers who need to recover from OOM should use
397/// [`try_empty_video_frame`].
398pub fn empty_video_frame() -> VideoFrame<PixelFormat, VideoFrameExtra, FfmpegBuffer> {
399  try_empty_video_frame().expect("empty_video_frame: av_buffer_alloc returned null (OOM)")
400}
401
402/// Fallible counterpart to [`empty_video_frame`]. Returns `None` if
403/// any of the four placeholder allocations fails.
404pub fn try_empty_video_frame() -> Option<VideoFrame<PixelFormat, VideoFrameExtra, FfmpegBuffer>> {
405  let planes = [
406    Plane::new(FfmpegBuffer::try_empty()?, 0),
407    Plane::new(FfmpegBuffer::try_empty()?, 0),
408    Plane::new(FfmpegBuffer::try_empty()?, 0),
409    Plane::new(FfmpegBuffer::try_empty()?, 0),
410  ];
411  Some(VideoFrame::new(
412    Dimensions::new(0, 0),
413    PixelFormat::Unknown(0),
414    planes,
415    0,
416    VideoFrameExtra::default(),
417  ))
418}
419
420/// Constructs an empty [`mediadecode::frame::AudioFrame`] suitable as
421/// the destination argument to
422/// [`mediadecode::decoder::AudioStreamDecoder::receive_frame`]. Same
423/// behaviour as [`empty_video_frame`] — eight 1-byte plane
424/// placeholders, `plane_count = 0`.
425///
426/// # Panics
427///
428/// Panics on FFmpeg-side OOM. See [`try_empty_audio_frame`] for the
429/// fallible variant.
430pub fn empty_audio_frame()
431-> AudioFrame<SampleFormat, AudioChannelLayout, AudioFrameExtra, FfmpegBuffer> {
432  try_empty_audio_frame().expect("empty_audio_frame: av_buffer_alloc returned null (OOM)")
433}
434
435/// Fallible counterpart to [`empty_audio_frame`]. Returns `None` if
436/// any of the eight placeholder allocations fails.
437pub fn try_empty_audio_frame()
438-> Option<AudioFrame<SampleFormat, AudioChannelLayout, AudioFrameExtra, FfmpegBuffer>> {
439  let planes = [
440    Plane::new(FfmpegBuffer::try_empty()?, 0),
441    Plane::new(FfmpegBuffer::try_empty()?, 0),
442    Plane::new(FfmpegBuffer::try_empty()?, 0),
443    Plane::new(FfmpegBuffer::try_empty()?, 0),
444    Plane::new(FfmpegBuffer::try_empty()?, 0),
445    Plane::new(FfmpegBuffer::try_empty()?, 0),
446    Plane::new(FfmpegBuffer::try_empty()?, 0),
447    Plane::new(FfmpegBuffer::try_empty()?, 0),
448  ];
449  Some(AudioFrame::new(
450    0,
451    0,
452    0,
453    SampleFormat::NONE,
454    AudioChannelLayout::default(),
455    planes,
456    0,
457    AudioFrameExtra::default(),
458  ))
459}
460
461/// Constructs an empty [`mediadecode::frame::SubtitleFrame`] suitable
462/// as the destination argument to
463/// [`mediadecode::decoder::SubtitleDecoder::receive_frame`]. The
464/// payload is an empty `Text` placeholder; the decoder overwrites
465/// it on success.
466///
467/// # Panics
468///
469/// Panics on FFmpeg-side OOM. See [`try_empty_subtitle_frame`] for
470/// the fallible variant.
471pub fn empty_subtitle_frame() -> SubtitleFrame<SubtitleFrameExtra, FfmpegBuffer> {
472  try_empty_subtitle_frame().expect("empty_subtitle_frame: av_buffer_alloc returned null (OOM)")
473}
474
475/// Fallible counterpart to [`empty_subtitle_frame`]. Returns `None`
476/// if the placeholder allocation fails.
477pub fn try_empty_subtitle_frame() -> Option<SubtitleFrame<SubtitleFrameExtra, FfmpegBuffer>> {
478  let buf = FfmpegBuffer::copy_from_slice(&[]).or_else(FfmpegBuffer::try_empty)?;
479  Some(SubtitleFrame::new(
480    SubtitlePayload::Text {
481      text: buf,
482      language: None,
483    },
484    SubtitleFrameExtra::default(),
485  ))
486}
487
488#[cfg(test)]
489mod tests {
490  use super::*;
491
492  #[test]
493  fn nv12_round_trips() {
494    assert_eq!(
495      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_NV12 as i32),
496      PixelFormat::Nv12,
497    );
498  }
499
500  #[test]
501  fn p010be_maps_to_p010be() {
502    // BE must map to the BE variant — the previous "fold to LE"
503    // mapping silently corrupted P010BE pixel data via the safe
504    // export path. The unsupported-format gate in `convert::av_frame_to_video_frame`
505    // is the right place to reject BE today.
506    assert_eq!(
507      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_P010BE as i32),
508      PixelFormat::P010Be,
509    );
510  }
511
512  #[test]
513  fn unknown_for_garbage_value() {
514    assert!(matches!(
515      from_av_pixel_format(-99_999),
516      PixelFormat::Unknown(_)
517    ));
518  }
519
520  #[test]
521  fn hw_formats_detected() {
522    assert!(is_hardware_pix_fmt(
523      AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32
524    ));
525    assert!(is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_VAAPI as i32));
526    assert!(is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_CUDA as i32));
527    assert!(is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_D3D11 as i32));
528  }
529
530  #[test]
531  fn cpu_formats_not_detected_as_hw() {
532    assert!(!is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_NV12 as i32));
533    assert!(!is_hardware_pix_fmt(
534      AVPixelFormat::AV_PIX_FMT_YUV420P as i32
535    ));
536    assert!(!is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_NONE as i32));
537  }
538
539  #[test]
540  fn hw_formats_map_to_unknown_in_pixel_format() {
541    // HW sentinels intentionally don't have a mediadecode::PixelFormat
542    // representation — they're not CPU pixel data.
543    assert!(matches!(
544      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32),
545      PixelFormat::Unknown(_)
546    ));
547    assert!(matches!(
548      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_VAAPI as i32),
549      PixelFormat::Unknown(_)
550    ));
551  }
552}