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