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::{
10  ffi::c_int,
11  ptr::{addr_of, read_unaligned},
12};
13
14use derive_more::{IsVariant, TryUnwrap, Unwrap};
15use ffmpeg_next::{Packet, ffi::AVPixelFormat};
16use mediadecode::{
17  PixelFormat, Timestamp,
18  demuxer::{AttachmentPacket, DataPacket},
19  frame::{AudioFrame, Dimensions, Plane, SubtitleFrame, VideoFrame},
20  packet::{AudioPacket, PacketFlags as MdPacketFlags, SubtitlePacket, VideoPacket},
21  subtitle::{SubtitlePayload, Text as SubtitleText},
22};
23use mediaframe::audio::ChannelLayoutDescription;
24
25use crate::{
26  FfmpegBuffer,
27  // `buffer::SideDataAlloc` is aliased: this module also defines its own
28  // `PacketBuildError::SideDataAlloc` payload (the write-side failure —
29  // FFmpeg refusing an allocation while rebuilding an `AVPacket`'s side
30  // data), which keeps the bare name since it is native to this file.
31  // `BufferSideDataAlloc` is `PacketBufferError`'s read-side counterpart
32  // (out of memory copying a side-data entry *out of* an `AVPacket`) —
33  // same short name, different struct, different direction.
34  buffer::{
35    PacketBufferError, Refcount, SideDataAlloc as BufferSideDataAlloc, SideDataArray,
36    SideDataBytes, SideDataEntries, SideDataPayload, UnrepresentableFlags,
37  },
38  convert::{SIDE_DATA_MAX_ENTRIES, SIDE_DATA_MAX_TOTAL_BYTES},
39  extras::{
40    AttachmentPacketExtra, AudioFrameExtra, AudioPacketExtra, DataPacketExtra, SideDataEntry,
41    SubtitleFrameExtra, SubtitlePacketExtra, VideoFrameExtra, VideoPacketExtra,
42  },
43  sample_format::SampleFormat,
44};
45
46/// Maps a raw `AVFrame.format` integer (i.e. the value of an
47/// `AVPixelFormat` enum variant) onto [`mediadecode::PixelFormat`].
48///
49/// Returns [`PixelFormat::None`] for raw integers we don't have a
50/// mapping for — including `AV_PIX_FMT_NONE` itself and the
51/// hardware-frame markers (`AV_PIX_FMT_VIDEOTOOLBOX` / `_VAAPI` /
52/// `_CUDA` / `_D3D11` / …), since those never describe CPU-side pixel
53/// data and the unified enum intentionally doesn't carry them. Use
54/// [`is_hardware_pix_fmt`] to identify HW frames before transferring
55/// to a CPU format.
56///
57/// mediaframe 0.3 struck `PixelFormat::Unknown(u32)`, so the raw
58/// integer no longer rides along in the returned value; the caller's
59/// own `raw` is the place it survives. Every consumer in this crate
60/// already treats the fall-through as "not a deliverable CPU format"
61/// (`pixdesc::to_av_pixel_format`, `is_supported_cpu_pix_fmt` and the
62/// geometry tables all reject it), so the rejection is unchanged.
63///
64/// The match never constructs an `AVPixelFormat` from a runtime
65/// value; it compares the input against `AVPixelFormat::AV_PIX_FMT_X
66/// as i32` constants. Sound regardless of which discriminant set the
67/// linked FFmpeg version exposes.
68pub const fn from_av_pixel_format(raw: i32) -> PixelFormat {
69  // Mirrors `crate::pixdesc::to_av_pixel_format` arm-for-arm (its
70  // inverse). Every deliverable CPU format plus the non-deliverable
71  // formats `to_av` still resolves a constant for (monochrome / PAL /
72  // sub-byte-packed RGB / Bayer) is mapped here, so a frame's raw
73  // `format` integer always lands on the same `PixelFormat` the round
74  // trip would produce. Deliverability (HWACCEL / BAYER / PAL /
75  // BITSTREAM rejection) is enforced separately by
76  // `pixdesc::is_deliverable` / the convert layer — this boundary is
77  // identity-only.
78  //
79  // BE-tagged formats map to mediadecode's distinct `*Be` variants
80  // (never folded onto the LE canonical). Folding BE onto LE silently
81  // corrupted pixel data: each >8-bit sample is byte-swapped between
82  // BE and LE, and the convert path exports the AVBufferRef bytes
83  // verbatim with no endian conversion, so a consumer reading a
84  // BE-tagged frame's planes as LE samples would see every sample
85  // byte-reversed. Mapping to the `*Be` variant keeps the format
86  // distinct so the convert layer can handle (or reject) it correctly.
87  //
88  // The match never constructs an `AVPixelFormat` from a runtime
89  // value; it compares the input against `AVPixelFormat::AV_PIX_FMT_X
90  // as i32` constants. Sound regardless of which discriminant set the
91  // linked FFmpeg version exposes.
92  match raw {
93    // Planar YUV 8-bit.
94    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P as i32 => PixelFormat::Yuv420p,
95    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P as i32 => PixelFormat::Yuv422p,
96    x if x == AVPixelFormat::AV_PIX_FMT_YUV440P as i32 => PixelFormat::Yuv440p,
97    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P as i32 => PixelFormat::Yuv444p,
98    x if x == AVPixelFormat::AV_PIX_FMT_YUV411P as i32 => PixelFormat::Yuv411p,
99    x if x == AVPixelFormat::AV_PIX_FMT_YUV410P as i32 => PixelFormat::Yuv410p,
100    // Deprecated JPEG-range planar YUV (yuvj-family).
101    x if x == AVPixelFormat::AV_PIX_FMT_YUVJ411P as i32 => PixelFormat::Yuvj411p,
102    x if x == AVPixelFormat::AV_PIX_FMT_YUVJ420P as i32 => PixelFormat::Yuvj420p,
103    x if x == AVPixelFormat::AV_PIX_FMT_YUVJ422P as i32 => PixelFormat::Yuvj422p,
104    x if x == AVPixelFormat::AV_PIX_FMT_YUVJ440P as i32 => PixelFormat::Yuvj440p,
105    x if x == AVPixelFormat::AV_PIX_FMT_YUVJ444P as i32 => PixelFormat::Yuvj444p,
106    // Planar YUV 4:2:0 high-bit.
107    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P9LE as i32 => PixelFormat::Yuv420p9Le,
108    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P9BE as i32 => PixelFormat::Yuv420p9Be,
109    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P10LE as i32 => PixelFormat::Yuv420p10Le,
110    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P10BE as i32 => PixelFormat::Yuv420p10Be,
111    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P12LE as i32 => PixelFormat::Yuv420p12Le,
112    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P12BE as i32 => PixelFormat::Yuv420p12Be,
113    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P14LE as i32 => PixelFormat::Yuv420p14Le,
114    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P14BE as i32 => PixelFormat::Yuv420p14Be,
115    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P16LE as i32 => PixelFormat::Yuv420p16Le,
116    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P16BE as i32 => PixelFormat::Yuv420p16Be,
117    // Planar YUV 4:2:2 high-bit.
118    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P9LE as i32 => PixelFormat::Yuv422p9Le,
119    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P9BE as i32 => PixelFormat::Yuv422p9Be,
120    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P10LE as i32 => PixelFormat::Yuv422p10Le,
121    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P10BE as i32 => PixelFormat::Yuv422p10Be,
122    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P12LE as i32 => PixelFormat::Yuv422p12Le,
123    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P12BE as i32 => PixelFormat::Yuv422p12Be,
124    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P14LE as i32 => PixelFormat::Yuv422p14Le,
125    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P14BE as i32 => PixelFormat::Yuv422p14Be,
126    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P16LE as i32 => PixelFormat::Yuv422p16Le,
127    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P16BE as i32 => PixelFormat::Yuv422p16Be,
128    // Planar YUV 4:4:0 high-bit.
129    x if x == AVPixelFormat::AV_PIX_FMT_YUV440P10LE as i32 => PixelFormat::Yuv440p10Le,
130    x if x == AVPixelFormat::AV_PIX_FMT_YUV440P10BE as i32 => PixelFormat::Yuv440p10Be,
131    x if x == AVPixelFormat::AV_PIX_FMT_YUV440P12LE as i32 => PixelFormat::Yuv440p12Le,
132    x if x == AVPixelFormat::AV_PIX_FMT_YUV440P12BE as i32 => PixelFormat::Yuv440p12Be,
133    // Planar YUV 4:4:4 high-bit.
134    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P9LE as i32 => PixelFormat::Yuv444p9Le,
135    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P9BE as i32 => PixelFormat::Yuv444p9Be,
136    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P10LE as i32 => PixelFormat::Yuv444p10Le,
137    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P10BE as i32 => PixelFormat::Yuv444p10Be,
138    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P12LE as i32 => PixelFormat::Yuv444p12Le,
139    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P12BE as i32 => PixelFormat::Yuv444p12Be,
140    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P14LE as i32 => PixelFormat::Yuv444p14Le,
141    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P14BE as i32 => PixelFormat::Yuv444p14Be,
142    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P16LE as i32 => PixelFormat::Yuv444p16Le,
143    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P16BE as i32 => PixelFormat::Yuv444p16Be,
144    // MSB-packed YUV 4:4:4.
145    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P10MSBLE as i32 => PixelFormat::Yuv444p10MsbLe,
146    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P10MSBBE as i32 => PixelFormat::Yuv444p10MsbBe,
147    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P12MSBLE as i32 => PixelFormat::Yuv444p12MsbLe,
148    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P12MSBBE as i32 => PixelFormat::Yuv444p12MsbBe,
149    // Planar YUVA.
150    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P as i32 => PixelFormat::Yuva420p,
151    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P as i32 => PixelFormat::Yuva422p,
152    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P as i32 => PixelFormat::Yuva444p,
153    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P9LE as i32 => PixelFormat::Yuva420p9Le,
154    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P9BE as i32 => PixelFormat::Yuva420p9Be,
155    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P9LE as i32 => PixelFormat::Yuva422p9Le,
156    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P9BE as i32 => PixelFormat::Yuva422p9Be,
157    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P9LE as i32 => PixelFormat::Yuva444p9Le,
158    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P9BE as i32 => PixelFormat::Yuva444p9Be,
159    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P10LE as i32 => PixelFormat::Yuva420p10Le,
160    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P10BE as i32 => PixelFormat::Yuva420p10Be,
161    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P10LE as i32 => PixelFormat::Yuva422p10Le,
162    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P10BE as i32 => PixelFormat::Yuva422p10Be,
163    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P10LE as i32 => PixelFormat::Yuva444p10Le,
164    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P10BE as i32 => PixelFormat::Yuva444p10Be,
165    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P12LE as i32 => PixelFormat::Yuva422p12Le,
166    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P12BE as i32 => PixelFormat::Yuva422p12Be,
167    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P12LE as i32 => PixelFormat::Yuva444p12Le,
168    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P12BE as i32 => PixelFormat::Yuva444p12Be,
169    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P16LE as i32 => PixelFormat::Yuva420p16Le,
170    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P16BE as i32 => PixelFormat::Yuva420p16Be,
171    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P16LE as i32 => PixelFormat::Yuva422p16Le,
172    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P16BE as i32 => PixelFormat::Yuva422p16Be,
173    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P16LE as i32 => PixelFormat::Yuva444p16Le,
174    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P16BE as i32 => PixelFormat::Yuva444p16Be,
175    // Semi-planar YUV 8-bit.
176    x if x == AVPixelFormat::AV_PIX_FMT_NV12 as i32 => PixelFormat::Nv12,
177    x if x == AVPixelFormat::AV_PIX_FMT_NV21 as i32 => PixelFormat::Nv21,
178    x if x == AVPixelFormat::AV_PIX_FMT_NV16 as i32 => PixelFormat::Nv16,
179    x if x == AVPixelFormat::AV_PIX_FMT_NV24 as i32 => PixelFormat::Nv24,
180    x if x == AVPixelFormat::AV_PIX_FMT_NV42 as i32 => PixelFormat::Nv42,
181    x if x == AVPixelFormat::AV_PIX_FMT_NV20LE as i32 => PixelFormat::Nv20Le,
182    x if x == AVPixelFormat::AV_PIX_FMT_NV20BE as i32 => PixelFormat::Nv20Be,
183    // Semi-planar YUV high-bit.
184    x if x == AVPixelFormat::AV_PIX_FMT_P010LE as i32 => PixelFormat::P010Le,
185    x if x == AVPixelFormat::AV_PIX_FMT_P010BE as i32 => PixelFormat::P010Be,
186    x if x == AVPixelFormat::AV_PIX_FMT_P012LE as i32 => PixelFormat::P012Le,
187    x if x == AVPixelFormat::AV_PIX_FMT_P012BE as i32 => PixelFormat::P012Be,
188    x if x == AVPixelFormat::AV_PIX_FMT_P016LE as i32 => PixelFormat::P016Le,
189    x if x == AVPixelFormat::AV_PIX_FMT_P016BE as i32 => PixelFormat::P016Be,
190    x if x == AVPixelFormat::AV_PIX_FMT_P210LE as i32 => PixelFormat::P210Le,
191    x if x == AVPixelFormat::AV_PIX_FMT_P210BE as i32 => PixelFormat::P210Be,
192    x if x == AVPixelFormat::AV_PIX_FMT_P212LE as i32 => PixelFormat::P212Le,
193    x if x == AVPixelFormat::AV_PIX_FMT_P212BE as i32 => PixelFormat::P212Be,
194    x if x == AVPixelFormat::AV_PIX_FMT_P216LE as i32 => PixelFormat::P216Le,
195    x if x == AVPixelFormat::AV_PIX_FMT_P216BE as i32 => PixelFormat::P216Be,
196    x if x == AVPixelFormat::AV_PIX_FMT_P410LE as i32 => PixelFormat::P410Le,
197    x if x == AVPixelFormat::AV_PIX_FMT_P410BE as i32 => PixelFormat::P410Be,
198    x if x == AVPixelFormat::AV_PIX_FMT_P412LE as i32 => PixelFormat::P412Le,
199    x if x == AVPixelFormat::AV_PIX_FMT_P412BE as i32 => PixelFormat::P412Be,
200    x if x == AVPixelFormat::AV_PIX_FMT_P416LE as i32 => PixelFormat::P416Le,
201    x if x == AVPixelFormat::AV_PIX_FMT_P416BE as i32 => PixelFormat::P416Be,
202    // Packed YUV 8-bit.
203    x if x == AVPixelFormat::AV_PIX_FMT_YUYV422 as i32 => PixelFormat::Yuyv422,
204    x if x == AVPixelFormat::AV_PIX_FMT_UYVY422 as i32 => PixelFormat::Uyvy422,
205    x if x == AVPixelFormat::AV_PIX_FMT_YVYU422 as i32 => PixelFormat::Yvyu422,
206    x if x == AVPixelFormat::AV_PIX_FMT_UYYVYY411 as i32 => PixelFormat::Uyyvyy411,
207    // Packed YUV high-bit.
208    x if x == AVPixelFormat::AV_PIX_FMT_Y210LE as i32 => PixelFormat::Y210Le,
209    x if x == AVPixelFormat::AV_PIX_FMT_Y210BE as i32 => PixelFormat::Y210Be,
210    x if x == AVPixelFormat::AV_PIX_FMT_Y212LE as i32 => PixelFormat::Y212Le,
211    x if x == AVPixelFormat::AV_PIX_FMT_Y212BE as i32 => PixelFormat::Y212Be,
212    x if x == AVPixelFormat::AV_PIX_FMT_Y216LE as i32 => PixelFormat::Y216Le,
213    x if x == AVPixelFormat::AV_PIX_FMT_Y216BE as i32 => PixelFormat::Y216Be,
214    x if x == AVPixelFormat::AV_PIX_FMT_XV30LE as i32 => PixelFormat::Xv30Le,
215    x if x == AVPixelFormat::AV_PIX_FMT_XV30BE as i32 => PixelFormat::Xv30Be,
216    x if x == AVPixelFormat::AV_PIX_FMT_V30XLE as i32 => PixelFormat::V30xLe,
217    x if x == AVPixelFormat::AV_PIX_FMT_V30XBE as i32 => PixelFormat::V30xBe,
218    x if x == AVPixelFormat::AV_PIX_FMT_XV36LE as i32 => PixelFormat::Xv36Le,
219    x if x == AVPixelFormat::AV_PIX_FMT_XV36BE as i32 => PixelFormat::Xv36Be,
220    x if x == AVPixelFormat::AV_PIX_FMT_XV48LE as i32 => PixelFormat::Xv48Le,
221    x if x == AVPixelFormat::AV_PIX_FMT_XV48BE as i32 => PixelFormat::Xv48Be,
222    x if x == AVPixelFormat::AV_PIX_FMT_VUYA as i32 => PixelFormat::Vuya,
223    x if x == AVPixelFormat::AV_PIX_FMT_VUYX as i32 => PixelFormat::Vuyx,
224    x if x == AVPixelFormat::AV_PIX_FMT_AYUV as i32 => PixelFormat::Ayuv,
225    x if x == AVPixelFormat::AV_PIX_FMT_AYUV64LE as i32 => PixelFormat::Ayuv64Le,
226    x if x == AVPixelFormat::AV_PIX_FMT_AYUV64BE as i32 => PixelFormat::Ayuv64Be,
227    x if x == AVPixelFormat::AV_PIX_FMT_UYVA as i32 => PixelFormat::Uyva,
228    x if x == AVPixelFormat::AV_PIX_FMT_VYU444 as i32 => PixelFormat::Vyu444,
229    // XYZ.
230    x if x == AVPixelFormat::AV_PIX_FMT_XYZ12LE as i32 => PixelFormat::Xyz12Le,
231    x if x == AVPixelFormat::AV_PIX_FMT_XYZ12BE as i32 => PixelFormat::Xyz12Be,
232    // Packed RGB 8-bit.
233    x if x == AVPixelFormat::AV_PIX_FMT_RGB24 as i32 => PixelFormat::Rgb24,
234    x if x == AVPixelFormat::AV_PIX_FMT_BGR24 as i32 => PixelFormat::Bgr24,
235    x if x == AVPixelFormat::AV_PIX_FMT_RGBA as i32 => PixelFormat::Rgba,
236    x if x == AVPixelFormat::AV_PIX_FMT_BGRA as i32 => PixelFormat::Bgra,
237    x if x == AVPixelFormat::AV_PIX_FMT_ARGB as i32 => PixelFormat::Argb,
238    x if x == AVPixelFormat::AV_PIX_FMT_ABGR as i32 => PixelFormat::Abgr,
239    x if x == AVPixelFormat::AV_PIX_FMT_RGB0 as i32 => PixelFormat::Rgbx,
240    x if x == AVPixelFormat::AV_PIX_FMT_BGR0 as i32 => PixelFormat::Bgrx,
241    x if x == AVPixelFormat::AV_PIX_FMT_0RGB as i32 => PixelFormat::Xrgb,
242    x if x == AVPixelFormat::AV_PIX_FMT_0BGR as i32 => PixelFormat::Xbgr,
243    x if x == AVPixelFormat::AV_PIX_FMT_X2RGB10LE as i32 => PixelFormat::X2Rgb10Le,
244    x if x == AVPixelFormat::AV_PIX_FMT_X2RGB10BE as i32 => PixelFormat::X2Rgb10Be,
245    x if x == AVPixelFormat::AV_PIX_FMT_X2BGR10LE as i32 => PixelFormat::X2Bgr10Le,
246    x if x == AVPixelFormat::AV_PIX_FMT_X2BGR10BE as i32 => PixelFormat::X2Bgr10Be,
247    // Gbr24p shares AV_PIX_FMT_GBRP's discriminant; mapped to Gbrp above.
248    // Packed RGB high-bit.
249    x if x == AVPixelFormat::AV_PIX_FMT_RGB48LE as i32 => PixelFormat::Rgb48Le,
250    x if x == AVPixelFormat::AV_PIX_FMT_RGB48BE as i32 => PixelFormat::Rgb48Be,
251    x if x == AVPixelFormat::AV_PIX_FMT_BGR48LE as i32 => PixelFormat::Bgr48Le,
252    x if x == AVPixelFormat::AV_PIX_FMT_BGR48BE as i32 => PixelFormat::Bgr48Be,
253    x if x == AVPixelFormat::AV_PIX_FMT_RGBA64LE as i32 => PixelFormat::Rgba64Le,
254    x if x == AVPixelFormat::AV_PIX_FMT_RGBA64BE as i32 => PixelFormat::Rgba64Be,
255    x if x == AVPixelFormat::AV_PIX_FMT_BGRA64LE as i32 => PixelFormat::Bgra64Le,
256    x if x == AVPixelFormat::AV_PIX_FMT_BGRA64BE as i32 => PixelFormat::Bgra64Be,
257    x if x == AVPixelFormat::AV_PIX_FMT_RGB96LE as i32 => PixelFormat::Rgb96Le,
258    x if x == AVPixelFormat::AV_PIX_FMT_RGB96BE as i32 => PixelFormat::Rgb96Be,
259    x if x == AVPixelFormat::AV_PIX_FMT_RGBA128LE as i32 => PixelFormat::Rgba128Le,
260    x if x == AVPixelFormat::AV_PIX_FMT_RGBA128BE as i32 => PixelFormat::Rgba128Be,
261    // Packed RGB float / half-float.
262    x if x == AVPixelFormat::AV_PIX_FMT_RGBF16LE as i32 => PixelFormat::Rgbf16Le,
263    x if x == AVPixelFormat::AV_PIX_FMT_RGBF16BE as i32 => PixelFormat::Rgbf16Be,
264    x if x == AVPixelFormat::AV_PIX_FMT_RGBF32LE as i32 => PixelFormat::Rgbf32Le,
265    x if x == AVPixelFormat::AV_PIX_FMT_RGBF32BE as i32 => PixelFormat::Rgbf32Be,
266    x if x == AVPixelFormat::AV_PIX_FMT_RGBAF16LE as i32 => PixelFormat::Rgbaf16Le,
267    x if x == AVPixelFormat::AV_PIX_FMT_RGBAF16BE as i32 => PixelFormat::Rgbaf16Be,
268    x if x == AVPixelFormat::AV_PIX_FMT_RGBAF32LE as i32 => PixelFormat::Rgbaf32Le,
269    x if x == AVPixelFormat::AV_PIX_FMT_RGBAF32BE as i32 => PixelFormat::Rgbaf32Be,
270    // Planar GBR.
271    x if x == AVPixelFormat::AV_PIX_FMT_GBRP as i32 => PixelFormat::Gbrp,
272    x if x == AVPixelFormat::AV_PIX_FMT_GBRP9LE as i32 => PixelFormat::Gbrp9Le,
273    x if x == AVPixelFormat::AV_PIX_FMT_GBRP9BE as i32 => PixelFormat::Gbrp9Be,
274    x if x == AVPixelFormat::AV_PIX_FMT_GBRP10LE as i32 => PixelFormat::Gbrp10Le,
275    x if x == AVPixelFormat::AV_PIX_FMT_GBRP10BE as i32 => PixelFormat::Gbrp10Be,
276    x if x == AVPixelFormat::AV_PIX_FMT_GBRP10MSBLE as i32 => PixelFormat::Gbrp10MsbLe,
277    x if x == AVPixelFormat::AV_PIX_FMT_GBRP10MSBBE as i32 => PixelFormat::Gbrp10MsbBe,
278    x if x == AVPixelFormat::AV_PIX_FMT_GBRP12LE as i32 => PixelFormat::Gbrp12Le,
279    x if x == AVPixelFormat::AV_PIX_FMT_GBRP12BE as i32 => PixelFormat::Gbrp12Be,
280    x if x == AVPixelFormat::AV_PIX_FMT_GBRP12MSBLE as i32 => PixelFormat::Gbrp12MsbLe,
281    x if x == AVPixelFormat::AV_PIX_FMT_GBRP12MSBBE as i32 => PixelFormat::Gbrp12MsbBe,
282    x if x == AVPixelFormat::AV_PIX_FMT_GBRP14LE as i32 => PixelFormat::Gbrp14Le,
283    x if x == AVPixelFormat::AV_PIX_FMT_GBRP14BE as i32 => PixelFormat::Gbrp14Be,
284    x if x == AVPixelFormat::AV_PIX_FMT_GBRP16LE as i32 => PixelFormat::Gbrp16Le,
285    x if x == AVPixelFormat::AV_PIX_FMT_GBRP16BE as i32 => PixelFormat::Gbrp16Be,
286    x if x == AVPixelFormat::AV_PIX_FMT_GBRPF16LE as i32 => PixelFormat::Gbrpf16Le,
287    x if x == AVPixelFormat::AV_PIX_FMT_GBRPF16BE as i32 => PixelFormat::Gbrpf16Be,
288    x if x == AVPixelFormat::AV_PIX_FMT_GBRPF32LE as i32 => PixelFormat::Gbrpf32Le,
289    x if x == AVPixelFormat::AV_PIX_FMT_GBRPF32BE as i32 => PixelFormat::Gbrpf32Be,
290    // Planar GBRA.
291    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP as i32 => PixelFormat::Gbrap,
292    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP10LE as i32 => PixelFormat::Gbrap10Le,
293    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP10BE as i32 => PixelFormat::Gbrap10Be,
294    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP12LE as i32 => PixelFormat::Gbrap12Le,
295    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP12BE as i32 => PixelFormat::Gbrap12Be,
296    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP14LE as i32 => PixelFormat::Gbrap14Le,
297    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP14BE as i32 => PixelFormat::Gbrap14Be,
298    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP16LE as i32 => PixelFormat::Gbrap16Le,
299    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP16BE as i32 => PixelFormat::Gbrap16Be,
300    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP32LE as i32 => PixelFormat::Gbrap32Le,
301    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP32BE as i32 => PixelFormat::Gbrap32Be,
302    x if x == AVPixelFormat::AV_PIX_FMT_GBRAPF16LE as i32 => PixelFormat::Gbrapf16Le,
303    x if x == AVPixelFormat::AV_PIX_FMT_GBRAPF16BE as i32 => PixelFormat::Gbrapf16Be,
304    x if x == AVPixelFormat::AV_PIX_FMT_GBRAPF32LE as i32 => PixelFormat::Gbrapf32Le,
305    x if x == AVPixelFormat::AV_PIX_FMT_GBRAPF32BE as i32 => PixelFormat::Gbrapf32Be,
306    // Greyscale.
307    x if x == AVPixelFormat::AV_PIX_FMT_GRAY8 as i32 => PixelFormat::Gray8,
308    x if x == AVPixelFormat::AV_PIX_FMT_GRAY9LE as i32 => PixelFormat::Gray9Le,
309    x if x == AVPixelFormat::AV_PIX_FMT_GRAY9BE as i32 => PixelFormat::Gray9Be,
310    x if x == AVPixelFormat::AV_PIX_FMT_GRAY10LE as i32 => PixelFormat::Gray10Le,
311    x if x == AVPixelFormat::AV_PIX_FMT_GRAY10BE as i32 => PixelFormat::Gray10Be,
312    x if x == AVPixelFormat::AV_PIX_FMT_GRAY12LE as i32 => PixelFormat::Gray12Le,
313    x if x == AVPixelFormat::AV_PIX_FMT_GRAY12BE as i32 => PixelFormat::Gray12Be,
314    x if x == AVPixelFormat::AV_PIX_FMT_GRAY14LE as i32 => PixelFormat::Gray14Le,
315    x if x == AVPixelFormat::AV_PIX_FMT_GRAY14BE as i32 => PixelFormat::Gray14Be,
316    x if x == AVPixelFormat::AV_PIX_FMT_GRAY16LE as i32 => PixelFormat::Gray16Le,
317    x if x == AVPixelFormat::AV_PIX_FMT_GRAY16BE as i32 => PixelFormat::Gray16Be,
318    x if x == AVPixelFormat::AV_PIX_FMT_GRAY32LE as i32 => PixelFormat::Gray32Le,
319    x if x == AVPixelFormat::AV_PIX_FMT_GRAY32BE as i32 => PixelFormat::Gray32Be,
320    x if x == AVPixelFormat::AV_PIX_FMT_GRAYF16LE as i32 => PixelFormat::Grayf16Le,
321    x if x == AVPixelFormat::AV_PIX_FMT_GRAYF16BE as i32 => PixelFormat::Grayf16Be,
322    x if x == AVPixelFormat::AV_PIX_FMT_GRAYF32LE as i32 => PixelFormat::Grayf32Le,
323    x if x == AVPixelFormat::AV_PIX_FMT_GRAYF32BE as i32 => PixelFormat::Grayf32Be,
324    x if x == AVPixelFormat::AV_PIX_FMT_YA8 as i32 => PixelFormat::Ya8,
325    x if x == AVPixelFormat::AV_PIX_FMT_YA16LE as i32 => PixelFormat::Ya16Le,
326    x if x == AVPixelFormat::AV_PIX_FMT_YA16BE as i32 => PixelFormat::Ya16Be,
327    x if x == AVPixelFormat::AV_PIX_FMT_YAF16LE as i32 => PixelFormat::Yaf16Le,
328    x if x == AVPixelFormat::AV_PIX_FMT_YAF16BE as i32 => PixelFormat::Yaf16Be,
329    x if x == AVPixelFormat::AV_PIX_FMT_YAF32LE as i32 => PixelFormat::Yaf32Le,
330    x if x == AVPixelFormat::AV_PIX_FMT_YAF32BE as i32 => PixelFormat::Yaf32Be,
331    x if x == AVPixelFormat::AV_PIX_FMT_MONOWHITE as i32 => PixelFormat::Monowhite,
332    x if x == AVPixelFormat::AV_PIX_FMT_MONOBLACK as i32 => PixelFormat::Monoblack,
333    x if x == AVPixelFormat::AV_PIX_FMT_PAL8 as i32 => PixelFormat::Pal8,
334    x if x == AVPixelFormat::AV_PIX_FMT_RGB4 as i32 => PixelFormat::Rgb4,
335    x if x == AVPixelFormat::AV_PIX_FMT_RGB4_BYTE as i32 => PixelFormat::Rgb4Byte,
336    x if x == AVPixelFormat::AV_PIX_FMT_RGB8 as i32 => PixelFormat::Rgb8,
337    x if x == AVPixelFormat::AV_PIX_FMT_BGR4 as i32 => PixelFormat::Bgr4,
338    x if x == AVPixelFormat::AV_PIX_FMT_BGR4_BYTE as i32 => PixelFormat::Bgr4Byte,
339    x if x == AVPixelFormat::AV_PIX_FMT_BGR8 as i32 => PixelFormat::Bgr8,
340    x if x == AVPixelFormat::AV_PIX_FMT_RGB444LE as i32 => PixelFormat::Rgb444Le,
341    x if x == AVPixelFormat::AV_PIX_FMT_RGB444BE as i32 => PixelFormat::Rgb444Be,
342    x if x == AVPixelFormat::AV_PIX_FMT_BGR444LE as i32 => PixelFormat::Bgr444Le,
343    x if x == AVPixelFormat::AV_PIX_FMT_BGR444BE as i32 => PixelFormat::Bgr444Be,
344    x if x == AVPixelFormat::AV_PIX_FMT_RGB555LE as i32 => PixelFormat::Rgb555Le,
345    x if x == AVPixelFormat::AV_PIX_FMT_RGB555BE as i32 => PixelFormat::Rgb555Be,
346    x if x == AVPixelFormat::AV_PIX_FMT_BGR555LE as i32 => PixelFormat::Bgr555Le,
347    x if x == AVPixelFormat::AV_PIX_FMT_BGR555BE as i32 => PixelFormat::Bgr555Be,
348    x if x == AVPixelFormat::AV_PIX_FMT_RGB565LE as i32 => PixelFormat::Rgb565Le,
349    x if x == AVPixelFormat::AV_PIX_FMT_RGB565BE as i32 => PixelFormat::Rgb565Be,
350    x if x == AVPixelFormat::AV_PIX_FMT_BGR565LE as i32 => PixelFormat::Bgr565Le,
351    x if x == AVPixelFormat::AV_PIX_FMT_BGR565BE as i32 => PixelFormat::Bgr565Be,
352    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_BGGR8 as i32 => PixelFormat::BayerBggr8,
353    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_RGGB8 as i32 => PixelFormat::BayerRggb8,
354    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GBRG8 as i32 => PixelFormat::BayerGbrg8,
355    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GRBG8 as i32 => PixelFormat::BayerGrbg8,
356    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_BGGR16LE as i32 => PixelFormat::BayerBggr16Le,
357    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_BGGR16BE as i32 => PixelFormat::BayerBggr16Be,
358    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_RGGB16LE as i32 => PixelFormat::BayerRggb16Le,
359    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_RGGB16BE as i32 => PixelFormat::BayerRggb16Be,
360    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GBRG16LE as i32 => PixelFormat::BayerGbrg16Le,
361    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GBRG16BE as i32 => PixelFormat::BayerGbrg16Be,
362    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GRBG16LE as i32 => PixelFormat::BayerGrbg16Le,
363    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GRBG16BE as i32 => PixelFormat::BayerGrbg16Be,
364    _ => PixelFormat::None,
365  }
366}
367
368/// Returns `true` when `raw` is one of FFmpeg's hardware-frame markers
369/// (`AV_PIX_FMT_VIDEOTOOLBOX` / `_VAAPI` / `_CUDA` / `_D3D11` /
370/// `_DRM_PRIME` / `_MEDIACODEC` / `_VULKAN`). Used by the HW probe to
371/// identify GPU-resident frames before triggering
372/// `av_hwframe_transfer_data`.
373pub const fn is_hardware_pix_fmt(raw: i32) -> bool {
374  matches!(
375    raw,
376    x if x == AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32
377      || x == AVPixelFormat::AV_PIX_FMT_VAAPI as i32
378      || x == AVPixelFormat::AV_PIX_FMT_CUDA as i32
379      || x == AVPixelFormat::AV_PIX_FMT_D3D11 as i32
380      || x == AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32
381      || x == AVPixelFormat::AV_PIX_FMT_MEDIACODEC as i32
382      || x == AVPixelFormat::AV_PIX_FMT_VULKAN as i32
383  )
384}
385
386/// Fallible counterpart to ffmpeg-next's `Packet::copy`.
387///
388/// The upstream helper calls `Packet::new(size)` (which silently
389/// truncates `size` to `c_int` and ignores `av_new_packet`'s return
390/// code) and then panics via `data_mut().unwrap().write_all(...).unwrap()`
391/// if the allocation failed. From a safe public decoder API we want
392/// the OOM / oversized-payload paths to surface as
393/// `ffmpeg_next::Error` rather than aborting the process — every
394/// `send_packet` path goes through this helper.
395///
396/// Failure modes:
397/// * payload larger than `c_int::MAX` (would overflow `AVPacket.size`)
398///   → `ffmpeg_next::Error::Other { errno: libc::EINVAL }`.
399/// * `av_new_packet` allocation failure (signalled by `data_mut()`
400///   returning `None`) → `ffmpeg_next::Error::Other { errno:
401///   libc::ENOMEM }`.
402fn try_packet_copy(data: &[u8]) -> std::result::Result<Packet, ffmpeg_next::Error> {
403  // FFmpeg's `AVPacket.size` is `c_int`. A payload larger than that
404  // can't fit in a single packet — refuse rather than truncate via
405  // `as c_int` inside `Packet::new`.
406  if data.len() > c_int::MAX as usize {
407    return Err(ffmpeg_next::Error::Other {
408      errno: libc::EINVAL,
409    });
410  }
411  // `Packet::new(size)` calls `av_new_packet(&mut pkt, size as
412  // c_int)` and ignores the return code; on OOM it returns a
413  // `Packet` whose `.data` is null. We detect that via
414  // `data_mut()` (returns `None` on null) and copy via
415  // `copy_nonoverlapping` so we never go through `data_mut()
416  // .unwrap().write_all().unwrap()` — the upstream `Packet::copy`'s
417  // double panic.
418  let mut pkt = Packet::new(data.len());
419  match pkt.data_mut() {
420    Some(slot) if slot.len() == data.len() => {
421      // SAFETY: `slot` is a `&mut [u8]` of `data.len()` bytes;
422      // `data` is a `&[u8]` of the same length. Non-overlapping
423      // because `slot` is a fresh allocation.
424      if !data.is_empty() {
425        unsafe {
426          core::ptr::copy_nonoverlapping(data.as_ptr(), slot.as_mut_ptr(), data.len());
427        }
428      }
429      Ok(pkt)
430    }
431    _ => Err(ffmpeg_next::Error::Other {
432      errno: libc::ENOMEM,
433    }),
434  }
435}
436
437/// Writes every flag the portable packet carries onto the `AVPacket`
438/// being rebuilt.
439///
440/// Not through `Packet::set_flags`, which takes `ffmpeg_next`'s `Flags`
441/// and so can only spell `KEY` and `CORRUPT`: the bits are written to
442/// `AVPacket.flags` directly, so `DISCARD` — and anything else the
443/// forward direction retained — reaches the decoder that has to obey
444/// it. `PacketFlags` is a `u8` bit set and the field is a `c_int`, so
445/// the widening is total and nothing needs deciding here.
446///
447/// # Safety
448///
449/// `packet` must own a live `AVPacket`.
450unsafe fn write_md_flags(packet: &mut Packet, flags: MdPacketFlags) {
451  use ffmpeg_next::packet::Mut;
452  unsafe {
453    (*packet.as_mut_ptr()).flags = c_int::from(flags.bits());
454  }
455}
456
457/// Payload for [`PacketBuildError::UnknownSideData`].
458///
459/// A side-data entry whose type this build of FFmpeg does not name.
460///
461/// Refused rather than dropped: this crate carries side-data types as
462/// the raw integers they are on the wire, and handing an unknown one
463/// to C would either form an invalid enum discriminant or attach a
464/// type nothing downstream can read. Everything the demuxer captured
465/// came from this same build and is in range, so this only answers a
466/// hand-built entry.
467#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
468#[error("side-data type {kind} is not one this FFmpeg build names (0..{limit})")]
469pub struct UnknownSideData {
470  kind: i32,
471  limit: i32,
472}
473
474impl UnknownSideData {
475  /// Constructs an `UnknownSideData` payload.
476  #[cfg_attr(not(tarpaulin), inline(always))]
477  pub const fn new(kind: i32, limit: i32) -> Self {
478    Self { kind, limit }
479  }
480  /// The type integer the entry carried.
481  #[cfg_attr(not(tarpaulin), inline(always))]
482  pub const fn kind(&self) -> i32 {
483    self.kind
484  }
485  /// How many side-data types this build names.
486  #[cfg_attr(not(tarpaulin), inline(always))]
487  pub const fn limit(&self) -> i32 {
488    self.limit
489  }
490}
491
492/// Payload for [`PacketBuildError::SideDataAlloc`].
493///
494/// FFmpeg refused the side-data allocation.
495#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
496#[error("out of memory attaching {size} bytes of side data of type {kind}")]
497pub struct SideDataAlloc {
498  kind: i32,
499  size: usize,
500}
501
502impl SideDataAlloc {
503  /// Constructs a `SideDataAlloc` payload.
504  #[cfg_attr(not(tarpaulin), inline(always))]
505  pub const fn new(kind: i32, size: usize) -> Self {
506    Self { kind, size }
507  }
508  /// The entry's type integer.
509  #[cfg_attr(not(tarpaulin), inline(always))]
510  pub const fn kind(&self) -> i32 {
511    self.kind
512  }
513  /// The entry's payload length.
514  #[cfg_attr(not(tarpaulin), inline(always))]
515  pub const fn size(&self) -> usize {
516    self.size
517  }
518}
519
520/// Why a portable packet could not be rebuilt as an `AVPacket`.
521///
522/// The reverse direction is what feeds a decoder, so everything the
523/// forward direction captured has to survive it or the capture was
524/// theatre.
525#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error, IsVariant, Unwrap, TryUnwrap)]
526#[unwrap(ref, ref_mut)]
527#[try_unwrap(ref, ref_mut)]
528pub enum PacketBuildError {
529  /// The packet body could not be allocated or is larger than
530  /// `AVPacket.size` can hold.
531  #[error(transparent)]
532  Ffmpeg(#[from] ffmpeg_next::Error),
533
534  /// A side-data entry whose type this build of FFmpeg does not name.
535  #[error(transparent)]
536  UnknownSideData(#[from] UnknownSideData),
537
538  /// FFmpeg refused the side-data allocation.
539  #[error(transparent)]
540  SideDataAlloc(#[from] SideDataAlloc),
541}
542
543/// Copies `entries` onto an `AVPacket` under construction.
544///
545/// A decoder learns things only this way: `AV_PKT_DATA_NEW_EXTRADATA`
546/// replaces its parameters mid-stream, `AV_PKT_DATA_PARAM_CHANGE` moves
547/// its rate or layout, `AV_PKT_DATA_SKIP_SAMPLES` is the encoder-delay
548/// trim without which a gapless stream is not gapless. Rebuilding a
549/// packet without them hands the decoder a body and a lie.
550fn attach_side_data(out: &mut Packet, entries: &[SideDataEntry]) -> Result<(), PacketBuildError> {
551  use ffmpeg_next::packet::Mut;
552  for entry in entries {
553    let kind = entry.kind();
554    let size = entry.data().len();
555    // SAFETY: `out` owns a live `AVPacket`; `packet_new_side_data`
556    // validates the type against this build's own range before handing
557    // it to C and reports a failed allocation as `None`.
558    let slot = unsafe { crate::ffi::packet_new_side_data(out.as_mut_ptr(), kind, size) };
559    let Some(slot) = slot else {
560      let limit = crate::ffi::side_data_type_count();
561      return Err(if kind < 0 || kind >= limit {
562        PacketBuildError::UnknownSideData(UnknownSideData::new(kind, limit))
563      } else {
564        PacketBuildError::SideDataAlloc(SideDataAlloc::new(kind, size))
565      });
566    };
567    if size > 0 {
568      // SAFETY: FFmpeg just allocated `size` bytes at `slot` (plus its
569      // padding), and `entry.data()` is a `&[u8]` of exactly that
570      // length; the two regions belong to different allocations.
571      unsafe { core::ptr::copy_nonoverlapping(entry.data().as_ptr(), slot, size) };
572    }
573  }
574  Ok(())
575}
576
577/// Builds an `ffmpeg::Packet` from a [`mediadecode::VideoPacket`]
578/// parameterized by [`crate::extras::VideoPacketExtra`] and
579/// [`crate::FfmpegBuffer`].
580///
581/// The compressed bytes are **copied** into a new packet allocation —
582/// zero-copy passthrough of the FfmpegBuffer's underlying AVBufferRef
583/// is a future optimization (would need to wire an `AVBufferRef` into
584/// `AVPacket.buf` directly via `av_packet_alloc` + manual buffer set).
585/// PTS / DTS / duration / flags / stream_index are propagated.
586///
587/// Side data on the extras is reattached to the rebuilt packet — see
588/// [`attach_side_data`] for why that is not optional.
589///
590/// Returns [`PacketBuildError`] on:
591/// * payload larger than `c_int::MAX` (would overflow `AVPacket.size`);
592/// * `av_new_packet` allocation failure (OOM);
593/// * a side-data entry this build of FFmpeg cannot name, or one whose
594///   allocation failed.
595pub fn ffmpeg_packet_from_video_packet(
596  packet: &mediadecode::packet::VideoPacket<VideoPacketExtra, FfmpegBuffer>,
597) -> std::result::Result<Packet, PacketBuildError> {
598  let mut out = try_packet_copy(packet.data().as_ref())?;
599  attach_side_data(&mut out, packet.extra().side_data())?;
600  if let Some(ts) = packet.pts() {
601    out.set_pts(Some(ts.pts()));
602  }
603  if let Some(ts) = packet.dts() {
604    out.set_dts(Some(ts.pts()));
605  }
606  if let Some(d) = packet.duration() {
607    out.set_duration(d.pts());
608  }
609  // SAFETY: `out` owns the `AVPacket` `try_packet_copy` just built.
610  unsafe { write_md_flags(&mut out, packet.flags()) };
611  out.set_stream(packet.extra().stream_index() as usize);
612  Ok(out)
613}
614
615/// Builds an `ffmpeg::Packet` from a [`mediadecode::AudioPacket`].
616/// Same shape as [`ffmpeg_packet_from_video_packet`] — bytes are
617/// copied; pts/dts/duration/flags/stream_index and side data are
618/// forwarded. Same failure modes.
619pub fn ffmpeg_packet_from_audio_packet(
620  packet: &mediadecode::packet::AudioPacket<AudioPacketExtra, FfmpegBuffer>,
621) -> std::result::Result<Packet, PacketBuildError> {
622  let mut out = try_packet_copy(packet.data().as_ref())?;
623  attach_side_data(&mut out, packet.extra().side_data())?;
624  if let Some(ts) = packet.pts() {
625    out.set_pts(Some(ts.pts()));
626  }
627  if let Some(ts) = packet.dts() {
628    out.set_dts(Some(ts.pts()));
629  }
630  if let Some(d) = packet.duration() {
631    out.set_duration(d.pts());
632  }
633  // SAFETY: `out` owns the `AVPacket` `try_packet_copy` just built.
634  unsafe { write_md_flags(&mut out, packet.flags()) };
635  out.set_stream(packet.extra().stream_index() as usize);
636  Ok(out)
637}
638
639/// Builds an `ffmpeg::Packet` from a [`mediadecode::SubtitlePacket`].
640/// Bytes copied; pts/duration/flags/stream_index and side data
641/// forwarded. Subtitle packets have no `dts` in the mediadecode model.
642/// Same failure modes as [`ffmpeg_packet_from_video_packet`].
643pub fn ffmpeg_packet_from_subtitle_packet(
644  packet: &mediadecode::packet::SubtitlePacket<SubtitlePacketExtra, FfmpegBuffer>,
645) -> std::result::Result<Packet, PacketBuildError> {
646  let mut out = try_packet_copy(packet.data().as_ref())?;
647  attach_side_data(&mut out, packet.extra().side_data())?;
648  if let Some(ts) = packet.pts() {
649    out.set_pts(Some(ts.pts()));
650  }
651  if let Some(d) = packet.duration() {
652    out.set_duration(d.pts());
653  }
654  // SAFETY: `out` owns the `AVPacket` `try_packet_copy` just built.
655  unsafe { write_md_flags(&mut out, packet.flags()) };
656  out.set_stream(packet.extra().stream_index() as usize);
657  Ok(out)
658}
659
660// ---------------------------------------------------------------------------
661//  Safe wrappers — `&ffmpeg::Packet` → `mediadecode::*Packet`.
662// ---------------------------------------------------------------------------
663
664/// Wraps a borrowed [`ffmpeg::Packet`] as a
665/// [`mediadecode::packet::VideoPacket`]. The compressed payload is
666/// shared with the source `AVPacket` via refcount bump (no copy).
667/// Timestamps, duration, key/corrupt flags, and the source stream
668/// index are forwarded to the produced packet.
669///
670/// Returns `Ok(None)` when the source packet has no payload at all
671/// (an empty packet — typical after EOF), and
672/// [`PacketBufferError`] when a payload that *is* there could not be
673/// referenced: the two are never the same answer. Caller can also fill
674/// in [`VideoPacketExtra::byte_pos`] / `side_data` post-construction if
675/// they need those.
676pub fn video_packet_from_ffmpeg(
677  packet: &Packet,
678) -> Result<Option<VideoPacket<VideoPacketExtra, FfmpegBuffer>>, PacketBufferError> {
679  video_packet_from_ffmpeg_in(packet, mediadecode::Timebase::default())
680}
681
682/// Wraps a borrowed [`ffmpeg::Packet`] as a
683/// [`mediadecode::packet::AudioPacket`]. Same shape as
684/// [`video_packet_from_ffmpeg`] — refcounted payload, forwarded
685/// metadata.
686pub fn audio_packet_from_ffmpeg(
687  packet: &Packet,
688) -> Result<Option<AudioPacket<AudioPacketExtra, FfmpegBuffer>>, PacketBufferError> {
689  audio_packet_from_ffmpeg_in(packet, mediadecode::Timebase::default())
690}
691
692/// Wraps a borrowed [`ffmpeg::Packet`] as a
693/// [`mediadecode::packet::SubtitlePacket`]. Subtitle packets have no
694/// `dts` in the mediadecode model; everything else mirrors
695/// [`video_packet_from_ffmpeg`].
696pub fn subtitle_packet_from_ffmpeg(
697  packet: &Packet,
698) -> Result<Option<SubtitlePacket<SubtitlePacketExtra, FfmpegBuffer>>, PacketBufferError> {
699  subtitle_packet_from_ffmpeg_in(packet, mediadecode::Timebase::default())
700}
701
702/// The most side-data entries this crate will walk on one packet.
703///
704/// The floor is [`SIDE_DATA_MAX_ENTRIES`], the same bound the
705/// frame-side collector uses; it rises with `AV_PKT_DATA_NB` so a
706/// future FFmpeg that names more side-data types than the floor cannot
707/// turn a legitimate packet into a refusal. Measured: FFmpeg's own
708/// packet API cannot exceed one entry per named type — both
709/// `av_packet_new_side_data` and `av_packet_add_side_data` replace an
710/// existing entry of the same type — so a packet over this cap is one
711/// no FFmpeg call produced.
712fn side_data_entry_cap() -> usize {
713  SIDE_DATA_MAX_ENTRIES.max(crate::ffi::side_data_type_count().max(0) as usize)
714}
715
716/// The side-data entries an `AVPacket` carries, copied into owned
717/// values — **all of them, or none and an error**.
718///
719/// The packet twin of `convert::collect_side_data`, and bounded the
720/// same way: at most [`side_data_entry_cap`] entries and
721/// [`SIDE_DATA_MAX_TOTAL_BYTES`] bytes per packet, allocated through
722/// `try_reserve_exact`. What is *not* the same is what happens when a
723/// bound is reached. The frame collector truncates and warns, which it
724/// can afford to — frame side data is descriptive, the frame is
725/// delivered either way, and nothing downstream acts on it. Packet side
726/// data is the opposite: `NEW_EXTRADATA` replaces a decoder's
727/// parameters, `PARAM_CHANGE` moves its rate, `SKIP_SAMPLES` trims the
728/// stream, and the codec acts on every one. A truncated copy is a
729/// decoder quietly running on stale parameters, and a truncated copy of
730/// a side-data-only packet is `Ok(None)` — the packet vanishing
731/// entirely, which is the very defect this seam was built to close. So
732/// every bound here is an error, and a caller either gets a packet with
733/// all of its side data or a `DemuxError` naming what stopped it.
734///
735/// `AVPacket.side_data` is a flat array of `AVPacketSideData` (not the
736/// array of pointers an `AVFrame` keeps), and its `type_` is read as
737/// the integer it is on the wire — a discriminant this build has no
738/// name for would be undefined behaviour the moment it existed as an
739/// `AVPacketSideDataType`.
740fn packet_side_data(packet: &Packet) -> Result<Vec<SideDataEntry>, PacketBufferError> {
741  use ffmpeg_next::packet::Ref;
742  // SAFETY: `packet` keeps the `AVPacket` live; `side_data` and
743  // `side_data_elems` are public fields.
744  let count_raw = unsafe { (*packet.as_ptr()).side_data_elems };
745  let entries = unsafe { (*packet.as_ptr()).side_data };
746  // Zero entries is the only shape that means "no side data". Every
747  // other reading of that answer — a malformed count, a missing array —
748  // is judged, and judged *before* the pointer: a count this crate
749  // cannot walk stays an error whether or not the array happens to be
750  // null, and a null array with entries to read is malformed rather
751  // than empty. Both used to leave here as `Ok(vec![])`, which is the
752  // silent loss the caps taught us to name, reached through the
753  // pointer instead of the budget.
754  if count_raw == 0 {
755    return Ok(Vec::new());
756  }
757  let cap = side_data_entry_cap();
758  if count_raw < 0 || count_raw as usize > cap {
759    return Err(PacketBufferError::SideDataEntries(SideDataEntries::new(
760      count_raw, cap,
761    )));
762  }
763  if entries.is_null() {
764    return Err(PacketBufferError::SideDataArray(SideDataArray::new(
765      count_raw,
766    )));
767  }
768  let count = count_raw as usize;
769  let mut out: Vec<SideDataEntry> = Vec::new();
770  if out.try_reserve_exact(count).is_err() {
771    return Err(PacketBufferError::SideDataAlloc(BufferSideDataAlloc::new(
772      count * core::mem::size_of::<SideDataEntry>(),
773    )));
774  }
775  let mut total_bytes: usize = 0;
776  for index in 0..count {
777    // SAFETY: `entries` is valid for `count_raw` contiguous
778    // `AVPacketSideData` values per FFmpeg's contract, and `index` is
779    // below that count.
780    let entry = unsafe { entries.add(index) };
781    let kind = unsafe { read_unaligned(addr_of!((*entry).type_).cast::<i32>()) };
782    let size = unsafe { (*entry).size };
783    let data_ptr = unsafe { (*entry).data };
784    let data = if size == 0 {
785      // A marker entry: a type and no bytes. FFmpeg emits these, and
786      // there is nothing to carry or to charge the budget for.
787      Vec::new()
788    } else if data_ptr.is_null() {
789      // Bytes declared and not carried. Reading this as an empty entry
790      // delivered a packet whose side data was a lie, and charged the
791      // budget nothing for it.
792      return Err(PacketBufferError::SideDataPayload(SideDataPayload::new(
793        index, size,
794      )));
795    } else {
796      total_bytes = total_bytes.saturating_add(size);
797      if total_bytes > SIDE_DATA_MAX_TOTAL_BYTES {
798        return Err(PacketBufferError::SideDataBytes(SideDataBytes::new(
799          total_bytes,
800          SIDE_DATA_MAX_TOTAL_BYTES,
801        )));
802      }
803      let mut buf: Vec<u8> = Vec::new();
804      if buf.try_reserve_exact(size).is_err() {
805        return Err(PacketBufferError::SideDataAlloc(BufferSideDataAlloc::new(
806          size,
807        )));
808      }
809      // SAFETY: `data` is valid for `size` bytes per FFmpeg's
810      // `AVPacketSideData` contract.
811      buf.extend_from_slice(unsafe { core::slice::from_raw_parts(data_ptr, size) });
812      buf
813    };
814    out.push(SideDataEntry::new(kind, data));
815  }
816  Ok(out)
817}
818
819/// The buffer a timed packet is delivered with, or `None` when there is
820/// no packet to deliver at all.
821///
822/// **`size == 0` is not the same as "nothing".** A packet with no body
823/// and one or more side-data entries is a real packet: FFmpeg uses that
824/// shape for `AV_PKT_DATA_NEW_EXTRADATA` and for a parameter change,
825/// and a decoder that never sees it keeps decoding on parameters the
826/// container has already replaced. Such a packet is delivered with an
827/// owned empty buffer — zero bytes, but a buffer, so the packet exists
828/// and its side data rides the extras.
829///
830/// `Ok(None)` therefore means the packet carried neither a payload nor
831/// side data: the empty marker, and the only thing a pull loop may skip.
832fn delivered_payload(
833  packet: &Packet,
834  side_data: &[SideDataEntry],
835) -> Result<Option<FfmpegBuffer>, PacketBufferError> {
836  if let Some(buf) = FfmpegBuffer::from_packet(packet)? {
837    return Ok(Some(buf));
838  }
839  if side_data.is_empty() {
840    return Ok(None);
841  }
842  FfmpegBuffer::copy_from_slice(&[])
843    .map(Some)
844    .ok_or(PacketBufferError::Refcount(Refcount::new(0)))
845}
846
847// ---------------------------------------------------------------------------
848//  Timebase-carrying variants.
849//
850//  An `AVPacket`'s timestamps are integers in its *stream's* timebase,
851//  which the packet does not carry — the four functions above therefore
852//  stamp `Timebase::default()` (1/1), leaving the caller to know what
853//  the ticks meant. A demuxer knows: it holds the track table. These
854//  variants take that timebase, so the produced `Timestamp` is a
855//  complete, self-describing value.
856// ---------------------------------------------------------------------------
857
858/// [`video_packet_from_ffmpeg`], with the stream's timebase stamped
859/// onto every timestamp instead of the 1/1 placeholder.
860pub fn video_packet_from_ffmpeg_in(
861  packet: &Packet,
862  time_base: mediadecode::Timebase,
863) -> Result<Option<VideoPacket<VideoPacketExtra, FfmpegBuffer>>, PacketBufferError> {
864  let side_data = packet_side_data(packet)?;
865  let Some(buf) = delivered_payload(packet, &side_data)? else {
866    return Ok(None);
867  };
868  let extra = VideoPacketExtra::new(packet.stream() as i32).with_side_data(side_data);
869  let mut out = VideoPacket::new(buf, extra)
870    .with_flags(md_flags_from_packet(packet)?)
871    .with_pts(packet.pts().map(|p| Timestamp::new(p, time_base)))
872    .with_dts(packet.dts().map(|d| Timestamp::new(d, time_base)));
873  let dur = packet.duration();
874  if dur > 0 {
875    out = out.with_duration(Some(Timestamp::new(dur, time_base)));
876  }
877  Ok(Some(out))
878}
879
880/// [`audio_packet_from_ffmpeg`], with the stream's timebase stamped
881/// onto every timestamp instead of the 1/1 placeholder.
882pub fn audio_packet_from_ffmpeg_in(
883  packet: &Packet,
884  time_base: mediadecode::Timebase,
885) -> Result<Option<AudioPacket<AudioPacketExtra, FfmpegBuffer>>, PacketBufferError> {
886  let side_data = packet_side_data(packet)?;
887  let Some(buf) = delivered_payload(packet, &side_data)? else {
888    return Ok(None);
889  };
890  let extra = AudioPacketExtra::new(packet.stream() as i32).with_side_data(side_data);
891  let mut out = AudioPacket::new(buf, extra)
892    .with_flags(md_flags_from_packet(packet)?)
893    .with_pts(packet.pts().map(|p| Timestamp::new(p, time_base)))
894    .with_dts(packet.dts().map(|d| Timestamp::new(d, time_base)));
895  let dur = packet.duration();
896  if dur > 0 {
897    out = out.with_duration(Some(Timestamp::new(dur, time_base)));
898  }
899  Ok(Some(out))
900}
901
902/// [`subtitle_packet_from_ffmpeg`], with the stream's timebase stamped
903/// onto every timestamp instead of the 1/1 placeholder.
904pub fn subtitle_packet_from_ffmpeg_in(
905  packet: &Packet,
906  time_base: mediadecode::Timebase,
907) -> Result<Option<SubtitlePacket<SubtitlePacketExtra, FfmpegBuffer>>, PacketBufferError> {
908  let side_data = packet_side_data(packet)?;
909  let Some(buf) = delivered_payload(packet, &side_data)? else {
910    return Ok(None);
911  };
912  let extra = SubtitlePacketExtra::new(packet.stream() as i32).with_side_data(side_data);
913  let mut out = SubtitlePacket::new(buf, extra)
914    .with_flags(md_flags_from_packet(packet)?)
915    .with_pts(packet.pts().map(|p| Timestamp::new(p, time_base)));
916  let dur = packet.duration();
917  if dur > 0 {
918    out = out.with_duration(Some(Timestamp::new(dur, time_base)));
919  }
920  Ok(Some(out))
921}
922
923/// Wraps a borrowed [`ffmpeg::Packet`] from a **data** track — timecode,
924/// KLV, timed ID3 — as a [`mediadecode::demuxer::DataPacket`], with the
925/// stream's timebase stamped onto every timestamp.
926///
927/// Data packets are never reordered, so the mediadecode model gives
928/// them no `dts` seat; everything else mirrors
929/// [`video_packet_from_ffmpeg_in`]. `byte_pos` is forwarded from
930/// `AVPacket.pos`, which data consumers use to correlate a payload with
931/// its position in the file.
932pub fn data_packet_from_ffmpeg_in(
933  packet: &Packet,
934  time_base: mediadecode::Timebase,
935) -> Result<Option<DataPacket<DataPacketExtra, FfmpegBuffer>>, PacketBufferError> {
936  let side_data = packet_side_data(packet)?;
937  let Some(buf) = delivered_payload(packet, &side_data)? else {
938    return Ok(None);
939  };
940  let pos = packet.position();
941  let extra = DataPacketExtra::new(packet.stream() as i32)
942    .with_byte_pos((pos >= 0).then_some(pos as i64))
943    .with_side_data(side_data);
944  let mut out = DataPacket::new(buf, extra)
945    .with_flags(md_flags_from_packet(packet)?)
946    .with_pts(packet.pts().map(|p| Timestamp::new(p, time_base)));
947  let dur = packet.duration();
948  if dur > 0 {
949    out = out.with_duration(Some(Timestamp::new(dur, time_base)));
950  }
951  Ok(Some(out))
952}
953
954/// Wraps a borrowed [`ffmpeg::Packet`] from an **attachment** track —
955/// cover art that the container really does store as a packet — as a
956/// [`mediadecode::demuxer::AttachmentPacket`].
957///
958/// No timestamps are forwarded, and there is nowhere to put them: an
959/// attachment is not on the timeline. `synthesized` is `false`, because
960/// this payload came from a real packet; the demuxer sets it `true` for
961/// the packets it builds out of codec extradata.
962///
963/// **The one arm where a payload-less packet really is nothing.** The
964/// four timed conversions deliver a side-data-only packet, because
965/// codec-control side data is content a decoder must see. An attachment
966/// is not decoded and is not on a timeline: it *is* its bytes, so a
967/// packet carrying none carries no attachment, and this answers
968/// `Ok(None)`. `AttachmentPacketExtra` accordingly has no side-data
969/// seat — a deliberate absence, not an oversight.
970pub fn attachment_packet_from_ffmpeg(
971  packet: &Packet,
972) -> Result<Option<AttachmentPacket<AttachmentPacketExtra, FfmpegBuffer>>, PacketBufferError> {
973  let Some(buf) = FfmpegBuffer::from_packet(packet)? else {
974    return Ok(None);
975  };
976  Ok(Some(
977    AttachmentPacket::new(buf, AttachmentPacketExtra::new(packet.stream() as i32))
978      .with_flags(md_flags_from_packet(packet)?),
979  ))
980}
981
982/// Every flag the packet really carries.
983///
984/// Read from `AVPacket.flags` as the raw integer rather than through
985/// `ffmpeg_next`'s `Packet::flags()`, whose `Flags` bit set names only
986/// `KEY` and `CORRUPT` and drops the rest in `from_bits_truncate`.
987/// `AV_PKT_FLAG_DISCARD` is among the dropped: it tells a consumer that
988/// a packet must be fed to the decoder and its output thrown away, and
989/// losing it makes preroll output look like something to keep.
990///
991/// `PacketFlags` is a bit set whose documented lossless door is
992/// `from_bits_retain`, and every packet flag FFmpeg names lives inside
993/// the byte it carries — including the three bits nothing names yet.
994/// A bit outside that byte cannot be carried at all, and is refused
995/// rather than dropped; the assertion below states the fact that keeps
996/// the refusal unreachable against this build.
997fn md_flags_from_packet(packet: &Packet) -> Result<MdPacketFlags, PacketBufferError> {
998  use ffmpeg_next::packet::Ref;
999  // SAFETY: `packet` keeps the `AVPacket` live for the call, which is
1000  // all the raw reader asks.
1001  unsafe { md_flags_from_av_packet(packet.as_ptr()) }
1002}
1003
1004/// [`md_flags_from_packet`] for an `AVPacket` that has no safe wrapper
1005/// — the one libavformat embeds in an `AVStream` for cover art.
1006///
1007/// The demuxer hoists that packet by hand, and hoisting it *without*
1008/// its flags was how an attached picture arrived with none: FFmpeg
1009/// marks it `AV_PKT_FLAG_KEY`, which is the one thing a still image is
1010/// certain to be. One reader, so a second construction site cannot
1011/// quietly disagree with the five that go through the boundary.
1012///
1013/// # Safety
1014///
1015/// `pkt` must be a live `*const AVPacket` for the duration of this
1016/// call.
1017pub(crate) unsafe fn md_flags_from_av_packet(
1018  pkt: *const ffmpeg_next::ffi::AVPacket,
1019) -> Result<MdPacketFlags, PacketBufferError> {
1020  // SAFETY: `pkt` is live per the contract above; `flags` is a public
1021  // field and a plain `c_int`.
1022  let raw = unsafe { (*pkt).flags };
1023  let carried = i32::from(u8::MAX);
1024  if raw & !carried != 0 {
1025    return Err(PacketBufferError::UnrepresentableFlags(
1026      UnrepresentableFlags::new(raw),
1027    ));
1028  }
1029  Ok(MdPacketFlags::from_bits_retain(raw as u8))
1030}
1031
1032/// Every packet flag this build of FFmpeg names fits the byte
1033/// `PacketFlags` carries. If that stops being true, this fails the
1034/// build rather than letting a flag go missing at run time.
1035const _: () = {
1036  assert!(
1037    (ffmpeg_next::ffi::AV_PKT_FLAG_KEY
1038      | ffmpeg_next::ffi::AV_PKT_FLAG_CORRUPT
1039      | ffmpeg_next::ffi::AV_PKT_FLAG_DISCARD
1040      | ffmpeg_next::ffi::AV_PKT_FLAG_TRUSTED
1041      | ffmpeg_next::ffi::AV_PKT_FLAG_DISPOSABLE)
1042      <= u8::MAX as c_int,
1043    "FFmpeg names a packet flag outside the byte `PacketFlags` carries",
1044  );
1045};
1046
1047// ---------------------------------------------------------------------------
1048//  Empty-frame placeholders for `receive_frame` destinations.
1049// ---------------------------------------------------------------------------
1050
1051/// Constructs an empty [`mediadecode::frame::VideoFrame`] suitable as
1052/// the destination argument to
1053/// [`mediadecode::decoder::VideoStreamDecoder::receive_frame`]. The
1054/// decoder overwrites the frame on success; this just provides a
1055/// well-formed slot.
1056///
1057/// All four plane slots get a 1-byte `FfmpegBuffer` placeholder
1058/// (the array shape requires a buffer in every slot, but
1059/// `plane_count = 0` reports them as inactive).
1060///
1061/// # Panics
1062///
1063/// Panics on FFmpeg-side OOM (the per-plane 1-byte allocation
1064/// failed). Callers who need to recover from OOM should use
1065/// [`try_empty_video_frame`].
1066pub fn empty_video_frame() -> VideoFrame<PixelFormat, VideoFrameExtra, FfmpegBuffer> {
1067  try_empty_video_frame().expect("empty_video_frame: av_buffer_alloc returned null (OOM)")
1068}
1069
1070/// Fallible counterpart to [`empty_video_frame`]. Returns `None` if
1071/// any of the four placeholder allocations fails.
1072pub fn try_empty_video_frame() -> Option<VideoFrame<PixelFormat, VideoFrameExtra, FfmpegBuffer>> {
1073  let planes = [
1074    Plane::new(FfmpegBuffer::try_empty()?, 0),
1075    Plane::new(FfmpegBuffer::try_empty()?, 0),
1076    Plane::new(FfmpegBuffer::try_empty()?, 0),
1077    Plane::new(FfmpegBuffer::try_empty()?, 0),
1078  ];
1079  Some(VideoFrame::new(
1080    Dimensions::new(0, 0),
1081    // mediaframe 0.3's named "no format yet" member, and its
1082    // `Default` — the state a descriptor is in before a decoder has
1083    // said what it produces, which is exactly this placeholder.
1084    PixelFormat::None,
1085    planes,
1086    0,
1087    VideoFrameExtra::default(),
1088  ))
1089}
1090
1091/// Constructs an empty [`mediadecode::frame::AudioFrame`] suitable as
1092/// the destination argument to
1093/// [`mediadecode::decoder::AudioStreamDecoder::receive_frame`]. Same
1094/// behaviour as [`empty_video_frame`] — eight 1-byte plane
1095/// placeholders, `plane_count = 0`.
1096///
1097/// # Panics
1098///
1099/// Panics on FFmpeg-side OOM. See [`try_empty_audio_frame`] for the
1100/// fallible variant.
1101pub fn empty_audio_frame()
1102-> AudioFrame<SampleFormat, ChannelLayoutDescription, AudioFrameExtra, FfmpegBuffer> {
1103  try_empty_audio_frame().expect("empty_audio_frame: av_buffer_alloc returned null (OOM)")
1104}
1105
1106/// Fallible counterpart to [`empty_audio_frame`]. Returns `None` if
1107/// any of the eight placeholder allocations fails.
1108pub fn try_empty_audio_frame()
1109-> Option<AudioFrame<SampleFormat, ChannelLayoutDescription, AudioFrameExtra, FfmpegBuffer>> {
1110  let planes = [
1111    Plane::new(FfmpegBuffer::try_empty()?, 0),
1112    Plane::new(FfmpegBuffer::try_empty()?, 0),
1113    Plane::new(FfmpegBuffer::try_empty()?, 0),
1114    Plane::new(FfmpegBuffer::try_empty()?, 0),
1115    Plane::new(FfmpegBuffer::try_empty()?, 0),
1116    Plane::new(FfmpegBuffer::try_empty()?, 0),
1117    Plane::new(FfmpegBuffer::try_empty()?, 0),
1118    Plane::new(FfmpegBuffer::try_empty()?, 0),
1119  ];
1120  Some(AudioFrame::new(
1121    0,
1122    0,
1123    0,
1124    SampleFormat::NONE,
1125    ChannelLayoutDescription::default(),
1126    planes,
1127    0,
1128    AudioFrameExtra::default(),
1129  ))
1130}
1131
1132/// Constructs an empty [`mediadecode::frame::SubtitleFrame`] suitable
1133/// as the destination argument to
1134/// [`mediadecode::decoder::SubtitleDecoder::receive_frame`]. The
1135/// payload is an empty `Text` placeholder; the decoder overwrites
1136/// it on success.
1137///
1138/// # Panics
1139///
1140/// Panics on FFmpeg-side OOM. See [`try_empty_subtitle_frame`] for
1141/// the fallible variant.
1142pub fn empty_subtitle_frame() -> SubtitleFrame<SubtitleFrameExtra, FfmpegBuffer> {
1143  try_empty_subtitle_frame().expect("empty_subtitle_frame: av_buffer_alloc returned null (OOM)")
1144}
1145
1146/// Fallible counterpart to [`empty_subtitle_frame`]. Returns `None`
1147/// if the placeholder allocation fails.
1148pub fn try_empty_subtitle_frame() -> Option<SubtitleFrame<SubtitleFrameExtra, FfmpegBuffer>> {
1149  let buf = FfmpegBuffer::copy_from_slice(&[]).or_else(FfmpegBuffer::try_empty)?;
1150  Some(SubtitleFrame::new(
1151    SubtitlePayload::Text(SubtitleText::new(buf, None)),
1152    SubtitleFrameExtra::default(),
1153  ))
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158  use super::*;
1159
1160  /// A refcounted packet whose header claims a payload larger than the
1161  /// buffer behind it — the shape a malformed container, or an
1162  /// `av_packet_split_side_data` gone wrong, produces. Its payload is
1163  /// *there* and cannot be wrapped, which is exactly the case that must
1164  /// not read as "empty".
1165  fn out_of_bounds_packet() -> Packet {
1166    use ffmpeg_next::packet::Mut;
1167    let mut packet = Packet::copy(&[1u8, 2, 3, 4]);
1168    // SAFETY: `packet` owns a live `AVPacket` with a refcounted buffer;
1169    // `size` is a public field.
1170    unsafe {
1171      (*packet.as_mut_ptr()).size = 1 << 20;
1172    }
1173    packet
1174  }
1175
1176  #[test]
1177  fn a_payload_that_cannot_be_wrapped_is_an_error_on_every_arm() {
1178    // All five delivery arms plus the attachment path go through the
1179    // same wrapper. If one of them still folded the failure into
1180    // `None`, the demuxer would drop that kind of packet in silence.
1181    let forged = out_of_bounds_packet();
1182    let tb = mediadecode::Timebase::default();
1183    assert!(matches!(
1184      video_packet_from_ffmpeg_in(&forged, tb),
1185      Err(PacketBufferError::Bounds(_)),
1186    ));
1187    assert!(matches!(
1188      audio_packet_from_ffmpeg_in(&forged, tb),
1189      Err(PacketBufferError::Bounds(_)),
1190    ));
1191    assert!(matches!(
1192      subtitle_packet_from_ffmpeg_in(&forged, tb),
1193      Err(PacketBufferError::Bounds(_)),
1194    ));
1195    assert!(matches!(
1196      data_packet_from_ffmpeg_in(&forged, tb),
1197      Err(PacketBufferError::Bounds(_)),
1198    ));
1199    assert!(matches!(
1200      attachment_packet_from_ffmpeg(&forged),
1201      Err(PacketBufferError::Bounds(_)),
1202    ));
1203  }
1204
1205  /// A packet with **no body** and one side-data entry — the shape
1206  /// FFmpeg uses to hand a decoder new extradata or a parameter change.
1207  /// `size` is 0 and `buf` is null, which is exactly what used to read
1208  /// as "empty, skip it".
1209  fn side_data_only_packet() -> Packet {
1210    use ffmpeg_next::{
1211      ffi::{AVPacketSideDataType, av_packet_new_side_data},
1212      packet::Mut,
1213    };
1214    let mut packet = Packet::empty();
1215    // SAFETY: `packet` owns a live `AVPacket`; the side-data type is a
1216    // compile-time constant of this build, so no invalid discriminant
1217    // is formed. The returned pointer is valid for the four bytes just
1218    // allocated.
1219    unsafe {
1220      let ptr = av_packet_new_side_data(
1221        packet.as_mut_ptr(),
1222        AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA,
1223        4,
1224      );
1225      assert!(!ptr.is_null(), "av_packet_new_side_data");
1226      core::ptr::copy_nonoverlapping([1u8, 2, 3, 4].as_ptr(), ptr, 4);
1227    }
1228    packet
1229  }
1230
1231  const NEW_EXTRADATA: i32 =
1232    ffmpeg_next::ffi::AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA as i32;
1233
1234  use ffmpeg_next::ffi::{AVPacketSideData, AVPacketSideDataType, av_malloc};
1235
1236  /// A packet carrying one side-data entry of exactly `size` bytes,
1237  /// with a body when `body` is set.
1238  fn packet_with_side_data(size: usize, body: bool) -> Packet {
1239    use ffmpeg_next::{
1240      ffi::{AVPacketSideDataType, av_packet_new_side_data},
1241      packet::Mut,
1242    };
1243    let mut packet = if body {
1244      Packet::copy(&[1u8, 2, 3])
1245    } else {
1246      Packet::empty()
1247    };
1248    // SAFETY: `packet` owns a live `AVPacket` and the type is a
1249    // compile-time constant of this build.
1250    let ptr = unsafe {
1251      av_packet_new_side_data(
1252        packet.as_mut_ptr(),
1253        AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA,
1254        size,
1255      )
1256    };
1257    assert!(!ptr.is_null(), "av_packet_new_side_data({size})");
1258    packet
1259  }
1260
1261  /// A packet whose side-data array has been forged into a shape
1262  /// FFmpeg's own API cannot produce, and cannot free either: `Drop`
1263  /// puts the header back into a state `av_packet_free_side_data` can
1264  /// walk, so a fixture for a malformed packet cannot take the test
1265  /// process down with it.
1266  ///
1267  /// Both `av_packet_new_side_data` and `av_packet_add_side_data`
1268  /// replace an existing entry of the same type (measured: seventy
1269  /// calls leave one entry), so every shape below — an over-cap count, a
1270  /// negative count, a missing array, an entry that declares bytes it
1271  /// does not carry — is one no FFmpeg call produces. That is exactly
1272  /// what the validation is for.
1273  struct Forged {
1274    packet: Packet,
1275    freeable: i32,
1276  }
1277
1278  impl Drop for Forged {
1279    fn drop(&mut self) {
1280      use ffmpeg_next::packet::Mut;
1281      // SAFETY: `packet` owns a live `AVPacket`; putting the count back
1282      // to what the array really holds is what makes the free sound.
1283      unsafe { (*self.packet.as_mut_ptr()).side_data_elems = self.freeable };
1284    }
1285  }
1286
1287  impl Forged {
1288    /// `count` real entries of four bytes each.
1289    fn entries(count: usize, body: bool) -> Self {
1290      let mut packet = Self::carrier(body);
1291      // SAFETY: the array and every entry payload come from FFmpeg's
1292      // own allocator and are handed to the packet, which frees them in
1293      // `av_packet_free_side_data`. The carrier has no side data of its
1294      // own, so the overwrite leaks nothing.
1295      unsafe {
1296        let array = Self::array(count);
1297        for index in 0..count {
1298          let data = av_malloc(4) as *mut u8;
1299          assert!(!data.is_null(), "av_malloc");
1300          core::ptr::write_bytes(data, 7, 4);
1301          (*array.add(index)).data = data;
1302          (*array.add(index)).size = 4;
1303          (*array.add(index)).type_ = AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA;
1304        }
1305        Self::attach(&mut packet, array, count as i32);
1306      }
1307      Self {
1308        packet,
1309        freeable: count as i32,
1310      }
1311    }
1312
1313    /// A count with no array behind it at all.
1314    fn null_array(count: i32, body: bool) -> Self {
1315      let mut packet = Self::carrier(body);
1316      use ffmpeg_next::packet::Mut;
1317      // SAFETY: the packet's side data is null already; only the count
1318      // is forged, and `Drop` puts it back to zero before the free.
1319      unsafe { (*packet.as_mut_ptr()).side_data_elems = count };
1320      Self {
1321        packet,
1322        freeable: 0,
1323      }
1324    }
1325
1326    /// One entry declaring `size` bytes it does not carry.
1327    fn null_entry_data(size: usize, body: bool) -> Self {
1328      let mut packet = Self::carrier(body);
1329      // SAFETY: as in `entries`, except the payload pointer is left
1330      // null — `av_freep(&NULL)` is a no-op, so the free stays sound.
1331      unsafe {
1332        let array = Self::array(1);
1333        (*array).data = core::ptr::null_mut();
1334        (*array).size = size;
1335        (*array).type_ = AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA;
1336        Self::attach(&mut packet, array, 1);
1337      }
1338      Self {
1339        packet,
1340        freeable: 1,
1341      }
1342    }
1343
1344    /// Overrides the declared count, keeping the array intact.
1345    fn with_declared_count(mut self, count: i32) -> Self {
1346      use ffmpeg_next::packet::Mut;
1347      // SAFETY: `Drop` restores `freeable`, which still describes the
1348      // array really attached.
1349      unsafe { (*self.packet.as_mut_ptr()).side_data_elems = count };
1350      self
1351    }
1352
1353    fn carrier(body: bool) -> Packet {
1354      if body {
1355        Packet::copy(&[1u8, 2, 3])
1356      } else {
1357        Packet::empty()
1358      }
1359    }
1360
1361    /// # Safety
1362    /// The returned array is FFmpeg-allocated and uninitialised.
1363    unsafe fn array(count: usize) -> *mut AVPacketSideData {
1364      let array = unsafe { av_malloc(count * core::mem::size_of::<AVPacketSideData>()) }
1365        as *mut AVPacketSideData;
1366      assert!(!array.is_null(), "av_malloc");
1367      array
1368    }
1369
1370    /// # Safety
1371    /// `array` must hold `count` initialised entries owned by FFmpeg.
1372    unsafe fn attach(packet: &mut Packet, array: *mut AVPacketSideData, count: i32) {
1373      use ffmpeg_next::packet::Mut;
1374      unsafe {
1375        (*packet.as_mut_ptr()).side_data = array;
1376        (*packet.as_mut_ptr()).side_data_elems = count;
1377      }
1378    }
1379  }
1380
1381  /// Runs one packet through all four timed conversions, returning what
1382  /// each answered — the arms share a collector, and a fix that misses
1383  /// one of them is a fix that misses.
1384  fn every_timed_arm(packet: &Packet) -> [(&'static str, Result<bool, PacketBufferError>); 4] {
1385    let tb = mediadecode::Timebase::default();
1386    [
1387      (
1388        "video",
1389        video_packet_from_ffmpeg_in(packet, tb).map(|p| p.is_some()),
1390      ),
1391      (
1392        "audio",
1393        audio_packet_from_ffmpeg_in(packet, tb).map(|p| p.is_some()),
1394      ),
1395      (
1396        "subtitle",
1397        subtitle_packet_from_ffmpeg_in(packet, tb).map(|p| p.is_some()),
1398      ),
1399      (
1400        "data",
1401        data_packet_from_ffmpeg_in(packet, tb).map(|p| p.is_some()),
1402      ),
1403    ]
1404  }
1405
1406  #[test]
1407  fn side_data_a_packet_declares_and_does_not_carry_refuses_it() {
1408    // Two pointer paths walked straight past the all-or-error rule: a
1409    // count with no array behind it returned "no side data", and an
1410    // entry declaring bytes it did not carry became an empty entry —
1411    // charged nothing, delivered as though the packet had said so. Both
1412    // are how a body-bearing packet reached a decoder stripped of its
1413    // control data, and how a side-data-only packet became `None` and
1414    // was skipped.
1415    for body in [true, false] {
1416      let forged = Forged::null_array(3, body);
1417      for (arm, result) in every_timed_arm(&forged.packet) {
1418        match result {
1419          Err(PacketBufferError::SideDataArray(p)) => assert_eq!(p.count(), 3, "{arm}"),
1420          other => panic!("{arm} (body={body}): expected SideDataArray, got {other:?}"),
1421        }
1422      }
1423
1424      let forged = Forged::null_entry_data(64, body);
1425      for (arm, result) in every_timed_arm(&forged.packet) {
1426        match result {
1427          Err(PacketBufferError::SideDataPayload(p)) => {
1428            assert_eq!((p.index(), p.size()), (0, 64), "{arm}");
1429          }
1430          other => panic!("{arm} (body={body}): expected SideDataPayload, got {other:?}"),
1431        }
1432      }
1433
1434      // And the malformed count keeps being refused when the array is
1435      // missing too — the count is judged on its own, before the
1436      // pointer, so one cannot excuse the other.
1437      let forged = Forged::null_array(-3, body);
1438      for (arm, result) in every_timed_arm(&forged.packet) {
1439        assert!(
1440          matches!(
1441            result,
1442            Err(PacketBufferError::SideDataEntries(p)) if p.count() == -3
1443          ),
1444          "{arm} (body={body}): {result:?}",
1445        );
1446      }
1447      let forged = Forged::null_array(i32::MAX, body);
1448      for (arm, result) in every_timed_arm(&forged.packet) {
1449        assert!(
1450          matches!(result, Err(PacketBufferError::SideDataEntries(_))),
1451          "{arm} (body={body}): {result:?}",
1452        );
1453      }
1454    }
1455
1456    // A zero-size entry is a marker, not a lie: a type with no bytes is
1457    // exactly what FFmpeg emits for some side data, and it stays
1458    // welcome.
1459    let marker = Forged::null_entry_data(0, true);
1460    let packet = video_packet_from_ffmpeg_in(&marker.packet, mediadecode::Timebase::default())
1461      .expect("a marker entry is carriable")
1462      .expect("present");
1463    assert_eq!(packet.extra().side_data().len(), 1);
1464    assert!(packet.extra().side_data()[0].data().is_empty());
1465  }
1466
1467  #[test]
1468  fn side_data_that_cannot_be_carried_whole_refuses_the_packet() {
1469    let tb = mediadecode::Timebase::default();
1470
1471    // A body-bearing packet whose side data is over the byte cap. The
1472    // caps used to truncate and warn, which handed the codec a packet
1473    // that looked complete and was not: `NEW_EXTRADATA` gone, a decoder
1474    // left on parameters the container had already replaced.
1475    let oversized = packet_with_side_data(SIDE_DATA_MAX_TOTAL_BYTES + 1, true);
1476    for (arm, result) in [
1477      (
1478        "video",
1479        video_packet_from_ffmpeg_in(&oversized, tb).map(|p| p.is_some()),
1480      ),
1481      (
1482        "audio",
1483        audio_packet_from_ffmpeg_in(&oversized, tb).map(|p| p.is_some()),
1484      ),
1485      (
1486        "subtitle",
1487        subtitle_packet_from_ffmpeg_in(&oversized, tb).map(|p| p.is_some()),
1488      ),
1489      (
1490        "data",
1491        data_packet_from_ffmpeg_in(&oversized, tb).map(|p| p.is_some()),
1492      ),
1493    ] {
1494      match result {
1495        Err(PacketBufferError::SideDataBytes(p)) => {
1496          assert_eq!(p.cap(), SIDE_DATA_MAX_TOTAL_BYTES);
1497          assert!(p.bytes() > p.cap(), "{arm}");
1498        }
1499        other => panic!("{arm}: expected SideDataBytes, got {other:?}"),
1500      }
1501    }
1502
1503    // And the same packet with no body at all. This one used to
1504    // collapse twice over: the side data was dropped, the packet
1505    // therefore looked empty, and the demuxer skipped it in silence.
1506    let oversized = packet_with_side_data(SIDE_DATA_MAX_TOTAL_BYTES + 1, false);
1507    assert!(matches!(
1508      video_packet_from_ffmpeg_in(&oversized, tb).map(|p| p.is_some()),
1509      Err(PacketBufferError::SideDataBytes(_)),
1510    ));
1511  }
1512
1513  #[test]
1514  fn the_side_data_caps_are_refusals_at_the_boundary_not_before_it() {
1515    let tb = mediadecode::Timebase::default();
1516
1517    // Exactly at the byte cap: carried, whole.
1518    let at_cap = packet_with_side_data(SIDE_DATA_MAX_TOTAL_BYTES, true);
1519    let packet = video_packet_from_ffmpeg_in(&at_cap, tb)
1520      .expect("exactly at the cap is not over it")
1521      .expect("present");
1522    assert_eq!(packet.extra().side_data().len(), 1);
1523    assert_eq!(
1524      packet.extra().side_data()[0].data().len(),
1525      SIDE_DATA_MAX_TOTAL_BYTES,
1526    );
1527
1528    // One byte past: refused.
1529    let past = packet_with_side_data(SIDE_DATA_MAX_TOTAL_BYTES + 1, true);
1530    assert!(matches!(
1531      video_packet_from_ffmpeg_in(&past, tb).map(|p| p.is_some()),
1532      Err(PacketBufferError::SideDataBytes(_)),
1533    ));
1534
1535    // The entry cap, both sides of it. FFmpeg names fewer types than
1536    // the floor today, so the effective cap is the floor; if a future
1537    // build names more, this assertion moves the boundary rather than
1538    // letting the lane quietly test nothing.
1539    let cap = side_data_entry_cap();
1540    assert_eq!(cap, SIDE_DATA_MAX_ENTRIES, "the cap this lane straddles");
1541
1542    let at_cap = Forged::entries(cap, true);
1543    let packet = video_packet_from_ffmpeg_in(&at_cap.packet, tb)
1544      .expect("exactly at the cap is not over it")
1545      .expect("present");
1546    assert_eq!(packet.extra().side_data().len(), cap);
1547
1548    let past = Forged::entries(cap + 1, true);
1549    match video_packet_from_ffmpeg_in(&past.packet, tb).map(|p| p.is_some()) {
1550      Err(PacketBufferError::SideDataEntries(p)) => {
1551        assert_eq!(p.count() as usize, cap + 1);
1552        assert_eq!(p.cap(), cap);
1553      }
1554      other => panic!("expected SideDataEntries, got {other:?}"),
1555    }
1556
1557    // A negative count is malformed, not empty — reading it as "no side
1558    // data" would be the same silent loss by another route.
1559    let corrupt = Forged::entries(1, true).with_declared_count(-3);
1560    assert!(matches!(
1561      video_packet_from_ffmpeg_in(&corrupt.packet, tb).map(|p| p.is_some()),
1562      Err(PacketBufferError::SideDataEntries(p)) if p.count() == -3,
1563    ));
1564  }
1565
1566  #[test]
1567  fn every_flag_bit_survives_both_directions() {
1568    // `PacketFlags` is a bit set whose documented lossless door is
1569    // `from_bits_retain`, and both directions used to squeeze it
1570    // through `ffmpeg_next`'s `Flags`, which names `KEY` and `CORRUPT`
1571    // and truncates the rest away. `AV_PKT_FLAG_DISCARD` is the one
1572    // that matters most: it tells a consumer to decode a packet and
1573    // throw its output away, and without it preroll output looks like
1574    // something to keep.
1575    use ffmpeg_next::packet::{Mut, Ref};
1576    const DISCARD: i32 = ffmpeg_next::ffi::AV_PKT_FLAG_DISCARD;
1577    const TRUSTED: i32 = ffmpeg_next::ffi::AV_PKT_FLAG_TRUSTED;
1578    const DISPOSABLE: i32 = ffmpeg_next::ffi::AV_PKT_FLAG_DISPOSABLE;
1579    const UNNAMED: i32 = 0b0010_0000; // nothing names this bit yet
1580    let raw = ffmpeg_next::ffi::AV_PKT_FLAG_KEY | DISCARD | TRUSTED | DISPOSABLE | UNNAMED;
1581
1582    let mut packet = Packet::copy(&[1u8, 2, 3]);
1583    // SAFETY: `packet` owns a live `AVPacket`; `flags` is a public
1584    // field and this is the only way to set a bit `ffmpeg_next`'s
1585    // `Flags` cannot spell.
1586    unsafe { (*packet.as_mut_ptr()).flags = raw };
1587    assert_ne!(
1588      packet.flags().bits(),
1589      raw,
1590      "the wrapper's own accessor is what loses them",
1591    );
1592
1593    let tb = mediadecode::Timebase::default();
1594    let expected = MdPacketFlags::from_bits_retain(raw as u8);
1595
1596    let video = video_packet_from_ffmpeg_in(&packet, tb)
1597      .expect("wrappable")
1598      .expect("present");
1599    assert_eq!(video.flags(), expected);
1600    assert!(video.flags().contains(MdPacketFlags::DISCARD));
1601    let audio = audio_packet_from_ffmpeg_in(&packet, tb)
1602      .expect("wrappable")
1603      .expect("present");
1604    assert_eq!(audio.flags(), expected);
1605    let subtitle = subtitle_packet_from_ffmpeg_in(&packet, tb)
1606      .expect("wrappable")
1607      .expect("present");
1608    assert_eq!(subtitle.flags(), expected);
1609    let data = data_packet_from_ffmpeg_in(&packet, tb)
1610      .expect("wrappable")
1611      .expect("present");
1612    assert_eq!(data.flags(), expected);
1613    let attachment = attachment_packet_from_ffmpeg(&packet)
1614      .expect("wrappable")
1615      .expect("present");
1616    assert_eq!(attachment.flags(), expected);
1617
1618    // And back out again, on the three paths a decoder is fed from.
1619    for (arm, rebuilt) in [
1620      (
1621        "video",
1622        ffmpeg_packet_from_video_packet(&video).expect("rebuilt"),
1623      ),
1624      (
1625        "audio",
1626        ffmpeg_packet_from_audio_packet(&audio).expect("rebuilt"),
1627      ),
1628      (
1629        "subtitle",
1630        ffmpeg_packet_from_subtitle_packet(&subtitle).expect("rebuilt"),
1631      ),
1632    ] {
1633      // SAFETY: `rebuilt` owns a live `AVPacket`.
1634      let carried = unsafe { (*rebuilt.as_ptr()).flags };
1635      assert_eq!(carried, raw, "{arm} rebuilt {carried:#x} from {raw:#x}");
1636    }
1637  }
1638
1639  #[test]
1640  fn a_flag_bit_the_vocabulary_cannot_hold_refuses_the_packet() {
1641    // Unreachable against this build — every flag FFmpeg names lives in
1642    // the byte `PacketFlags` carries, and a compile-time assertion says
1643    // so. It is here because the day that stops being true, a packet
1644    // must be refused rather than delivered with a bit missing.
1645    use ffmpeg_next::packet::Mut;
1646    let mut packet = Packet::copy(&[1u8]);
1647    // SAFETY: `packet` owns a live `AVPacket`.
1648    unsafe { (*packet.as_mut_ptr()).flags = 0x1_00 };
1649    let tb = mediadecode::Timebase::default();
1650    for (arm, result) in every_timed_arm(&packet) {
1651      assert!(
1652        matches!(
1653          result,
1654          Err(PacketBufferError::UnrepresentableFlags(p)) if p.raw() == 0x1_00
1655        ),
1656        "{arm}: {result:?}",
1657      );
1658    }
1659    assert!(matches!(
1660      attachment_packet_from_ffmpeg(&packet).map(|p| p.is_some()),
1661      Err(PacketBufferError::UnrepresentableFlags(_)),
1662    ));
1663    let _ = tb;
1664  }
1665
1666  #[test]
1667  fn side_data_survives_the_round_trip_on_every_decoder_bound_arm() {
1668    // The forward direction captures side data; the reverse direction
1669    // is what a decoder is actually handed. Capturing without
1670    // reattaching is theatre: `NEW_EXTRADATA` never reaches the codec,
1671    // `SKIP_SAMPLES` never trims, and nothing says so.
1672    let tb = mediadecode::Timebase::default();
1673    let mut with_body = Packet::copy(&[4u8, 5, 6]);
1674    {
1675      use ffmpeg_next::{
1676        ffi::{AVPacketSideDataType, av_packet_new_side_data},
1677        packet::Mut,
1678      };
1679      unsafe {
1680        let ptr = av_packet_new_side_data(
1681          with_body.as_mut_ptr(),
1682          AVPacketSideDataType::AV_PKT_DATA_SKIP_SAMPLES,
1683          3,
1684        );
1685        assert!(!ptr.is_null());
1686        core::ptr::copy_nonoverlapping([1u8, 2, 3].as_ptr(), ptr, 3);
1687      }
1688    }
1689    const SKIP_SAMPLES: i32 =
1690      ffmpeg_next::ffi::AVPacketSideDataType::AV_PKT_DATA_SKIP_SAMPLES as i32;
1691
1692    for (name, source) in [
1693      ("body plus side data", &with_body),
1694      ("side data only", &side_data_only_packet()),
1695    ] {
1696      let expected_kind = if name == "side data only" {
1697        NEW_EXTRADATA
1698      } else {
1699        SKIP_SAMPLES
1700      };
1701
1702      let video = video_packet_from_ffmpeg_in(source, tb)
1703        .expect("wrappable")
1704        .expect("present");
1705      let rebuilt = ffmpeg_packet_from_video_packet(&video).expect("rebuilt");
1706      let carried = packet_side_data(&rebuilt).expect("readable");
1707      assert_eq!(carried.len(), 1, "{name}: video lost its side data");
1708      assert_eq!(carried[0].kind(), expected_kind, "{name}: video");
1709      assert_eq!(carried[0].data(), video.extra().side_data()[0].data());
1710
1711      let audio = audio_packet_from_ffmpeg_in(source, tb)
1712        .expect("wrappable")
1713        .expect("present");
1714      let rebuilt = ffmpeg_packet_from_audio_packet(&audio).expect("rebuilt");
1715      let carried = packet_side_data(&rebuilt).expect("readable");
1716      assert_eq!(carried.len(), 1, "{name}: audio lost its side data");
1717      assert_eq!(carried[0].data(), audio.extra().side_data()[0].data());
1718
1719      let subtitle = subtitle_packet_from_ffmpeg_in(source, tb)
1720        .expect("wrappable")
1721        .expect("present");
1722      let rebuilt = ffmpeg_packet_from_subtitle_packet(&subtitle).expect("rebuilt");
1723      let carried = packet_side_data(&rebuilt).expect("readable");
1724      assert_eq!(carried.len(), 1, "{name}: subtitle lost its side data");
1725      assert_eq!(carried[0].data(), subtitle.extra().side_data()[0].data());
1726    }
1727  }
1728
1729  #[test]
1730  fn a_side_data_type_this_build_cannot_name_is_refused_not_dropped() {
1731    // This crate carries side-data types as the raw integers they are
1732    // on the wire, so a hand-built entry can name anything. Handing an
1733    // unknown one to C would either form an invalid discriminant or
1734    // attach a type nothing downstream reads — and dropping it quietly
1735    // is the very defect this whole seam exists to close.
1736    let limit = crate::ffi::side_data_type_count();
1737    let packet = mediadecode::packet::VideoPacket::new(
1738      FfmpegBuffer::copy_from_slice(&[1u8]).expect("body"),
1739      VideoPacketExtra::new(0).with_side_data(vec![SideDataEntry::new(limit, vec![9u8])]),
1740    );
1741    match ffmpeg_packet_from_video_packet(&packet).map(|_| ()) {
1742      Err(PacketBuildError::UnknownSideData(p)) => {
1743        assert_eq!(p.kind(), limit);
1744        assert_eq!(p.limit(), limit);
1745      }
1746      other => panic!("expected UnknownSideData, got {other:?}"),
1747    }
1748    assert!(matches!(
1749      ffmpeg_packet_from_video_packet(&mediadecode::packet::VideoPacket::new(
1750        FfmpegBuffer::copy_from_slice(&[1u8]).expect("body"),
1751        VideoPacketExtra::new(0).with_side_data(vec![SideDataEntry::new(-1, vec![9u8])]),
1752      ))
1753      .map(|_| ()),
1754      Err(PacketBuildError::UnknownSideData(p)) if p.kind() == -1,
1755    ));
1756  }
1757
1758  #[test]
1759  fn a_side_data_only_packet_is_delivered_on_every_timed_arm() {
1760    // Codec-control data with no body is still a packet. Dropped as an
1761    // "empty marker", it leaves a decoder running on parameters the
1762    // container has already replaced — and says nothing about it.
1763    let packet = side_data_only_packet();
1764    let tb = mediadecode::Timebase::default();
1765
1766    let video = video_packet_from_ffmpeg_in(&packet, tb)
1767      .expect("wrappable")
1768      .expect("a side-data-only packet is a packet");
1769    assert!(video.data().as_ref().is_empty(), "no body, but a buffer");
1770    assert_eq!(video.extra().side_data().len(), 1);
1771    assert_eq!(video.extra().side_data()[0].kind(), NEW_EXTRADATA);
1772    assert_eq!(video.extra().side_data()[0].data(), &[1, 2, 3, 4]);
1773
1774    let audio = audio_packet_from_ffmpeg_in(&packet, tb)
1775      .expect("wrappable")
1776      .expect("present");
1777    assert!(audio.data().as_ref().is_empty());
1778    assert_eq!(audio.extra().side_data()[0].data(), &[1, 2, 3, 4]);
1779
1780    let subtitle = subtitle_packet_from_ffmpeg_in(&packet, tb)
1781      .expect("wrappable")
1782      .expect("present");
1783    assert!(subtitle.data().as_ref().is_empty());
1784    assert_eq!(subtitle.extra().side_data()[0].data(), &[1, 2, 3, 4]);
1785
1786    let data = data_packet_from_ffmpeg_in(&packet, tb)
1787      .expect("wrappable")
1788      .expect("present");
1789    assert!(data.data().as_ref().is_empty());
1790    assert_eq!(data.extra().side_data()[0].data(), &[1, 2, 3, 4]);
1791
1792    // The one arm where a payload-less packet really is nothing: an
1793    // attachment is its bytes, and there are none.
1794    assert!(
1795      attachment_packet_from_ffmpeg(&packet)
1796        .expect("wrappable")
1797        .is_none(),
1798      "an attachment with no bytes is no attachment",
1799    );
1800  }
1801
1802  #[test]
1803  fn side_data_rides_a_packet_that_has_a_body_too() {
1804    // The seat's documented promise — "the raw side-data entries from
1805    // `AVPacket.side_data`" — was never kept by the conversion before.
1806    use ffmpeg_next::{
1807      ffi::{AVPacketSideDataType, av_packet_new_side_data},
1808      packet::Mut,
1809    };
1810    let mut packet = Packet::copy(&[7u8, 7, 7]);
1811    unsafe {
1812      let ptr = av_packet_new_side_data(
1813        packet.as_mut_ptr(),
1814        AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA,
1815        2,
1816      );
1817      assert!(!ptr.is_null());
1818      core::ptr::copy_nonoverlapping([9u8, 9].as_ptr(), ptr, 2);
1819    }
1820    let video = video_packet_from_ffmpeg_in(&packet, mediadecode::Timebase::default())
1821      .expect("wrappable")
1822      .expect("present");
1823    assert_eq!(video.data().as_ref(), &[7, 7, 7]);
1824    assert_eq!(video.extra().side_data()[0].data(), &[9, 9]);
1825  }
1826
1827  #[test]
1828  fn an_empty_packet_is_absent_and_a_real_one_is_present() {
1829    let tb = mediadecode::Timebase::default();
1830    // No payload at all: the marker some demuxers emit. Absent, not an
1831    // error — this is the only thing a pull loop may skip.
1832    let empty = Packet::empty();
1833    assert!(
1834      video_packet_from_ffmpeg_in(&empty, tb)
1835        .expect("not a failure")
1836        .is_none()
1837    );
1838    assert!(
1839      attachment_packet_from_ffmpeg(&empty)
1840        .expect("not a failure")
1841        .is_none()
1842    );
1843
1844    let real = Packet::copy(&[9u8, 8, 7]);
1845    let wrapped = video_packet_from_ffmpeg_in(&real, tb)
1846      .expect("wrappable")
1847      .expect("present");
1848    assert_eq!(wrapped.data().as_ref(), &[9, 8, 7]);
1849  }
1850
1851  #[test]
1852  fn nv12_round_trips() {
1853    assert_eq!(
1854      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_NV12 as i32),
1855      PixelFormat::Nv12,
1856    );
1857  }
1858
1859  #[test]
1860  fn p010be_maps_to_p010be() {
1861    // BE must map to the BE variant — the previous "fold to LE"
1862    // mapping silently corrupted P010BE pixel data via the safe
1863    // export path. The unsupported-format gate in `convert::av_frame_to_video_frame`
1864    // is the right place to reject BE today.
1865    assert_eq!(
1866      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_P010BE as i32),
1867      PixelFormat::P010Be,
1868    );
1869  }
1870
1871  #[test]
1872  fn unnamed_raw_maps_to_none() {
1873    assert_eq!(from_av_pixel_format(-99_999), PixelFormat::None);
1874  }
1875
1876  #[test]
1877  fn av_pix_fmt_none_maps_to_none() {
1878    assert_eq!(
1879      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_NONE as i32),
1880      PixelFormat::None,
1881    );
1882  }
1883
1884  #[test]
1885  fn hw_formats_detected() {
1886    assert!(is_hardware_pix_fmt(
1887      AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32
1888    ));
1889    assert!(is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_VAAPI as i32));
1890    assert!(is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_CUDA as i32));
1891    assert!(is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_D3D11 as i32));
1892  }
1893
1894  #[test]
1895  fn cpu_formats_not_detected_as_hw() {
1896    assert!(!is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_NV12 as i32));
1897    assert!(!is_hardware_pix_fmt(
1898      AVPixelFormat::AV_PIX_FMT_YUV420P as i32
1899    ));
1900    assert!(!is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_NONE as i32));
1901  }
1902
1903  #[test]
1904  fn hw_formats_map_to_none_in_pixel_format() {
1905    // HW sentinels intentionally don't have a mediadecode::PixelFormat
1906    // representation — they're not CPU pixel data.
1907    assert_eq!(
1908      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32),
1909      PixelFormat::None,
1910    );
1911    assert_eq!(
1912      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_VAAPI as i32),
1913      PixelFormat::None,
1914    );
1915  }
1916}