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  // `buffer::SideDataAlloc` is aliased: this module also defines its own
27  // `PacketBuildError::SideDataAlloc` payload (the write-side failure —
28  // FFmpeg refusing an allocation while rebuilding an `AVPacket`'s side
29  // data), which keeps the bare name since it is native to this file.
30  // `BufferSideDataAlloc` is `PacketBufferError`'s read-side counterpart
31  // (out of memory copying a side-data entry *out of* an `AVPacket`) —
32  // same short name, different struct, different direction.
33  buffer::{
34    FfmpegBytes, PacketBufferError, SideDataAlloc as BufferSideDataAlloc, SideDataArray,
35    SideDataBytes, SideDataEntries, SideDataPayload, UnrepresentableFlags,
36  },
37  carrier::BodyRoute,
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  limits::PacketLimits,
44  sample_format::SampleFormat,
45};
46
47/// Maps a raw `AVFrame.format` integer (i.e. the value of an
48/// `AVPixelFormat` enum variant) onto [`mediadecode::PixelFormat`].
49///
50/// Returns [`PixelFormat::None`] for raw integers we don't have a
51/// mapping for — including `AV_PIX_FMT_NONE` itself and the
52/// hardware-frame markers (`AV_PIX_FMT_VIDEOTOOLBOX` / `_VAAPI` /
53/// `_CUDA` / `_D3D11` / …), since those never describe CPU-side pixel
54/// data and the unified enum intentionally doesn't carry them. Use
55/// [`is_hardware_pix_fmt`] to identify HW frames before transferring
56/// to a CPU format.
57///
58/// mediaframe 0.3 struck `PixelFormat::Unknown(u32)`, so the raw
59/// integer no longer rides along in the returned value; the caller's
60/// own `raw` is the place it survives. Every consumer in this crate
61/// already treats the fall-through as "not a deliverable CPU format"
62/// (`pixdesc::to_av_pixel_format`, `is_supported_cpu_pix_fmt` and the
63/// geometry tables all reject it), so the rejection is unchanged.
64///
65/// The match never constructs an `AVPixelFormat` from a runtime
66/// value; it compares the input against `AVPixelFormat::AV_PIX_FMT_X
67/// as i32` constants. Sound regardless of which discriminant set the
68/// linked FFmpeg version exposes.
69pub const fn from_av_pixel_format(raw: i32) -> PixelFormat {
70  // Mirrors `crate::pixdesc::to_av_pixel_format` arm-for-arm (its
71  // inverse). Every deliverable CPU format plus the non-deliverable
72  // formats `to_av` still resolves a constant for (monochrome / PAL /
73  // sub-byte-packed RGB / Bayer) is mapped here, so a frame's raw
74  // `format` integer always lands on the same `PixelFormat` the round
75  // trip would produce. Deliverability (HWACCEL / BAYER / PAL /
76  // BITSTREAM rejection) is enforced separately by
77  // `pixdesc::is_deliverable` / the convert layer — this boundary is
78  // identity-only.
79  //
80  // BE-tagged formats map to mediadecode's distinct `*Be` variants
81  // (never folded onto the LE canonical). Folding BE onto LE silently
82  // corrupted pixel data: each >8-bit sample is byte-swapped between
83  // BE and LE, and the convert path exports the AVBufferRef bytes
84  // verbatim with no endian conversion, so a consumer reading a
85  // BE-tagged frame's planes as LE samples would see every sample
86  // byte-reversed. Mapping to the `*Be` variant keeps the format
87  // distinct so the convert layer can handle (or reject) it correctly.
88  //
89  // The match never constructs an `AVPixelFormat` from a runtime
90  // value; it compares the input against `AVPixelFormat::AV_PIX_FMT_X
91  // as i32` constants. Sound regardless of which discriminant set the
92  // linked FFmpeg version exposes.
93  match raw {
94    // Planar YUV 8-bit.
95    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P as i32 => PixelFormat::Yuv420p,
96    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P as i32 => PixelFormat::Yuv422p,
97    x if x == AVPixelFormat::AV_PIX_FMT_YUV440P as i32 => PixelFormat::Yuv440p,
98    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P as i32 => PixelFormat::Yuv444p,
99    x if x == AVPixelFormat::AV_PIX_FMT_YUV411P as i32 => PixelFormat::Yuv411p,
100    x if x == AVPixelFormat::AV_PIX_FMT_YUV410P as i32 => PixelFormat::Yuv410p,
101    // Deprecated JPEG-range planar YUV (yuvj-family).
102    x if x == AVPixelFormat::AV_PIX_FMT_YUVJ411P as i32 => PixelFormat::Yuvj411p,
103    x if x == AVPixelFormat::AV_PIX_FMT_YUVJ420P as i32 => PixelFormat::Yuvj420p,
104    x if x == AVPixelFormat::AV_PIX_FMT_YUVJ422P as i32 => PixelFormat::Yuvj422p,
105    x if x == AVPixelFormat::AV_PIX_FMT_YUVJ440P as i32 => PixelFormat::Yuvj440p,
106    x if x == AVPixelFormat::AV_PIX_FMT_YUVJ444P as i32 => PixelFormat::Yuvj444p,
107    // Planar YUV 4:2:0 high-bit.
108    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P9LE as i32 => PixelFormat::Yuv420p9Le,
109    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P9BE as i32 => PixelFormat::Yuv420p9Be,
110    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P10LE as i32 => PixelFormat::Yuv420p10Le,
111    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P10BE as i32 => PixelFormat::Yuv420p10Be,
112    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P12LE as i32 => PixelFormat::Yuv420p12Le,
113    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P12BE as i32 => PixelFormat::Yuv420p12Be,
114    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P14LE as i32 => PixelFormat::Yuv420p14Le,
115    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P14BE as i32 => PixelFormat::Yuv420p14Be,
116    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P16LE as i32 => PixelFormat::Yuv420p16Le,
117    x if x == AVPixelFormat::AV_PIX_FMT_YUV420P16BE as i32 => PixelFormat::Yuv420p16Be,
118    // Planar YUV 4:2:2 high-bit.
119    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P9LE as i32 => PixelFormat::Yuv422p9Le,
120    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P9BE as i32 => PixelFormat::Yuv422p9Be,
121    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P10LE as i32 => PixelFormat::Yuv422p10Le,
122    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P10BE as i32 => PixelFormat::Yuv422p10Be,
123    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P12LE as i32 => PixelFormat::Yuv422p12Le,
124    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P12BE as i32 => PixelFormat::Yuv422p12Be,
125    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P14LE as i32 => PixelFormat::Yuv422p14Le,
126    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P14BE as i32 => PixelFormat::Yuv422p14Be,
127    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P16LE as i32 => PixelFormat::Yuv422p16Le,
128    x if x == AVPixelFormat::AV_PIX_FMT_YUV422P16BE as i32 => PixelFormat::Yuv422p16Be,
129    // Planar YUV 4:4:0 high-bit.
130    x if x == AVPixelFormat::AV_PIX_FMT_YUV440P10LE as i32 => PixelFormat::Yuv440p10Le,
131    x if x == AVPixelFormat::AV_PIX_FMT_YUV440P10BE as i32 => PixelFormat::Yuv440p10Be,
132    x if x == AVPixelFormat::AV_PIX_FMT_YUV440P12LE as i32 => PixelFormat::Yuv440p12Le,
133    x if x == AVPixelFormat::AV_PIX_FMT_YUV440P12BE as i32 => PixelFormat::Yuv440p12Be,
134    // Planar YUV 4:4:4 high-bit.
135    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P9LE as i32 => PixelFormat::Yuv444p9Le,
136    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P9BE as i32 => PixelFormat::Yuv444p9Be,
137    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P10LE as i32 => PixelFormat::Yuv444p10Le,
138    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P10BE as i32 => PixelFormat::Yuv444p10Be,
139    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P12LE as i32 => PixelFormat::Yuv444p12Le,
140    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P12BE as i32 => PixelFormat::Yuv444p12Be,
141    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P14LE as i32 => PixelFormat::Yuv444p14Le,
142    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P14BE as i32 => PixelFormat::Yuv444p14Be,
143    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P16LE as i32 => PixelFormat::Yuv444p16Le,
144    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P16BE as i32 => PixelFormat::Yuv444p16Be,
145    // MSB-packed YUV 4:4:4.
146    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P10MSBLE as i32 => PixelFormat::Yuv444p10MsbLe,
147    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P10MSBBE as i32 => PixelFormat::Yuv444p10MsbBe,
148    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P12MSBLE as i32 => PixelFormat::Yuv444p12MsbLe,
149    x if x == AVPixelFormat::AV_PIX_FMT_YUV444P12MSBBE as i32 => PixelFormat::Yuv444p12MsbBe,
150    // Planar YUVA.
151    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P as i32 => PixelFormat::Yuva420p,
152    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P as i32 => PixelFormat::Yuva422p,
153    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P as i32 => PixelFormat::Yuva444p,
154    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P9LE as i32 => PixelFormat::Yuva420p9Le,
155    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P9BE as i32 => PixelFormat::Yuva420p9Be,
156    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P9LE as i32 => PixelFormat::Yuva422p9Le,
157    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P9BE as i32 => PixelFormat::Yuva422p9Be,
158    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P9LE as i32 => PixelFormat::Yuva444p9Le,
159    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P9BE as i32 => PixelFormat::Yuva444p9Be,
160    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P10LE as i32 => PixelFormat::Yuva420p10Le,
161    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P10BE as i32 => PixelFormat::Yuva420p10Be,
162    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P10LE as i32 => PixelFormat::Yuva422p10Le,
163    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P10BE as i32 => PixelFormat::Yuva422p10Be,
164    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P10LE as i32 => PixelFormat::Yuva444p10Le,
165    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P10BE as i32 => PixelFormat::Yuva444p10Be,
166    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P12LE as i32 => PixelFormat::Yuva422p12Le,
167    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P12BE as i32 => PixelFormat::Yuva422p12Be,
168    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P12LE as i32 => PixelFormat::Yuva444p12Le,
169    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P12BE as i32 => PixelFormat::Yuva444p12Be,
170    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P16LE as i32 => PixelFormat::Yuva420p16Le,
171    x if x == AVPixelFormat::AV_PIX_FMT_YUVA420P16BE as i32 => PixelFormat::Yuva420p16Be,
172    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P16LE as i32 => PixelFormat::Yuva422p16Le,
173    x if x == AVPixelFormat::AV_PIX_FMT_YUVA422P16BE as i32 => PixelFormat::Yuva422p16Be,
174    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P16LE as i32 => PixelFormat::Yuva444p16Le,
175    x if x == AVPixelFormat::AV_PIX_FMT_YUVA444P16BE as i32 => PixelFormat::Yuva444p16Be,
176    // Semi-planar YUV 8-bit.
177    x if x == AVPixelFormat::AV_PIX_FMT_NV12 as i32 => PixelFormat::Nv12,
178    x if x == AVPixelFormat::AV_PIX_FMT_NV21 as i32 => PixelFormat::Nv21,
179    x if x == AVPixelFormat::AV_PIX_FMT_NV16 as i32 => PixelFormat::Nv16,
180    x if x == AVPixelFormat::AV_PIX_FMT_NV24 as i32 => PixelFormat::Nv24,
181    x if x == AVPixelFormat::AV_PIX_FMT_NV42 as i32 => PixelFormat::Nv42,
182    x if x == AVPixelFormat::AV_PIX_FMT_NV20LE as i32 => PixelFormat::Nv20Le,
183    x if x == AVPixelFormat::AV_PIX_FMT_NV20BE as i32 => PixelFormat::Nv20Be,
184    // Semi-planar YUV high-bit.
185    x if x == AVPixelFormat::AV_PIX_FMT_P010LE as i32 => PixelFormat::P010Le,
186    x if x == AVPixelFormat::AV_PIX_FMT_P010BE as i32 => PixelFormat::P010Be,
187    x if x == AVPixelFormat::AV_PIX_FMT_P012LE as i32 => PixelFormat::P012Le,
188    x if x == AVPixelFormat::AV_PIX_FMT_P012BE as i32 => PixelFormat::P012Be,
189    x if x == AVPixelFormat::AV_PIX_FMT_P016LE as i32 => PixelFormat::P016Le,
190    x if x == AVPixelFormat::AV_PIX_FMT_P016BE as i32 => PixelFormat::P016Be,
191    x if x == AVPixelFormat::AV_PIX_FMT_P210LE as i32 => PixelFormat::P210Le,
192    x if x == AVPixelFormat::AV_PIX_FMT_P210BE as i32 => PixelFormat::P210Be,
193    x if x == AVPixelFormat::AV_PIX_FMT_P212LE as i32 => PixelFormat::P212Le,
194    x if x == AVPixelFormat::AV_PIX_FMT_P212BE as i32 => PixelFormat::P212Be,
195    x if x == AVPixelFormat::AV_PIX_FMT_P216LE as i32 => PixelFormat::P216Le,
196    x if x == AVPixelFormat::AV_PIX_FMT_P216BE as i32 => PixelFormat::P216Be,
197    x if x == AVPixelFormat::AV_PIX_FMT_P410LE as i32 => PixelFormat::P410Le,
198    x if x == AVPixelFormat::AV_PIX_FMT_P410BE as i32 => PixelFormat::P410Be,
199    x if x == AVPixelFormat::AV_PIX_FMT_P412LE as i32 => PixelFormat::P412Le,
200    x if x == AVPixelFormat::AV_PIX_FMT_P412BE as i32 => PixelFormat::P412Be,
201    x if x == AVPixelFormat::AV_PIX_FMT_P416LE as i32 => PixelFormat::P416Le,
202    x if x == AVPixelFormat::AV_PIX_FMT_P416BE as i32 => PixelFormat::P416Be,
203    // Packed YUV 8-bit.
204    x if x == AVPixelFormat::AV_PIX_FMT_YUYV422 as i32 => PixelFormat::Yuyv422,
205    x if x == AVPixelFormat::AV_PIX_FMT_UYVY422 as i32 => PixelFormat::Uyvy422,
206    x if x == AVPixelFormat::AV_PIX_FMT_YVYU422 as i32 => PixelFormat::Yvyu422,
207    x if x == AVPixelFormat::AV_PIX_FMT_UYYVYY411 as i32 => PixelFormat::Uyyvyy411,
208    // Packed YUV high-bit.
209    x if x == AVPixelFormat::AV_PIX_FMT_Y210LE as i32 => PixelFormat::Y210Le,
210    x if x == AVPixelFormat::AV_PIX_FMT_Y210BE as i32 => PixelFormat::Y210Be,
211    x if x == AVPixelFormat::AV_PIX_FMT_Y212LE as i32 => PixelFormat::Y212Le,
212    x if x == AVPixelFormat::AV_PIX_FMT_Y212BE as i32 => PixelFormat::Y212Be,
213    x if x == AVPixelFormat::AV_PIX_FMT_Y216LE as i32 => PixelFormat::Y216Le,
214    x if x == AVPixelFormat::AV_PIX_FMT_Y216BE as i32 => PixelFormat::Y216Be,
215    x if x == AVPixelFormat::AV_PIX_FMT_XV30LE as i32 => PixelFormat::Xv30Le,
216    x if x == AVPixelFormat::AV_PIX_FMT_XV30BE as i32 => PixelFormat::Xv30Be,
217    x if x == AVPixelFormat::AV_PIX_FMT_V30XLE as i32 => PixelFormat::V30xLe,
218    x if x == AVPixelFormat::AV_PIX_FMT_V30XBE as i32 => PixelFormat::V30xBe,
219    x if x == AVPixelFormat::AV_PIX_FMT_XV36LE as i32 => PixelFormat::Xv36Le,
220    x if x == AVPixelFormat::AV_PIX_FMT_XV36BE as i32 => PixelFormat::Xv36Be,
221    x if x == AVPixelFormat::AV_PIX_FMT_XV48LE as i32 => PixelFormat::Xv48Le,
222    x if x == AVPixelFormat::AV_PIX_FMT_XV48BE as i32 => PixelFormat::Xv48Be,
223    x if x == AVPixelFormat::AV_PIX_FMT_VUYA as i32 => PixelFormat::Vuya,
224    x if x == AVPixelFormat::AV_PIX_FMT_VUYX as i32 => PixelFormat::Vuyx,
225    x if x == AVPixelFormat::AV_PIX_FMT_AYUV as i32 => PixelFormat::Ayuv,
226    x if x == AVPixelFormat::AV_PIX_FMT_AYUV64LE as i32 => PixelFormat::Ayuv64Le,
227    x if x == AVPixelFormat::AV_PIX_FMT_AYUV64BE as i32 => PixelFormat::Ayuv64Be,
228    x if x == AVPixelFormat::AV_PIX_FMT_UYVA as i32 => PixelFormat::Uyva,
229    x if x == AVPixelFormat::AV_PIX_FMT_VYU444 as i32 => PixelFormat::Vyu444,
230    // XYZ.
231    x if x == AVPixelFormat::AV_PIX_FMT_XYZ12LE as i32 => PixelFormat::Xyz12Le,
232    x if x == AVPixelFormat::AV_PIX_FMT_XYZ12BE as i32 => PixelFormat::Xyz12Be,
233    // Packed RGB 8-bit.
234    x if x == AVPixelFormat::AV_PIX_FMT_RGB24 as i32 => PixelFormat::Rgb24,
235    x if x == AVPixelFormat::AV_PIX_FMT_BGR24 as i32 => PixelFormat::Bgr24,
236    x if x == AVPixelFormat::AV_PIX_FMT_RGBA as i32 => PixelFormat::Rgba,
237    x if x == AVPixelFormat::AV_PIX_FMT_BGRA as i32 => PixelFormat::Bgra,
238    x if x == AVPixelFormat::AV_PIX_FMT_ARGB as i32 => PixelFormat::Argb,
239    x if x == AVPixelFormat::AV_PIX_FMT_ABGR as i32 => PixelFormat::Abgr,
240    x if x == AVPixelFormat::AV_PIX_FMT_RGB0 as i32 => PixelFormat::Rgbx,
241    x if x == AVPixelFormat::AV_PIX_FMT_BGR0 as i32 => PixelFormat::Bgrx,
242    x if x == AVPixelFormat::AV_PIX_FMT_0RGB as i32 => PixelFormat::Xrgb,
243    x if x == AVPixelFormat::AV_PIX_FMT_0BGR as i32 => PixelFormat::Xbgr,
244    x if x == AVPixelFormat::AV_PIX_FMT_X2RGB10LE as i32 => PixelFormat::X2Rgb10Le,
245    x if x == AVPixelFormat::AV_PIX_FMT_X2RGB10BE as i32 => PixelFormat::X2Rgb10Be,
246    x if x == AVPixelFormat::AV_PIX_FMT_X2BGR10LE as i32 => PixelFormat::X2Bgr10Le,
247    x if x == AVPixelFormat::AV_PIX_FMT_X2BGR10BE as i32 => PixelFormat::X2Bgr10Be,
248    // Gbr24p shares AV_PIX_FMT_GBRP's discriminant; mapped to Gbrp above.
249    // Packed RGB high-bit.
250    x if x == AVPixelFormat::AV_PIX_FMT_RGB48LE as i32 => PixelFormat::Rgb48Le,
251    x if x == AVPixelFormat::AV_PIX_FMT_RGB48BE as i32 => PixelFormat::Rgb48Be,
252    x if x == AVPixelFormat::AV_PIX_FMT_BGR48LE as i32 => PixelFormat::Bgr48Le,
253    x if x == AVPixelFormat::AV_PIX_FMT_BGR48BE as i32 => PixelFormat::Bgr48Be,
254    x if x == AVPixelFormat::AV_PIX_FMT_RGBA64LE as i32 => PixelFormat::Rgba64Le,
255    x if x == AVPixelFormat::AV_PIX_FMT_RGBA64BE as i32 => PixelFormat::Rgba64Be,
256    x if x == AVPixelFormat::AV_PIX_FMT_BGRA64LE as i32 => PixelFormat::Bgra64Le,
257    x if x == AVPixelFormat::AV_PIX_FMT_BGRA64BE as i32 => PixelFormat::Bgra64Be,
258    x if x == AVPixelFormat::AV_PIX_FMT_RGB96LE as i32 => PixelFormat::Rgb96Le,
259    x if x == AVPixelFormat::AV_PIX_FMT_RGB96BE as i32 => PixelFormat::Rgb96Be,
260    x if x == AVPixelFormat::AV_PIX_FMT_RGBA128LE as i32 => PixelFormat::Rgba128Le,
261    x if x == AVPixelFormat::AV_PIX_FMT_RGBA128BE as i32 => PixelFormat::Rgba128Be,
262    // Packed RGB float / half-float.
263    x if x == AVPixelFormat::AV_PIX_FMT_RGBF16LE as i32 => PixelFormat::Rgbf16Le,
264    x if x == AVPixelFormat::AV_PIX_FMT_RGBF16BE as i32 => PixelFormat::Rgbf16Be,
265    x if x == AVPixelFormat::AV_PIX_FMT_RGBF32LE as i32 => PixelFormat::Rgbf32Le,
266    x if x == AVPixelFormat::AV_PIX_FMT_RGBF32BE as i32 => PixelFormat::Rgbf32Be,
267    x if x == AVPixelFormat::AV_PIX_FMT_RGBAF16LE as i32 => PixelFormat::Rgbaf16Le,
268    x if x == AVPixelFormat::AV_PIX_FMT_RGBAF16BE as i32 => PixelFormat::Rgbaf16Be,
269    x if x == AVPixelFormat::AV_PIX_FMT_RGBAF32LE as i32 => PixelFormat::Rgbaf32Le,
270    x if x == AVPixelFormat::AV_PIX_FMT_RGBAF32BE as i32 => PixelFormat::Rgbaf32Be,
271    // Planar GBR.
272    x if x == AVPixelFormat::AV_PIX_FMT_GBRP as i32 => PixelFormat::Gbrp,
273    x if x == AVPixelFormat::AV_PIX_FMT_GBRP9LE as i32 => PixelFormat::Gbrp9Le,
274    x if x == AVPixelFormat::AV_PIX_FMT_GBRP9BE as i32 => PixelFormat::Gbrp9Be,
275    x if x == AVPixelFormat::AV_PIX_FMT_GBRP10LE as i32 => PixelFormat::Gbrp10Le,
276    x if x == AVPixelFormat::AV_PIX_FMT_GBRP10BE as i32 => PixelFormat::Gbrp10Be,
277    x if x == AVPixelFormat::AV_PIX_FMT_GBRP10MSBLE as i32 => PixelFormat::Gbrp10MsbLe,
278    x if x == AVPixelFormat::AV_PIX_FMT_GBRP10MSBBE as i32 => PixelFormat::Gbrp10MsbBe,
279    x if x == AVPixelFormat::AV_PIX_FMT_GBRP12LE as i32 => PixelFormat::Gbrp12Le,
280    x if x == AVPixelFormat::AV_PIX_FMT_GBRP12BE as i32 => PixelFormat::Gbrp12Be,
281    x if x == AVPixelFormat::AV_PIX_FMT_GBRP12MSBLE as i32 => PixelFormat::Gbrp12MsbLe,
282    x if x == AVPixelFormat::AV_PIX_FMT_GBRP12MSBBE as i32 => PixelFormat::Gbrp12MsbBe,
283    x if x == AVPixelFormat::AV_PIX_FMT_GBRP14LE as i32 => PixelFormat::Gbrp14Le,
284    x if x == AVPixelFormat::AV_PIX_FMT_GBRP14BE as i32 => PixelFormat::Gbrp14Be,
285    x if x == AVPixelFormat::AV_PIX_FMT_GBRP16LE as i32 => PixelFormat::Gbrp16Le,
286    x if x == AVPixelFormat::AV_PIX_FMT_GBRP16BE as i32 => PixelFormat::Gbrp16Be,
287    x if x == AVPixelFormat::AV_PIX_FMT_GBRPF16LE as i32 => PixelFormat::Gbrpf16Le,
288    x if x == AVPixelFormat::AV_PIX_FMT_GBRPF16BE as i32 => PixelFormat::Gbrpf16Be,
289    x if x == AVPixelFormat::AV_PIX_FMT_GBRPF32LE as i32 => PixelFormat::Gbrpf32Le,
290    x if x == AVPixelFormat::AV_PIX_FMT_GBRPF32BE as i32 => PixelFormat::Gbrpf32Be,
291    // Planar GBRA.
292    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP as i32 => PixelFormat::Gbrap,
293    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP10LE as i32 => PixelFormat::Gbrap10Le,
294    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP10BE as i32 => PixelFormat::Gbrap10Be,
295    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP12LE as i32 => PixelFormat::Gbrap12Le,
296    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP12BE as i32 => PixelFormat::Gbrap12Be,
297    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP14LE as i32 => PixelFormat::Gbrap14Le,
298    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP14BE as i32 => PixelFormat::Gbrap14Be,
299    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP16LE as i32 => PixelFormat::Gbrap16Le,
300    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP16BE as i32 => PixelFormat::Gbrap16Be,
301    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP32LE as i32 => PixelFormat::Gbrap32Le,
302    x if x == AVPixelFormat::AV_PIX_FMT_GBRAP32BE as i32 => PixelFormat::Gbrap32Be,
303    x if x == AVPixelFormat::AV_PIX_FMT_GBRAPF16LE as i32 => PixelFormat::Gbrapf16Le,
304    x if x == AVPixelFormat::AV_PIX_FMT_GBRAPF16BE as i32 => PixelFormat::Gbrapf16Be,
305    x if x == AVPixelFormat::AV_PIX_FMT_GBRAPF32LE as i32 => PixelFormat::Gbrapf32Le,
306    x if x == AVPixelFormat::AV_PIX_FMT_GBRAPF32BE as i32 => PixelFormat::Gbrapf32Be,
307    // Greyscale.
308    x if x == AVPixelFormat::AV_PIX_FMT_GRAY8 as i32 => PixelFormat::Gray8,
309    x if x == AVPixelFormat::AV_PIX_FMT_GRAY9LE as i32 => PixelFormat::Gray9Le,
310    x if x == AVPixelFormat::AV_PIX_FMT_GRAY9BE as i32 => PixelFormat::Gray9Be,
311    x if x == AVPixelFormat::AV_PIX_FMT_GRAY10LE as i32 => PixelFormat::Gray10Le,
312    x if x == AVPixelFormat::AV_PIX_FMT_GRAY10BE as i32 => PixelFormat::Gray10Be,
313    x if x == AVPixelFormat::AV_PIX_FMT_GRAY12LE as i32 => PixelFormat::Gray12Le,
314    x if x == AVPixelFormat::AV_PIX_FMT_GRAY12BE as i32 => PixelFormat::Gray12Be,
315    x if x == AVPixelFormat::AV_PIX_FMT_GRAY14LE as i32 => PixelFormat::Gray14Le,
316    x if x == AVPixelFormat::AV_PIX_FMT_GRAY14BE as i32 => PixelFormat::Gray14Be,
317    x if x == AVPixelFormat::AV_PIX_FMT_GRAY16LE as i32 => PixelFormat::Gray16Le,
318    x if x == AVPixelFormat::AV_PIX_FMT_GRAY16BE as i32 => PixelFormat::Gray16Be,
319    x if x == AVPixelFormat::AV_PIX_FMT_GRAY32LE as i32 => PixelFormat::Gray32Le,
320    x if x == AVPixelFormat::AV_PIX_FMT_GRAY32BE as i32 => PixelFormat::Gray32Be,
321    x if x == AVPixelFormat::AV_PIX_FMT_GRAYF16LE as i32 => PixelFormat::Grayf16Le,
322    x if x == AVPixelFormat::AV_PIX_FMT_GRAYF16BE as i32 => PixelFormat::Grayf16Be,
323    x if x == AVPixelFormat::AV_PIX_FMT_GRAYF32LE as i32 => PixelFormat::Grayf32Le,
324    x if x == AVPixelFormat::AV_PIX_FMT_GRAYF32BE as i32 => PixelFormat::Grayf32Be,
325    x if x == AVPixelFormat::AV_PIX_FMT_YA8 as i32 => PixelFormat::Ya8,
326    x if x == AVPixelFormat::AV_PIX_FMT_YA16LE as i32 => PixelFormat::Ya16Le,
327    x if x == AVPixelFormat::AV_PIX_FMT_YA16BE as i32 => PixelFormat::Ya16Be,
328    x if x == AVPixelFormat::AV_PIX_FMT_YAF16LE as i32 => PixelFormat::Yaf16Le,
329    x if x == AVPixelFormat::AV_PIX_FMT_YAF16BE as i32 => PixelFormat::Yaf16Be,
330    x if x == AVPixelFormat::AV_PIX_FMT_YAF32LE as i32 => PixelFormat::Yaf32Le,
331    x if x == AVPixelFormat::AV_PIX_FMT_YAF32BE as i32 => PixelFormat::Yaf32Be,
332    x if x == AVPixelFormat::AV_PIX_FMT_MONOWHITE as i32 => PixelFormat::Monowhite,
333    x if x == AVPixelFormat::AV_PIX_FMT_MONOBLACK as i32 => PixelFormat::Monoblack,
334    x if x == AVPixelFormat::AV_PIX_FMT_PAL8 as i32 => PixelFormat::Pal8,
335    x if x == AVPixelFormat::AV_PIX_FMT_RGB4 as i32 => PixelFormat::Rgb4,
336    x if x == AVPixelFormat::AV_PIX_FMT_RGB4_BYTE as i32 => PixelFormat::Rgb4Byte,
337    x if x == AVPixelFormat::AV_PIX_FMT_RGB8 as i32 => PixelFormat::Rgb8,
338    x if x == AVPixelFormat::AV_PIX_FMT_BGR4 as i32 => PixelFormat::Bgr4,
339    x if x == AVPixelFormat::AV_PIX_FMT_BGR4_BYTE as i32 => PixelFormat::Bgr4Byte,
340    x if x == AVPixelFormat::AV_PIX_FMT_BGR8 as i32 => PixelFormat::Bgr8,
341    x if x == AVPixelFormat::AV_PIX_FMT_RGB444LE as i32 => PixelFormat::Rgb444Le,
342    x if x == AVPixelFormat::AV_PIX_FMT_RGB444BE as i32 => PixelFormat::Rgb444Be,
343    x if x == AVPixelFormat::AV_PIX_FMT_BGR444LE as i32 => PixelFormat::Bgr444Le,
344    x if x == AVPixelFormat::AV_PIX_FMT_BGR444BE as i32 => PixelFormat::Bgr444Be,
345    x if x == AVPixelFormat::AV_PIX_FMT_RGB555LE as i32 => PixelFormat::Rgb555Le,
346    x if x == AVPixelFormat::AV_PIX_FMT_RGB555BE as i32 => PixelFormat::Rgb555Be,
347    x if x == AVPixelFormat::AV_PIX_FMT_BGR555LE as i32 => PixelFormat::Bgr555Le,
348    x if x == AVPixelFormat::AV_PIX_FMT_BGR555BE as i32 => PixelFormat::Bgr555Be,
349    x if x == AVPixelFormat::AV_PIX_FMT_RGB565LE as i32 => PixelFormat::Rgb565Le,
350    x if x == AVPixelFormat::AV_PIX_FMT_RGB565BE as i32 => PixelFormat::Rgb565Be,
351    x if x == AVPixelFormat::AV_PIX_FMT_BGR565LE as i32 => PixelFormat::Bgr565Le,
352    x if x == AVPixelFormat::AV_PIX_FMT_BGR565BE as i32 => PixelFormat::Bgr565Be,
353    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_BGGR8 as i32 => PixelFormat::BayerBggr8,
354    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_RGGB8 as i32 => PixelFormat::BayerRggb8,
355    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GBRG8 as i32 => PixelFormat::BayerGbrg8,
356    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GRBG8 as i32 => PixelFormat::BayerGrbg8,
357    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_BGGR16LE as i32 => PixelFormat::BayerBggr16Le,
358    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_BGGR16BE as i32 => PixelFormat::BayerBggr16Be,
359    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_RGGB16LE as i32 => PixelFormat::BayerRggb16Le,
360    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_RGGB16BE as i32 => PixelFormat::BayerRggb16Be,
361    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GBRG16LE as i32 => PixelFormat::BayerGbrg16Le,
362    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GBRG16BE as i32 => PixelFormat::BayerGbrg16Be,
363    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GRBG16LE as i32 => PixelFormat::BayerGrbg16Le,
364    x if x == AVPixelFormat::AV_PIX_FMT_BAYER_GRBG16BE as i32 => PixelFormat::BayerGrbg16Be,
365    _ => PixelFormat::None,
366  }
367}
368
369/// Returns `true` when `raw` is one of FFmpeg's hardware-frame markers
370/// (`AV_PIX_FMT_VIDEOTOOLBOX` / `_VAAPI` / `_CUDA` / `_D3D11` /
371/// `_DRM_PRIME` / `_MEDIACODEC` / `_VULKAN`). Used by the HW probe to
372/// identify GPU-resident frames before triggering
373/// `av_hwframe_transfer_data`.
374pub const fn is_hardware_pix_fmt(raw: i32) -> bool {
375  matches!(
376    raw,
377    x if x == AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32
378      || x == AVPixelFormat::AV_PIX_FMT_VAAPI as i32
379      || x == AVPixelFormat::AV_PIX_FMT_CUDA as i32
380      || x == AVPixelFormat::AV_PIX_FMT_D3D11 as i32
381      || x == AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32
382      || x == AVPixelFormat::AV_PIX_FMT_MEDIACODEC as i32
383      || x == AVPixelFormat::AV_PIX_FMT_VULKAN as i32
384  )
385}
386
387/// Fallible counterpart to ffmpeg-next's `Packet::copy`.
388///
389/// The upstream helper calls `Packet::new(size)` (which silently
390/// truncates `size` to `c_int` and ignores `av_new_packet`'s return
391/// code) and then panics via `data_mut().unwrap().write_all(...).unwrap()`
392/// if the allocation failed. From a safe public decoder API we want
393/// the OOM / oversized-payload paths to surface as
394/// `ffmpeg_next::Error` rather than aborting the process — every
395/// `send_packet` path goes through this helper.
396///
397/// Failure modes:
398/// * payload larger than `c_int::MAX` (would overflow `AVPacket.size`)
399///   → `ffmpeg_next::Error::Other { errno: libc::EINVAL }`.
400/// * `av_new_packet` allocation failure (signalled by `data_mut()`
401///   returning `None`) → `ffmpeg_next::Error::Other { errno:
402///   libc::ENOMEM }`.
403pub(crate) fn try_packet_copy(data: &[u8]) -> std::result::Result<Packet, ffmpeg_next::Error> {
404  // FFmpeg's `AVPacket.size` is `c_int`. A payload larger than that
405  // can't fit in a single packet — refuse rather than truncate via
406  // `as c_int` inside `Packet::new`.
407  if data.len() > c_int::MAX as usize {
408    return Err(ffmpeg_next::Error::Other {
409      errno: libc::EINVAL,
410    });
411  }
412  // `Packet::new(size)` calls `av_new_packet(&mut pkt, size as
413  // c_int)` and ignores the return code; on OOM it returns a
414  // `Packet` whose `.data` is null. We detect that via
415  // `data_mut()` (returns `None` on null) and copy via
416  // `copy_nonoverlapping` so we never go through `data_mut()
417  // .unwrap().write_all().unwrap()` — the upstream `Packet::copy`'s
418  // double panic.
419  let mut pkt = Packet::new(data.len());
420  match pkt.data_mut() {
421    Some(slot) if slot.len() == data.len() => {
422      // SAFETY: `slot` is a `&mut [u8]` of `data.len()` bytes;
423      // `data` is a `&[u8]` of the same length. Non-overlapping
424      // because `slot` is a fresh allocation.
425      if !data.is_empty() {
426        unsafe {
427          core::ptr::copy_nonoverlapping(data.as_ptr(), slot.as_mut_ptr(), data.len());
428        }
429      }
430      Ok(pkt)
431    }
432    _ => Err(ffmpeg_next::Error::Other {
433      errno: libc::ENOMEM,
434    }),
435  }
436}
437
438/// Writes every flag the portable packet carries onto the `AVPacket`
439/// being rebuilt.
440///
441/// The one place this crate writes packet flags into FFmpeg. The three
442/// stream families rebuild through it, and so does the one-shot image
443/// road — which for two releases forgot to, and sent every attachment
444/// to libavcodec with a zeroed `flags` field.
445///
446/// Not through `Packet::set_flags`, which takes `ffmpeg_next`'s `Flags`
447/// and so can only spell `KEY` and `CORRUPT`: the bits are written to
448/// `AVPacket.flags` directly, so `DISCARD` — and anything else the
449/// forward direction retained — reaches the decoder that has to obey
450/// it. `PacketFlags` is a `u8` bit set and the field is a `c_int`, so
451/// the widening is total and nothing needs deciding here.
452///
453/// # Safety
454///
455/// `packet` must own a live `AVPacket`.
456pub(crate) unsafe fn write_md_flags(packet: &mut Packet, flags: MdPacketFlags) {
457  use ffmpeg_next::packet::Mut;
458  unsafe {
459    (*packet.as_mut_ptr()).flags = c_int::from(flags.bits());
460  }
461}
462
463/// Payload for [`PacketBuildError::UnknownSideData`].
464///
465/// A side-data entry whose type this build of FFmpeg does not name.
466///
467/// Refused rather than dropped: this crate carries side-data types as
468/// the raw integers they are on the wire, and handing an unknown one
469/// to C would either form an invalid enum discriminant or attach a
470/// type nothing downstream can read. Everything the demuxer captured
471/// came from this same build and is in range, so this only answers a
472/// hand-built entry.
473#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
474#[error("side-data type {kind} is not one this FFmpeg build names (0..{limit})")]
475pub struct UnknownSideData {
476  kind: i32,
477  limit: i32,
478}
479
480impl UnknownSideData {
481  /// Constructs an `UnknownSideData` payload.
482  #[cfg_attr(not(tarpaulin), inline(always))]
483  pub const fn new(kind: i32, limit: i32) -> Self {
484    Self { kind, limit }
485  }
486  /// The type integer the entry carried.
487  #[cfg_attr(not(tarpaulin), inline(always))]
488  pub const fn kind(&self) -> i32 {
489    self.kind
490  }
491  /// How many side-data types this build names.
492  #[cfg_attr(not(tarpaulin), inline(always))]
493  pub const fn limit(&self) -> i32 {
494    self.limit
495  }
496}
497
498/// Payload for [`PacketBuildError::SideDataAlloc`].
499///
500/// FFmpeg refused the side-data allocation.
501#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
502#[error("out of memory attaching {size} bytes of side data of type {kind}")]
503pub struct SideDataAlloc {
504  kind: i32,
505  size: usize,
506}
507
508impl SideDataAlloc {
509  /// Constructs a `SideDataAlloc` payload.
510  #[cfg_attr(not(tarpaulin), inline(always))]
511  pub const fn new(kind: i32, size: usize) -> Self {
512    Self { kind, size }
513  }
514  /// The entry's type integer.
515  #[cfg_attr(not(tarpaulin), inline(always))]
516  pub const fn kind(&self) -> i32 {
517    self.kind
518  }
519  /// The entry's payload length.
520  #[cfg_attr(not(tarpaulin), inline(always))]
521  pub const fn size(&self) -> usize {
522    self.size
523  }
524}
525
526/// Payload for [`PacketBuildError::SendPayloadTooLarge`].
527///
528/// A compressed payload on its way **into** FFmpeg is larger than the
529/// session's budget allows.
530///
531/// Named for the direction: this is the send leg. Its outbound twin is
532/// [`PacketBufferError::PacketTooLarge`](crate::PacketBufferError),
533/// which judges the same quantity coming the other way, against the
534/// same seat.
535#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
536#[error("a {bytes}-byte packet payload exceeds the {limit}-byte budget on the way into FFmpeg")]
537pub struct SendPayloadTooLarge {
538  bytes: usize,
539  limit: usize,
540}
541
542impl SendPayloadTooLarge {
543  /// Constructs a `SendPayloadTooLarge` payload.
544  #[cfg_attr(not(tarpaulin), inline(always))]
545  pub const fn new(bytes: usize, limit: usize) -> Self {
546    Self { bytes, limit }
547  }
548  /// The payload length the caller handed over.
549  #[cfg_attr(not(tarpaulin), inline(always))]
550  pub const fn bytes(&self) -> usize {
551    self.bytes
552  }
553  /// The budget in force.
554  #[cfg_attr(not(tarpaulin), inline(always))]
555  pub const fn limit(&self) -> usize {
556    self.limit
557  }
558}
559
560/// Why a portable packet could not be rebuilt as an `AVPacket`.
561///
562/// The reverse direction is what feeds a decoder, so everything the
563/// forward direction captured has to survive it or the capture was
564/// theatre.
565#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error, IsVariant, Unwrap, TryUnwrap)]
566#[unwrap(ref, ref_mut)]
567#[try_unwrap(ref, ref_mut)]
568pub enum PacketBuildError {
569  /// The payload is larger than the session's budget allows. Refused
570  /// **before** the `AVPacket` is allocated — the into-FFmpeg leg of
571  /// the budget the outbound leg already kept.
572  #[error(transparent)]
573  SendPayloadTooLarge(#[from] SendPayloadTooLarge),
574
575  /// The packet body could not be allocated or is larger than
576  /// `AVPacket.size` can hold.
577  #[error(transparent)]
578  Ffmpeg(#[from] ffmpeg_next::Error),
579
580  /// A side-data entry whose type this build of FFmpeg does not name.
581  #[error(transparent)]
582  UnknownSideData(#[from] UnknownSideData),
583
584  /// FFmpeg refused the side-data allocation.
585  #[error(transparent)]
586  SideDataAlloc(#[from] SideDataAlloc),
587
588  /// The packet is marked `AV_PKT_FLAG_TRUSTED`. See
589  /// [`crate::buffer::TrustedPayload`] — the other leg of the same
590  /// refusal.
591  #[error(transparent)]
592  TrustedPayload(#[from] crate::buffer::TrustedPayload),
593
594  /// The packet's side-data list is over the entry or byte cap the read
595  /// side has always applied. See [`SendSideDataTooLarge`].
596  #[error(transparent)]
597  SendSideDataTooLarge(#[from] SendSideDataTooLarge),
598}
599
600/// Judges the **whole** side-data list before a byte of it is
601/// allocated.
602///
603/// # One seat, both directions
604///
605/// The read side has capped frame and packet side data at 64 entries
606/// and 256 KiB since it was written; the send side had no cap at all.
607/// The preflight checked `body.len()` and then `attach_side_data`
608/// allocated every entry the caller handed over, one
609/// `av_packet_new_side_data` at a time, with nothing bounding the count
610/// or the total — so bytes the demux boundary would have refused coming
611/// *out* of a container went straight into libavcodec going *in*. That
612/// is the same asymmetry `PacketLimits` was introduced to close for the
613/// packet body, one field along.
614///
615/// So the caps are the read side's, applied here, before anything is
616/// allocated — including the body, because a list that cannot be
617/// carried should not cost a packet allocation first.
618fn check_side_data_budget(entries: &[SideDataEntry]) -> Result<(), PacketBuildError> {
619  use crate::convert::{SIDE_DATA_MAX_ENTRIES, SIDE_DATA_MAX_TOTAL_BYTES};
620
621  if entries.len() > SIDE_DATA_MAX_ENTRIES {
622    return Err(PacketBuildError::SendSideDataTooLarge(
623      SendSideDataTooLarge::new_entries(entries.len(), SIDE_DATA_MAX_ENTRIES),
624    ));
625  }
626  let mut total: usize = 0;
627  for entry in entries {
628    total = total.saturating_add(entry.data().len());
629    if total > SIDE_DATA_MAX_TOTAL_BYTES {
630      return Err(PacketBuildError::SendSideDataTooLarge(
631        SendSideDataTooLarge::new_bytes(total, SIDE_DATA_MAX_TOTAL_BYTES),
632      ));
633    }
634  }
635  Ok(())
636}
637
638/// Payload for [`PacketBuildError::SendSideDataTooLarge`].
639///
640/// A side-data list too long or too large to carry into a decoder.
641/// Carries whichever of the two caps was reached, so a caller can tell
642/// "too many annotations" from "too much annotation".
643#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
644#[error("packet side data is over the {what} ceiling: {value} against {limit}")]
645pub struct SendSideDataTooLarge {
646  what: &'static str,
647  value: usize,
648  limit: usize,
649}
650
651impl SendSideDataTooLarge {
652  /// The entry-count cap was reached.
653  #[inline]
654  pub const fn new_entries(value: usize, limit: usize) -> Self {
655    Self {
656      what: "entry-count",
657      value,
658      limit,
659    }
660  }
661  /// The aggregate-byte cap was reached.
662  #[inline]
663  pub const fn new_bytes(value: usize, limit: usize) -> Self {
664    Self {
665      what: "byte",
666      value,
667      limit,
668    }
669  }
670  /// The count or byte total the list reached.
671  #[inline]
672  pub const fn value(&self) -> usize {
673    self.value
674  }
675  /// The cap in force.
676  #[inline]
677  pub const fn limit(&self) -> usize {
678    self.limit
679  }
680  /// Which cap: `"entry-count"` or `"byte"`.
681  #[inline]
682  pub const fn what(&self) -> &'static str {
683    self.what
684  }
685}
686
687/// Refuses a portable packet whose flags carry `AV_PKT_FLAG_TRUSTED`.
688///
689/// The rebuild leg of the refusal `payload_of` makes on the way out.
690/// Closing only the copy-out leg would leave the loop open: a flag that
691/// reached a portable packet by some other route (a caller composing
692/// one by hand, a future producer, a graph that round-trips flags it
693/// does not interpret) would be written back onto a fresh `AVPacket` by
694/// `write_md_flags` and handed to a decoder entitled to believe it.
695///
696/// See [`crate::buffer::TrustedPayload`] for why the flag makes the
697/// payload uncarriable rather than merely suspicious.
698fn refuse_trusted(flags: MdPacketFlags, len: usize) -> std::result::Result<(), PacketBuildError> {
699  if flags.bits() & crate::buffer::TRUSTED_BIT != 0 {
700    return Err(PacketBuildError::TrustedPayload(
701      crate::buffer::TrustedPayload::new(len),
702    ));
703  }
704  Ok(())
705}
706
707/// Copies `entries` onto an `AVPacket` under construction.
708///
709/// A decoder learns things only this way: `AV_PKT_DATA_NEW_EXTRADATA`
710/// replaces its parameters mid-stream, `AV_PKT_DATA_PARAM_CHANGE` moves
711/// its rate or layout, `AV_PKT_DATA_SKIP_SAMPLES` is the encoder-delay
712/// trim without which a gapless stream is not gapless. Rebuilding a
713/// packet without them hands the decoder a body and a lie.
714fn attach_side_data(out: &mut Packet, entries: &[SideDataEntry]) -> Result<(), PacketBuildError> {
715  use ffmpeg_next::packet::Mut;
716  for entry in entries {
717    let kind = entry.kind();
718    let size = entry.data().len();
719    // SAFETY: `out` owns a live `AVPacket`; `packet_new_side_data`
720    // validates the type against this build's own range before handing
721    // it to C and reports a failed allocation as `None`.
722    let slot = unsafe { crate::ffi::packet_new_side_data(out.as_mut_ptr(), kind, size) };
723    let Some(slot) = slot else {
724      let limit = crate::ffi::side_data_type_count();
725      return Err(if kind < 0 || kind >= limit {
726        PacketBuildError::UnknownSideData(UnknownSideData::new(kind, limit))
727      } else {
728        PacketBuildError::SideDataAlloc(SideDataAlloc::new(kind, size))
729      });
730    };
731    if size > 0 {
732      // SAFETY: FFmpeg just allocated `size` bytes at `slot` (plus its
733      // padding), and `entry.data()` is a `&[u8]` of exactly that
734      // length; the two regions belong to different allocations.
735      unsafe { core::ptr::copy_nonoverlapping(entry.data().as_ptr(), slot, size) };
736    }
737  }
738  Ok(())
739}
740
741/// Builds an `AVPacket` that **shares** a view carrier's buffer instead
742/// of copying it — the send half of the zero-copy chain.
743///
744/// # What the census found
745///
746/// 0.8 did not have this. Its reverse builders called the same
747/// `try_packet_copy` the owned lane uses, so a packet demuxed
748/// zero-copy was copied again on its way back into a decoder. The
749/// zero-copy chain 0.8 actually shipped was demux to consumer, and
750/// stopped there.
751///
752/// # The padding proof
753///
754/// libavcodec is entitled to read `AV_INPUT_BUFFER_PADDING_SIZE` bytes
755/// **past** a packet's payload — bitstream readers do it routinely, and
756/// libavformat allocates that slack behind every packet it produces. A
757/// carrier viewing such a packet inherits the slack; a carrier this
758/// crate minted from a slice through
759/// [`FfmpegCarrier::from_bytes`](crate::FfmpegCarrier::from_bytes) does
760/// not.
761///
762/// So sharing is offered only where the slack is **provable**, and
763/// proving it takes two facts, not one:
764///
765/// * **provenance** — the carrier must be a payload captured out of an
766///   `AVPacket`'s own buffer, which is the only place libavformat's
767///   padding contract applies. Trailing *capacity* is not padding: a
768///   video plane has more pixels after it and a resampler's output
769///   frame has more samples, and a bitstream reader running past the
770///   payload would eat either as though it were bitstream. Provenance
771///   is recorded at capture, where it is known, rather than inferred
772///   here from a size comparison that cannot tell padding from a
773///   neighbour;
774/// * **extent** — the view must still leave at least the padding
775///   between its end and the end of the buffer, because a payload can
776///   be narrowed after capture.
777///
778/// Where either fails, the packet is copied, which is what
779/// `try_packet_copy` allocates the padding for. Handing a decoder an
780/// unpadded buffer would be an out-of-bounds read inside somebody
781/// else's bitstream reader, found by nobody.
782pub(crate) fn share_or_copy(
783  body: &crate::FfmpegBuffer,
784) -> std::result::Result<Packet, ffmpeg_next::Error> {
785  use ffmpeg_next::packet::Mut;
786
787  const PADDING: usize = ffmpeg_next::ffi::AV_INPUT_BUFFER_PADDING_SIZE as usize;
788
789  // **Empty first, before anything is dereferenced.** The empty carrier
790  // is deliberately backed by no buffer at all — that is what makes a
791  // placeholder plane slot free — so `as_av_buffer_ref` answers null
792  // for it, and reading `size` through that would be undefined on the
793  // most ordinary packet there is: a side-data-only one.
794  if body.is_empty() {
795    return try_packet_copy(&[]);
796  }
797
798  // Provenance before extent: the cheaper question, and the one that
799  // decides whether the other is worth asking.
800  if body.origin() != crate::view::Origin::PacketPayload {
801    return try_packet_copy(body.as_ref());
802  }
803
804  let buf = body.as_av_buffer_ref();
805  if buf.is_null() {
806    return try_packet_copy(body.as_ref());
807  }
808  // SAFETY: the carrier holds a live reference, just checked non-null;
809  // `size` is a public field on the `AVBufferRef` it names.
810  let capacity = unsafe { (*buf).size };
811  let end = body.offset().saturating_add(body.len());
812  let padded = capacity
813    .checked_sub(end)
814    .is_some_and(|slack| slack >= PADDING);
815
816  if !padded {
817    // No provable slack behind the view — copy, which allocates the
818    // padding libavcodec expects.
819    return try_packet_copy(body.as_ref());
820  }
821
822  let mut out = Packet::empty();
823  // SAFETY: `out` owns a live, zeroed `AVPacket`. `av_buffer_ref`
824  // returns a new reference to the same allocation — the packet owns
825  // that one and releases it on drop, while `body` keeps its own — and
826  // `data`/`size` are set to the view, which was proved to lie inside
827  // the buffer when the carrier was constructed.
828  unsafe {
829    let raw = out.as_mut_ptr();
830    let shared = ffmpeg_next::ffi::av_buffer_ref(buf.cast_mut());
831    if shared.is_null() {
832      return Err(ffmpeg_next::Error::Other {
833        errno: libc::ENOMEM,
834      });
835    }
836    (*raw).buf = shared;
837    (*raw).data = (*shared).data.add(body.offset());
838    (*raw).size = c_int::try_from(body.len()).map_err(|_| ffmpeg_next::Error::Other {
839      errno: libc::EINVAL,
840    })?;
841  }
842  Ok(out)
843}
844
845/// Builds an `ffmpeg::Packet` from a [`mediadecode::VideoPacket`]
846/// parameterized by [`crate::extras::VideoPacketExtra`] and
847/// `FfmpegBytes`.
848///
849/// The compressed bytes are **copied** into a new packet allocation.
850/// The return leg copies for the same reason the outbound one does:
851/// the carrier is Rust-owned memory with no `AVBufferRef` behind it to
852/// hand back.
853/// PTS / DTS / duration / flags / stream_index are propagated.
854///
855/// Side data on the extras is reattached to the rebuilt packet — see
856/// [`attach_side_data`] for why that is not optional.
857///
858/// Returns [`PacketBuildError`] on:
859/// * payload larger than `c_int::MAX` (would overflow `AVPacket.size`);
860/// * `av_new_packet` allocation failure (OOM);
861/// * a side-data entry this build of FFmpeg cannot name, or one whose
862///   allocation failed.
863pub fn ffmpeg_packet_from_video_packet(
864  packet: &mediadecode::packet::VideoPacket<VideoPacketExtra, crate::FfmpegBuffer>,
865  limits: PacketLimits,
866) -> std::result::Result<Packet, PacketBuildError> {
867  build_video_packet::<crate::View>(packet, limits, BodyRoute::Copy)
868}
869
870/// [`ffmpeg_packet_from_video_packet`] on the owned lane.
871pub fn ffmpeg_packet_from_owned_video_packet(
872  packet: &mediadecode::packet::VideoPacket<VideoPacketExtra, FfmpegBytes>,
873  limits: PacketLimits,
874) -> std::result::Result<Packet, PacketBuildError> {
875  build_video_packet::<crate::Owned>(packet, limits, BodyRoute::Copy)
876}
877
878/// [`ffmpeg_packet_from_video_packet`] built for **submission**, and
879/// scoped so the packet cannot outlive the call.
880///
881/// This is the only road on which the view lane may hand a decoder
882/// its own buffer rather than a copy, and the scoping is why it may.
883/// An `ffmpeg_next::Packet` lends `&mut [u8]` through `data_mut`,
884/// while the carrier it was built from still lends `&[u8]` — so a
885/// shared body reachable from a value a caller *holds* is two live
886/// references to one allocation with one of them mutable, out of
887/// entirely safe code, and `!Sync` would not save it either. Here the
888/// packet is built, lent to `submit` as `&Packet`, and dropped before
889/// this function returns: no value that can produce a `&mut` into the
890/// shared bytes ever exists.
891///
892/// **Which is why the route is the caller's to choose.** "Dropped
893/// before this returns" is a claim about this function, and a decoder
894/// that *records* what it is sent makes it false: the video decoder's
895/// hardware probe `av_packet_ref`s every accepted packet into a rescue
896/// history, and `FallbackFailed::unconsumed_packets` hands those
897/// recordings to the caller as owned, **mutable** `Packet`s. A
898/// submission that could be recorded must therefore carry
899/// [`BodyRoute::Copy`], so that what enters the history is storage
900/// nobody else reads. Callers with no history — every software road,
901/// and the hardware road after commit — pass
902/// [`BodyRoute::Submission`] and keep the zero-copy send.
903pub(crate) fn with_ffmpeg_video_packet<C: crate::FfmpegCarrier + crate::CarrierOps, T>(
904  packet: &mediadecode::packet::VideoPacket<VideoPacketExtra, C::Buffer>,
905  limits: PacketLimits,
906  route: BodyRoute,
907  submit: impl FnOnce(&Packet) -> T,
908) -> std::result::Result<T, PacketBuildError> {
909  let av_packet = build_video_packet::<C>(packet, limits, route)?;
910  let out = submit(&av_packet);
911  drop(av_packet);
912  Ok(out)
913}
914
915fn build_video_packet<C: crate::FfmpegCarrier + crate::CarrierOps>(
916  packet: &mediadecode::packet::VideoPacket<VideoPacketExtra, C::Buffer>,
917  limits: PacketLimits,
918  route: BodyRoute,
919) -> std::result::Result<Packet, PacketBuildError> {
920  let body = packet.data().as_ref();
921  // Before the budget and before the allocation: an uncarriable
922  // payload is not made carriable by fitting.
923  refuse_trusted(packet.flags(), body.len())?;
924  // And the annotations, judged whole before the body is allocated —
925  // a list that cannot be carried should not cost a packet first.
926  check_side_data_budget(packet.extra().side_data())?;
927  // **The into-FFmpeg budget, before the allocation.** `try_packet_copy`
928  // duplicates these bytes into a fresh `AVPacket` and checks only that
929  // they fit `c_int`. Without this the configured ceiling was dead on
930  // the road a caller feeds a decoder directly: bytes the demux
931  // boundary would have refused went straight into libavcodec.
932  if body.len() > limits.max_packet_bytes() {
933    return Err(PacketBuildError::SendPayloadTooLarge(
934      SendPayloadTooLarge::new(body.len(), limits.max_packet_bytes()),
935    ));
936  }
937  let mut out = C::packet_body(packet.data(), route)?;
938  attach_side_data(&mut out, packet.extra().side_data())?;
939  if let Some(ts) = packet.pts() {
940    out.set_pts(Some(ts.pts()));
941  }
942  if let Some(ts) = packet.dts() {
943    out.set_dts(Some(ts.pts()));
944  }
945  if let Some(d) = packet.duration() {
946    out.set_duration(d.pts());
947  }
948  // SAFETY: `out` owns the `AVPacket` `try_packet_copy` just built.
949  unsafe { write_md_flags(&mut out, packet.flags()) };
950  out.set_stream(packet.extra().stream_index() as usize);
951  Ok(out)
952}
953
954/// Builds an `ffmpeg::Packet` from a [`mediadecode::AudioPacket`].
955/// Same shape as [`ffmpeg_packet_from_video_packet`] — bytes are
956/// copied; pts/dts/duration/flags/stream_index and side data are
957/// forwarded. Same failure modes.
958pub fn ffmpeg_packet_from_audio_packet(
959  packet: &mediadecode::packet::AudioPacket<AudioPacketExtra, crate::FfmpegBuffer>,
960  limits: PacketLimits,
961) -> std::result::Result<Packet, PacketBuildError> {
962  build_audio_packet::<crate::View>(packet, limits, BodyRoute::Copy)
963}
964
965/// [`ffmpeg_packet_from_audio_packet`] on the owned lane.
966pub fn ffmpeg_packet_from_owned_audio_packet(
967  packet: &mediadecode::packet::AudioPacket<AudioPacketExtra, FfmpegBytes>,
968  limits: PacketLimits,
969) -> std::result::Result<Packet, PacketBuildError> {
970  build_audio_packet::<crate::Owned>(packet, limits, BodyRoute::Copy)
971}
972
973/// [`ffmpeg_packet_from_audio_packet`] built for **submission**, and
974/// scoped so the packet cannot outlive the call.
975///
976/// This is the only road on which the view lane may hand a decoder
977/// its own buffer rather than a copy, and the scoping is why it may.
978/// An `ffmpeg_next::Packet` lends `&mut [u8]` through `data_mut`,
979/// while the carrier it was built from still lends `&[u8]` — so a
980/// shared body reachable from a value a caller *holds* is two live
981/// references to one allocation with one of them mutable, out of
982/// entirely safe code, and `!Sync` would not save it either. Here the
983/// packet is built, lent to `submit` as `&Packet`, and dropped before
984/// this function returns: no value that can produce a `&mut` into the
985/// shared bytes ever exists.
986pub(crate) fn with_ffmpeg_audio_packet<C: crate::FfmpegCarrier + crate::CarrierOps, T>(
987  packet: &mediadecode::packet::AudioPacket<AudioPacketExtra, C::Buffer>,
988  limits: PacketLimits,
989  route: BodyRoute,
990  submit: impl FnOnce(&Packet) -> T,
991) -> std::result::Result<T, PacketBuildError> {
992  let av_packet = build_audio_packet::<C>(packet, limits, route)?;
993  let out = submit(&av_packet);
994  drop(av_packet);
995  Ok(out)
996}
997
998fn build_audio_packet<C: crate::FfmpegCarrier + crate::CarrierOps>(
999  packet: &mediadecode::packet::AudioPacket<AudioPacketExtra, C::Buffer>,
1000  limits: PacketLimits,
1001  route: BodyRoute,
1002) -> std::result::Result<Packet, PacketBuildError> {
1003  let body = packet.data().as_ref();
1004  // Before the budget and before the allocation: an uncarriable
1005  // payload is not made carriable by fitting.
1006  refuse_trusted(packet.flags(), body.len())?;
1007  // And the annotations, judged whole before the body is allocated —
1008  // a list that cannot be carried should not cost a packet first.
1009  check_side_data_budget(packet.extra().side_data())?;
1010  // **The into-FFmpeg budget, before the allocation.** `try_packet_copy`
1011  // duplicates these bytes into a fresh `AVPacket` and checks only that
1012  // they fit `c_int`. Without this the configured ceiling was dead on
1013  // the road a caller feeds a decoder directly: bytes the demux
1014  // boundary would have refused went straight into libavcodec.
1015  if body.len() > limits.max_packet_bytes() {
1016    return Err(PacketBuildError::SendPayloadTooLarge(
1017      SendPayloadTooLarge::new(body.len(), limits.max_packet_bytes()),
1018    ));
1019  }
1020  let mut out = C::packet_body(packet.data(), route)?;
1021  attach_side_data(&mut out, packet.extra().side_data())?;
1022  if let Some(ts) = packet.pts() {
1023    out.set_pts(Some(ts.pts()));
1024  }
1025  if let Some(ts) = packet.dts() {
1026    out.set_dts(Some(ts.pts()));
1027  }
1028  if let Some(d) = packet.duration() {
1029    out.set_duration(d.pts());
1030  }
1031  // SAFETY: `out` owns the `AVPacket` `try_packet_copy` just built.
1032  unsafe { write_md_flags(&mut out, packet.flags()) };
1033  out.set_stream(packet.extra().stream_index() as usize);
1034  Ok(out)
1035}
1036
1037/// Builds an `ffmpeg::Packet` from a [`mediadecode::SubtitlePacket`].
1038/// Bytes copied; pts/duration/flags/stream_index and side data
1039/// forwarded. Subtitle packets have no `dts` in the mediadecode model.
1040/// Same failure modes as [`ffmpeg_packet_from_video_packet`].
1041pub fn ffmpeg_packet_from_subtitle_packet(
1042  packet: &mediadecode::packet::SubtitlePacket<SubtitlePacketExtra, crate::FfmpegBuffer>,
1043  limits: PacketLimits,
1044) -> std::result::Result<Packet, PacketBuildError> {
1045  build_subtitle_packet::<crate::View>(packet, limits, BodyRoute::Copy)
1046}
1047
1048/// [`ffmpeg_packet_from_subtitle_packet`] on the owned lane.
1049pub fn ffmpeg_packet_from_owned_subtitle_packet(
1050  packet: &mediadecode::packet::SubtitlePacket<SubtitlePacketExtra, FfmpegBytes>,
1051  limits: PacketLimits,
1052) -> std::result::Result<Packet, PacketBuildError> {
1053  build_subtitle_packet::<crate::Owned>(packet, limits, BodyRoute::Copy)
1054}
1055
1056/// [`ffmpeg_packet_from_subtitle_packet`] built for **submission**, and
1057/// scoped so the packet cannot outlive the call.
1058///
1059/// This is the only road on which the view lane may hand a decoder
1060/// its own buffer rather than a copy, and the scoping is why it may.
1061/// An `ffmpeg_next::Packet` lends `&mut [u8]` through `data_mut`,
1062/// while the carrier it was built from still lends `&[u8]` — so a
1063/// shared body reachable from a value a caller *holds* is two live
1064/// references to one allocation with one of them mutable, out of
1065/// entirely safe code, and `!Sync` would not save it either. Here the
1066/// packet is built, lent to `submit` as `&Packet`, and dropped before
1067/// this function returns: no value that can produce a `&mut` into the
1068/// shared bytes ever exists.
1069pub(crate) fn with_ffmpeg_subtitle_packet<C: crate::FfmpegCarrier + crate::CarrierOps, T>(
1070  packet: &mediadecode::packet::SubtitlePacket<SubtitlePacketExtra, C::Buffer>,
1071  limits: PacketLimits,
1072  route: BodyRoute,
1073  submit: impl FnOnce(&Packet) -> T,
1074) -> std::result::Result<T, PacketBuildError> {
1075  let av_packet = build_subtitle_packet::<C>(packet, limits, route)?;
1076  let out = submit(&av_packet);
1077  drop(av_packet);
1078  Ok(out)
1079}
1080
1081fn build_subtitle_packet<C: crate::FfmpegCarrier + crate::CarrierOps>(
1082  packet: &mediadecode::packet::SubtitlePacket<SubtitlePacketExtra, C::Buffer>,
1083  limits: PacketLimits,
1084  route: BodyRoute,
1085) -> std::result::Result<Packet, PacketBuildError> {
1086  let body = packet.data().as_ref();
1087  // Before the budget and before the allocation: an uncarriable
1088  // payload is not made carriable by fitting.
1089  refuse_trusted(packet.flags(), body.len())?;
1090  // And the annotations, judged whole before the body is allocated —
1091  // a list that cannot be carried should not cost a packet first.
1092  check_side_data_budget(packet.extra().side_data())?;
1093  // **The into-FFmpeg budget, before the allocation.** `try_packet_copy`
1094  // duplicates these bytes into a fresh `AVPacket` and checks only that
1095  // they fit `c_int`. Without this the configured ceiling was dead on
1096  // the road a caller feeds a decoder directly: bytes the demux
1097  // boundary would have refused went straight into libavcodec.
1098  if body.len() > limits.max_packet_bytes() {
1099    return Err(PacketBuildError::SendPayloadTooLarge(
1100      SendPayloadTooLarge::new(body.len(), limits.max_packet_bytes()),
1101    ));
1102  }
1103  let mut out = C::packet_body(packet.data(), route)?;
1104  attach_side_data(&mut out, packet.extra().side_data())?;
1105  if let Some(ts) = packet.pts() {
1106    out.set_pts(Some(ts.pts()));
1107  }
1108  if let Some(d) = packet.duration() {
1109    out.set_duration(d.pts());
1110  }
1111  // SAFETY: `out` owns the `AVPacket` `try_packet_copy` just built.
1112  unsafe { write_md_flags(&mut out, packet.flags()) };
1113  out.set_stream(packet.extra().stream_index() as usize);
1114  Ok(out)
1115}
1116
1117// ---------------------------------------------------------------------------
1118//  Safe wrappers — `&ffmpeg::Packet` → `mediadecode::*Packet`.
1119// ---------------------------------------------------------------------------
1120
1121/// Carries a borrowed [`ffmpeg::Packet`] out as a
1122/// [`mediadecode::packet::VideoPacket`] on the **view** lane: the
1123/// payload is a refcounted window onto the source `AVPacket`'s own
1124/// buffer, which is why the delivered packet's lifetime is answerable
1125/// to libavformat's — see [the two carrier lanes][lanes]. For a packet
1126/// that must outlive the demuxer, ask for the owned lane by name:
1127/// `video_packet_from_ffmpeg_as::<Owned>`, whose payload is copied and
1128/// which the [D-seat amputation contract][law] governs.
1129///
1130/// Timestamps, duration, key/corrupt flags, and the source stream index
1131/// are forwarded to the produced packet.
1132///
1133/// Uses the default [`PacketLimits`]; the `_in` sibling takes them
1134/// explicitly, alongside the stream's timebase.
1135///
1136/// [lanes]: mediadecode::adapter#the-two-carrier-lanes
1137///
1138/// Returns `Ok(None)` when the source packet has no payload at all
1139/// (an empty packet — typical after EOF), and [`PacketBufferError`]
1140/// when a payload that *is* there could not be carried — over budget,
1141/// or claiming bytes outside its own buffer. Those are never the same
1142/// answer. Caller can also fill in [`VideoPacketExtra::byte_pos`] /
1143/// `side_data` post-construction if they need those.
1144///
1145/// [law]: mediadecode::adapter#the-d-seat-amputation-contract
1146pub fn video_packet_from_ffmpeg(
1147  packet: &Packet,
1148  time_base: mediadecode::Timebase,
1149) -> Result<Option<VideoPacket<VideoPacketExtra, FfmpegBytes>>, PacketBufferError> {
1150  video_packet_from_borrowed::<crate::Owned>(
1151    packet,
1152    time_base,
1153    PacketLimits::default(),
1154    crate::buffer::PayloadProvenance::CallerSupplied,
1155  )
1156}
1157
1158/// Carries a borrowed [`ffmpeg::Packet`] out as a
1159/// [`mediadecode::packet::AudioPacket`]. Same shape as
1160/// [`video_packet_from_ffmpeg`] — shared payload, forwarded metadata,
1161/// default budgets.
1162pub fn audio_packet_from_ffmpeg(
1163  packet: &Packet,
1164  time_base: mediadecode::Timebase,
1165) -> Result<Option<AudioPacket<AudioPacketExtra, FfmpegBytes>>, PacketBufferError> {
1166  audio_packet_from_borrowed::<crate::Owned>(
1167    packet,
1168    time_base,
1169    PacketLimits::default(),
1170    crate::buffer::PayloadProvenance::CallerSupplied,
1171  )
1172}
1173
1174/// Carries a borrowed [`ffmpeg::Packet`] out as a
1175/// [`mediadecode::packet::SubtitlePacket`]. Subtitle packets have no
1176/// `dts` in the mediadecode model; everything else mirrors
1177/// [`video_packet_from_ffmpeg`], shared payload included.
1178pub fn subtitle_packet_from_ffmpeg(
1179  packet: &Packet,
1180  time_base: mediadecode::Timebase,
1181) -> Result<Option<SubtitlePacket<SubtitlePacketExtra, FfmpegBytes>>, PacketBufferError> {
1182  subtitle_packet_from_borrowed::<crate::Owned>(
1183    packet,
1184    time_base,
1185    PacketLimits::default(),
1186    crate::buffer::PayloadProvenance::CallerSupplied,
1187  )
1188}
1189
1190/// The most side-data entries this crate will walk on one packet.
1191///
1192/// The floor is [`SIDE_DATA_MAX_ENTRIES`], the same bound the
1193/// frame-side collector uses; it rises with `AV_PKT_DATA_NB` so a
1194/// future FFmpeg that names more side-data types than the floor cannot
1195/// turn a legitimate packet into a refusal. Measured: FFmpeg's own
1196/// packet API cannot exceed one entry per named type — both
1197/// `av_packet_new_side_data` and `av_packet_add_side_data` replace an
1198/// existing entry of the same type — so a packet over this cap is one
1199/// no FFmpeg call produced.
1200fn side_data_entry_cap() -> usize {
1201  SIDE_DATA_MAX_ENTRIES.max(crate::ffi::side_data_type_count().max(0) as usize)
1202}
1203
1204/// The side-data entries an `AVPacket` carries, copied into owned
1205/// values — **all of them, or none and an error**.
1206///
1207/// The packet twin of `convert::collect_side_data`, and bounded the
1208/// same way: at most [`side_data_entry_cap`] entries and
1209/// [`SIDE_DATA_MAX_TOTAL_BYTES`] bytes per packet, allocated through
1210/// `try_reserve_exact`. What is *not* the same is what happens when a
1211/// bound is reached. The frame collector truncates and warns, which it
1212/// can afford to — frame side data is descriptive, the frame is
1213/// delivered either way, and nothing downstream acts on it. Packet side
1214/// data is the opposite: `NEW_EXTRADATA` replaces a decoder's
1215/// parameters, `PARAM_CHANGE` moves its rate, `SKIP_SAMPLES` trims the
1216/// stream, and the codec acts on every one. A truncated copy is a
1217/// decoder quietly running on stale parameters, and a truncated copy of
1218/// a side-data-only packet is `Ok(None)` — the packet vanishing
1219/// entirely, which is the very defect this seam was built to close. So
1220/// every bound here is an error, and a caller either gets a packet with
1221/// all of its side data or a `DemuxError` naming what stopped it.
1222///
1223/// `AVPacket.side_data` is a flat array of `AVPacketSideData` (not the
1224/// array of pointers an `AVFrame` keeps), and its `type_` is read as
1225/// the integer it is on the wire — a discriminant this build has no
1226/// name for would be undefined behaviour the moment it existed as an
1227/// `AVPacketSideDataType`.
1228fn packet_side_data(packet: &Packet) -> Result<Vec<SideDataEntry>, PacketBufferError> {
1229  use ffmpeg_next::packet::Ref;
1230  // SAFETY: `packet` keeps the `AVPacket` live; `side_data` and
1231  // `side_data_elems` are public fields.
1232  let count_raw = unsafe { (*packet.as_ptr()).side_data_elems };
1233  let entries = unsafe { (*packet.as_ptr()).side_data };
1234  // Zero entries is the only shape that means "no side data". Every
1235  // other reading of that answer — a malformed count, a missing array —
1236  // is judged, and judged *before* the pointer: a count this crate
1237  // cannot walk stays an error whether or not the array happens to be
1238  // null, and a null array with entries to read is malformed rather
1239  // than empty. Both used to leave here as `Ok(vec![])`, which is the
1240  // silent loss the caps taught us to name, reached through the
1241  // pointer instead of the budget.
1242  if count_raw == 0 {
1243    return Ok(Vec::new());
1244  }
1245  let cap = side_data_entry_cap();
1246  if count_raw < 0 || count_raw as usize > cap {
1247    return Err(PacketBufferError::SideDataEntries(SideDataEntries::new(
1248      count_raw, cap,
1249    )));
1250  }
1251  if entries.is_null() {
1252    return Err(PacketBufferError::SideDataArray(SideDataArray::new(
1253      count_raw,
1254    )));
1255  }
1256  let count = count_raw as usize;
1257  let mut out: Vec<SideDataEntry> = Vec::new();
1258  if out.try_reserve_exact(count).is_err() {
1259    return Err(PacketBufferError::SideDataAlloc(BufferSideDataAlloc::new(
1260      count * core::mem::size_of::<SideDataEntry>(),
1261    )));
1262  }
1263  let mut total_bytes: usize = 0;
1264  for index in 0..count {
1265    // SAFETY: `entries` is valid for `count_raw` contiguous
1266    // `AVPacketSideData` values per FFmpeg's contract, and `index` is
1267    // below that count.
1268    let entry = unsafe { entries.add(index) };
1269    let kind = unsafe { read_unaligned(addr_of!((*entry).type_).cast::<i32>()) };
1270    let size = unsafe { (*entry).size };
1271    let data_ptr = unsafe { (*entry).data };
1272    let data = if size == 0 {
1273      // A marker entry: a type and no bytes. FFmpeg emits these, and
1274      // there is nothing to carry or to charge the budget for.
1275      FfmpegBytes::empty()
1276    } else if data_ptr.is_null() {
1277      // Bytes declared and not carried. Reading this as an empty entry
1278      // delivered a packet whose side data was a lie, and charged the
1279      // budget nothing for it.
1280      return Err(PacketBufferError::SideDataPayload(SideDataPayload::new(
1281        index, size,
1282      )));
1283    } else {
1284      total_bytes = total_bytes.saturating_add(size);
1285      if total_bytes > SIDE_DATA_MAX_TOTAL_BYTES {
1286        return Err(PacketBufferError::SideDataBytes(SideDataBytes::new(
1287          total_bytes,
1288          SIDE_DATA_MAX_TOTAL_BYTES,
1289        )));
1290      }
1291      // **One allocation, and a fallible one.** This used to stage
1292      // through a `try_reserve_exact`ed `Vec` and then copy again into
1293      // the carrier — two payload-sized allocations, the second
1294      // infallible — with a comment conceding the doubling was
1295      // affordable only because side data is capped. The carrier
1296      // allocates fallibly itself now, so the staging bought nothing
1297      // and is gone with it.
1298      //
1299      // SAFETY: `data` is valid for `size` bytes per FFmpeg's
1300      // `AVPacketSideData` contract.
1301      FfmpegBytes::try_copy_from_slice(unsafe { core::slice::from_raw_parts(data_ptr, size) })
1302        .ok_or(PacketBufferError::SideDataAlloc(BufferSideDataAlloc::new(
1303          size,
1304        )))?
1305    };
1306    out.push(SideDataEntry::new(kind, data));
1307  }
1308  Ok(out)
1309}
1310
1311/// The buffer a timed packet is delivered with, or `None` when there is
1312/// no packet to deliver at all.
1313///
1314/// **`size == 0` is not the same as "nothing".** A packet with no body
1315/// and one or more side-data entries is a real packet: FFmpeg uses that
1316/// shape for `AV_PKT_DATA_NEW_EXTRADATA` and for a parameter change,
1317/// and a decoder that never sees it keeps decoding on parameters the
1318/// container has already replaced. Such a packet is delivered with an
1319/// owned empty buffer — zero bytes, but a buffer, so the packet exists
1320/// and its side data rides the extras.
1321///
1322/// `Ok(None)` therefore means the packet carried neither a payload nor
1323/// side data: the empty marker, and the only thing a pull loop may skip.
1324fn delivered_payload<C: crate::FfmpegCarrier + crate::CarrierOps>(
1325  packet: &Packet,
1326  side_data: &[SideDataEntry],
1327  limits: PacketLimits,
1328  provenance: crate::buffer::PayloadProvenance,
1329) -> Result<Option<C::Buffer>, PacketBufferError> {
1330  use ffmpeg_next::packet::Ref;
1331  // SAFETY: `packet` keeps the AVPacket live for the duration of this
1332  // call, which is all `payload_of` requires.
1333  if let Some(bytes) = unsafe {
1334    crate::buffer::payload_of::<C>(packet.as_ptr(), limits.max_packet_bytes(), provenance)
1335  }? {
1336    return Ok(Some(bytes));
1337  }
1338  if side_data.is_empty() {
1339    return Ok(None);
1340  }
1341  Ok(Some(C::empty()))
1342}
1343
1344// ---------------------------------------------------------------------------
1345//  Timebase-carrying variants.
1346//
1347//  An `AVPacket`'s timestamps are integers in its *stream's* timebase,
1348//  which the packet does not carry. The functions above therefore ask
1349//  the caller for it — they used to stamp `Timebase::default()` (1/1)
1350//  instead, which turned PTS 90,000 from a 1/90,000 stream into a
1351//  self-describing timestamp reading 90,000 *seconds*, with nothing in
1352//  the value marking 1/1 as a placeholder rather than a reading. A
1353//  demuxer knows the real one: it holds the track table. The variants
1354//  below add explicit budgets on top of the same requirement.
1355// ---------------------------------------------------------------------------
1356
1357/// [`video_packet_from_ffmpeg`], with explicit carrier and
1358/// provenance seats on top of the same timebase requirement.
1359pub(crate) fn video_packet_from_ffmpeg_as<C: crate::FfmpegCarrier + crate::CarrierOps>(
1360  source: Packet,
1361  time_base: mediadecode::Timebase,
1362  limits: PacketLimits,
1363  provenance: crate::buffer::PayloadProvenance,
1364) -> Result<Option<VideoPacket<VideoPacketExtra, C::Buffer>>, PacketBufferError> {
1365  // **The source is consumed.** A borrowed `AVPacket` cannot be
1366  // safely viewed: `ffmpeg_next::Packet` lends `&mut [u8]` through
1367  // `data_mut` and shares its buffer by refcount with no
1368  // copy-on-write, so a caller who kept the packet would hold a
1369  // mutable alias of every byte the returned carrier reads — and
1370  // both sides are `Send`. Taking it by value is what makes that
1371  // unconstructible; the packet is released when this returns, and
1372  // the view lane's carrier keeps the buffer alive by its own
1373  // reference. See the borrowed siblings for the owned lane, which
1374  // copies and so may borrow.
1375  video_packet_from_borrowed::<C>(&source, time_base, limits, provenance)
1376}
1377
1378/// The shared implementation of the video road.
1379///
1380/// Crate-private, and borrowed: the two public doors differ only in
1381/// what they owe the borrow checker. The consuming one may be asked
1382/// for either lane; the borrowing one is the owned lane, where the
1383/// bytes are copied and the source is nobody's concern afterwards.
1384pub(crate) fn video_packet_from_borrowed<C: crate::FfmpegCarrier + crate::CarrierOps>(
1385  packet: &Packet,
1386  time_base: mediadecode::Timebase,
1387  limits: PacketLimits,
1388  provenance: crate::buffer::PayloadProvenance,
1389) -> Result<Option<VideoPacket<VideoPacketExtra, C::Buffer>>, PacketBufferError> {
1390  let side_data = packet_side_data(packet)?;
1391  let Some(buf) = delivered_payload::<C>(packet, &side_data, limits, provenance)? else {
1392    return Ok(None);
1393  };
1394  let extra = VideoPacketExtra::new(packet.stream() as i32).with_side_data(side_data);
1395  let mut out = VideoPacket::new(buf, extra)
1396    .with_flags(md_flags_from_packet(packet)?)
1397    .with_pts(packet.pts().map(|p| Timestamp::new(p, time_base)))
1398    .with_dts(packet.dts().map(|d| Timestamp::new(d, time_base)));
1399  let dur = packet.duration();
1400  if dur > 0 {
1401    out = out.with_duration(Some(Timestamp::new(dur, time_base)));
1402  }
1403  Ok(Some(out))
1404}
1405
1406/// [`audio_packet_from_ffmpeg`], with explicit carrier and
1407/// provenance seats on top of the same timebase requirement.
1408pub(crate) fn audio_packet_from_ffmpeg_as<C: crate::FfmpegCarrier + crate::CarrierOps>(
1409  source: Packet,
1410  time_base: mediadecode::Timebase,
1411  limits: PacketLimits,
1412  provenance: crate::buffer::PayloadProvenance,
1413) -> Result<Option<AudioPacket<AudioPacketExtra, C::Buffer>>, PacketBufferError> {
1414  // **The source is consumed.** A borrowed `AVPacket` cannot be
1415  // safely viewed: `ffmpeg_next::Packet` lends `&mut [u8]` through
1416  // `data_mut` and shares its buffer by refcount with no
1417  // copy-on-write, so a caller who kept the packet would hold a
1418  // mutable alias of every byte the returned carrier reads — and
1419  // both sides are `Send`. Taking it by value is what makes that
1420  // unconstructible; the packet is released when this returns, and
1421  // the view lane's carrier keeps the buffer alive by its own
1422  // reference. See the borrowed siblings for the owned lane, which
1423  // copies and so may borrow.
1424  audio_packet_from_borrowed::<C>(&source, time_base, limits, provenance)
1425}
1426
1427/// The shared implementation of the audio road.
1428///
1429/// Crate-private, and borrowed: the two public doors differ only in
1430/// what they owe the borrow checker. The consuming one may be asked
1431/// for either lane; the borrowing one is the owned lane, where the
1432/// bytes are copied and the source is nobody's concern afterwards.
1433pub(crate) fn audio_packet_from_borrowed<C: crate::FfmpegCarrier + crate::CarrierOps>(
1434  packet: &Packet,
1435  time_base: mediadecode::Timebase,
1436  limits: PacketLimits,
1437  provenance: crate::buffer::PayloadProvenance,
1438) -> Result<Option<AudioPacket<AudioPacketExtra, C::Buffer>>, PacketBufferError> {
1439  let side_data = packet_side_data(packet)?;
1440  let Some(buf) = delivered_payload::<C>(packet, &side_data, limits, provenance)? else {
1441    return Ok(None);
1442  };
1443  let extra = AudioPacketExtra::new(packet.stream() as i32).with_side_data(side_data);
1444  let mut out = AudioPacket::new(buf, extra)
1445    .with_flags(md_flags_from_packet(packet)?)
1446    .with_pts(packet.pts().map(|p| Timestamp::new(p, time_base)))
1447    .with_dts(packet.dts().map(|d| Timestamp::new(d, time_base)));
1448  let dur = packet.duration();
1449  if dur > 0 {
1450    out = out.with_duration(Some(Timestamp::new(dur, time_base)));
1451  }
1452  Ok(Some(out))
1453}
1454
1455/// [`subtitle_packet_from_ffmpeg`], with explicit carrier and
1456/// provenance seats on top of the same timebase requirement.
1457pub(crate) fn subtitle_packet_from_ffmpeg_as<C: crate::FfmpegCarrier + crate::CarrierOps>(
1458  source: Packet,
1459  time_base: mediadecode::Timebase,
1460  limits: PacketLimits,
1461  provenance: crate::buffer::PayloadProvenance,
1462) -> Result<Option<SubtitlePacket<SubtitlePacketExtra, C::Buffer>>, PacketBufferError> {
1463  // **The source is consumed.** A borrowed `AVPacket` cannot be
1464  // safely viewed: `ffmpeg_next::Packet` lends `&mut [u8]` through
1465  // `data_mut` and shares its buffer by refcount with no
1466  // copy-on-write, so a caller who kept the packet would hold a
1467  // mutable alias of every byte the returned carrier reads — and
1468  // both sides are `Send`. Taking it by value is what makes that
1469  // unconstructible; the packet is released when this returns, and
1470  // the view lane's carrier keeps the buffer alive by its own
1471  // reference. See the borrowed siblings for the owned lane, which
1472  // copies and so may borrow.
1473  subtitle_packet_from_borrowed::<C>(&source, time_base, limits, provenance)
1474}
1475
1476/// The shared implementation of the subtitle road.
1477///
1478/// Crate-private, and borrowed: the two public doors differ only in
1479/// what they owe the borrow checker. The consuming one may be asked
1480/// for either lane; the borrowing one is the owned lane, where the
1481/// bytes are copied and the source is nobody's concern afterwards.
1482pub(crate) fn subtitle_packet_from_borrowed<C: crate::FfmpegCarrier + crate::CarrierOps>(
1483  packet: &Packet,
1484  time_base: mediadecode::Timebase,
1485  limits: PacketLimits,
1486  provenance: crate::buffer::PayloadProvenance,
1487) -> Result<Option<SubtitlePacket<SubtitlePacketExtra, C::Buffer>>, PacketBufferError> {
1488  let side_data = packet_side_data(packet)?;
1489  let Some(buf) = delivered_payload::<C>(packet, &side_data, limits, provenance)? else {
1490    return Ok(None);
1491  };
1492  let extra = SubtitlePacketExtra::new(packet.stream() as i32).with_side_data(side_data);
1493  let mut out = SubtitlePacket::new(buf, extra)
1494    .with_flags(md_flags_from_packet(packet)?)
1495    .with_pts(packet.pts().map(|p| Timestamp::new(p, time_base)));
1496  let dur = packet.duration();
1497  if dur > 0 {
1498    out = out.with_duration(Some(Timestamp::new(dur, time_base)));
1499  }
1500  Ok(Some(out))
1501}
1502
1503/// Wraps a borrowed [`ffmpeg::Packet`] from a **data** track — timecode,
1504/// KLV, timed ID3 — as a [`mediadecode::demuxer::DataPacket`], with the
1505/// stream's timebase stamped onto every timestamp.
1506///
1507/// Data packets are never reordered, so the mediadecode model gives
1508/// them no `dts` seat; everything else mirrors
1509/// [`video_packet_from_ffmpeg_in`]. `byte_pos` is forwarded from
1510/// `AVPacket.pos`, which data consumers use to correlate a payload with
1511/// its position in the file.
1512pub(crate) fn data_packet_from_ffmpeg_as<C: crate::FfmpegCarrier + crate::CarrierOps>(
1513  source: Packet,
1514  time_base: mediadecode::Timebase,
1515  limits: PacketLimits,
1516  provenance: crate::buffer::PayloadProvenance,
1517) -> Result<Option<DataPacket<DataPacketExtra, C::Buffer>>, PacketBufferError> {
1518  // **The source is consumed.** A borrowed `AVPacket` cannot be
1519  // safely viewed: `ffmpeg_next::Packet` lends `&mut [u8]` through
1520  // `data_mut` and shares its buffer by refcount with no
1521  // copy-on-write, so a caller who kept the packet would hold a
1522  // mutable alias of every byte the returned carrier reads — and
1523  // both sides are `Send`. Taking it by value is what makes that
1524  // unconstructible; the packet is released when this returns, and
1525  // the view lane's carrier keeps the buffer alive by its own
1526  // reference. See the borrowed siblings for the owned lane, which
1527  // copies and so may borrow.
1528  data_packet_from_borrowed::<C>(&source, time_base, limits, provenance)
1529}
1530
1531/// The shared implementation of the data road.
1532///
1533/// Crate-private, and borrowed: the two public doors differ only in
1534/// what they owe the borrow checker. The consuming one may be asked
1535/// for either lane; the borrowing one is the owned lane, where the
1536/// bytes are copied and the source is nobody's concern afterwards.
1537pub(crate) fn data_packet_from_borrowed<C: crate::FfmpegCarrier + crate::CarrierOps>(
1538  packet: &Packet,
1539  time_base: mediadecode::Timebase,
1540  limits: PacketLimits,
1541  provenance: crate::buffer::PayloadProvenance,
1542) -> Result<Option<DataPacket<DataPacketExtra, C::Buffer>>, PacketBufferError> {
1543  let side_data = packet_side_data(packet)?;
1544  let Some(buf) = delivered_payload::<C>(packet, &side_data, limits, provenance)? else {
1545    return Ok(None);
1546  };
1547  let pos = packet.position();
1548  let extra = DataPacketExtra::new(packet.stream() as i32)
1549    .with_byte_pos((pos >= 0).then_some(pos as i64))
1550    .with_side_data(side_data);
1551  let mut out = DataPacket::new(buf, extra)
1552    .with_flags(md_flags_from_packet(packet)?)
1553    .with_pts(packet.pts().map(|p| Timestamp::new(p, time_base)));
1554  let dur = packet.duration();
1555  if dur > 0 {
1556    out = out.with_duration(Some(Timestamp::new(dur, time_base)));
1557  }
1558  Ok(Some(out))
1559}
1560
1561/// Wraps a borrowed [`ffmpeg::Packet`] from an **attachment** track —
1562/// cover art that the container really does store as a packet — as a
1563/// [`mediadecode::demuxer::AttachmentPacket`].
1564///
1565/// No timestamps are forwarded, and there is nowhere to put them: an
1566/// attachment is not on the timeline. `synthesized` is `false`, because
1567/// this payload came from a real packet; the demuxer sets it `true` for
1568/// the packets it builds out of codec extradata.
1569///
1570/// **The one arm where a payload-less packet really is nothing.** The
1571/// four timed conversions deliver a side-data-only packet, because
1572/// codec-control side data is content a decoder must see. An attachment
1573/// is not decoded and is not on a timeline: it *is* its bytes, so a
1574/// packet carrying none carries no attachment, and this answers
1575/// `Ok(None)`. `AttachmentPacketExtra` accordingly has no side-data
1576/// seat — a deliberate absence, not an oversight.
1577pub(crate) fn attachment_packet_from_ffmpeg_as<C: crate::FfmpegCarrier + crate::CarrierOps>(
1578  source: Packet,
1579  limits: PacketLimits,
1580  provenance: crate::buffer::PayloadProvenance,
1581) -> Result<Option<AttachmentPacket<AttachmentPacketExtra, C::Buffer>>, PacketBufferError> {
1582  // **The source is consumed.** A borrowed `AVPacket` cannot be
1583  // safely viewed: `ffmpeg_next::Packet` lends `&mut [u8]` through
1584  // `data_mut` and shares its buffer by refcount with no
1585  // copy-on-write, so a caller who kept the packet would hold a
1586  // mutable alias of every byte the returned carrier reads — and
1587  // both sides are `Send`. Taking it by value is what makes that
1588  // unconstructible; the packet is released when this returns, and
1589  // the view lane's carrier keeps the buffer alive by its own
1590  // reference. See the borrowed siblings for the owned lane, which
1591  // copies and so may borrow.
1592  attachment_packet_from_borrowed::<C>(&source, limits, provenance)
1593}
1594
1595/// The shared implementation of the attachment road.
1596///
1597/// Crate-private, and borrowed: the two public doors differ only in
1598/// what they owe the borrow checker. The consuming one may be asked
1599/// for either lane; the borrowing one is the owned lane, where the
1600/// bytes are copied and the source is nobody's concern afterwards.
1601pub(crate) fn attachment_packet_from_borrowed<C: crate::FfmpegCarrier + crate::CarrierOps>(
1602  packet: &Packet,
1603  limits: PacketLimits,
1604  provenance: crate::buffer::PayloadProvenance,
1605) -> Result<Option<AttachmentPacket<AttachmentPacketExtra, C::Buffer>>, PacketBufferError> {
1606  use ffmpeg_next::packet::Ref;
1607  // SAFETY: `packet` keeps the AVPacket live for the duration of this
1608  // call, which is all `payload_of` requires.
1609  let Some(buf) = (unsafe {
1610    crate::buffer::payload_of::<C>(packet.as_ptr(), limits.max_packet_bytes(), provenance)
1611  })?
1612  else {
1613    return Ok(None);
1614  };
1615  Ok(Some(
1616    AttachmentPacket::new(buf, AttachmentPacketExtra::new(packet.stream() as i32))
1617      .with_flags(md_flags_from_packet(packet)?),
1618  ))
1619}
1620
1621/// Every flag the packet really carries.
1622///
1623/// Read from `AVPacket.flags` as the raw integer rather than through
1624/// `ffmpeg_next`'s `Packet::flags()`, whose `Flags` bit set names only
1625/// `KEY` and `CORRUPT` and drops the rest in `from_bits_truncate`.
1626/// `AV_PKT_FLAG_DISCARD` is among the dropped: it tells a consumer that
1627/// a packet must be fed to the decoder and its output thrown away, and
1628/// losing it makes preroll output look like something to keep.
1629///
1630/// `PacketFlags` is a bit set whose documented lossless door is
1631/// `from_bits_retain`, and every packet flag FFmpeg names lives inside
1632/// the byte it carries — including the three bits nothing names yet.
1633/// A bit outside that byte cannot be carried at all, and is refused
1634/// rather than dropped; the assertion below states the fact that keeps
1635/// the refusal unreachable against this build.
1636fn md_flags_from_packet(packet: &Packet) -> Result<MdPacketFlags, PacketBufferError> {
1637  use ffmpeg_next::packet::Ref;
1638  // SAFETY: `packet` keeps the `AVPacket` live for the call, which is
1639  // all the raw reader asks.
1640  unsafe { md_flags_from_av_packet(packet.as_ptr()) }
1641}
1642
1643/// [`md_flags_from_packet`] for an `AVPacket` that has no safe wrapper
1644/// — the one libavformat embeds in an `AVStream` for cover art.
1645///
1646/// The demuxer hoists that packet by hand, and hoisting it *without*
1647/// its flags was how an attached picture arrived with none: FFmpeg
1648/// marks it `AV_PKT_FLAG_KEY`, which is the one thing a still image is
1649/// certain to be. One reader, so a second construction site cannot
1650/// quietly disagree with the five that go through the boundary.
1651///
1652/// # Safety
1653///
1654/// `pkt` must be a live `*const AVPacket` for the duration of this
1655/// call.
1656pub(crate) unsafe fn md_flags_from_av_packet(
1657  pkt: *const ffmpeg_next::ffi::AVPacket,
1658) -> Result<MdPacketFlags, PacketBufferError> {
1659  // SAFETY: `pkt` is live per the contract above; `flags` is a public
1660  // field and a plain `c_int`.
1661  let raw = unsafe { (*pkt).flags };
1662  let carried = i32::from(u8::MAX);
1663  if raw & !carried != 0 {
1664    return Err(PacketBufferError::UnrepresentableFlags(
1665      UnrepresentableFlags::new(raw),
1666    ));
1667  }
1668  Ok(MdPacketFlags::from_bits_retain(raw as u8))
1669}
1670
1671/// What a track is, folded from `AVCodecParameters.codec_type` read as
1672/// the integer it is on the wire.
1673///
1674/// The dependency-API half of this crate's open-C-enum discipline.
1675/// `ffmpeg_next::codec::Parameters::medium()` materialises an
1676/// `AVMediaType` out of FFmpeg memory to answer the same question, and
1677/// a value outside this build's discriminant set is undefined behaviour
1678/// the moment it exists — before any `match` on it can run. That the
1679/// set is small and has been stable for years is a reason it has not
1680/// bitten, not a reason it cannot.
1681///
1682/// So the read is raw and the fold is total: anything this build does
1683/// not name becomes [`Unknown`](Self::Unknown), which the callers
1684/// already had to handle.
1685#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant)]
1686pub enum MediaKind {
1687  /// `AVMEDIA_TYPE_VIDEO`.
1688  Video,
1689  /// `AVMEDIA_TYPE_AUDIO`.
1690  Audio,
1691  /// `AVMEDIA_TYPE_SUBTITLE`.
1692  Subtitle,
1693  /// `AVMEDIA_TYPE_DATA`.
1694  Data,
1695  /// `AVMEDIA_TYPE_ATTACHMENT`.
1696  Attachment,
1697  /// Anything this build of FFmpeg does not name, `AVMEDIA_TYPE_UNKNOWN`
1698  /// included.
1699  Unknown,
1700}
1701
1702/// Folds a raw `AVMediaType` integer into [`MediaKind`].
1703pub(crate) fn media_kind_from_raw(raw: i32) -> MediaKind {
1704  use ffmpeg_next::ffi::AVMediaType::*;
1705  match raw {
1706    x if x == AVMEDIA_TYPE_VIDEO as i32 => MediaKind::Video,
1707    x if x == AVMEDIA_TYPE_AUDIO as i32 => MediaKind::Audio,
1708    x if x == AVMEDIA_TYPE_SUBTITLE as i32 => MediaKind::Subtitle,
1709    x if x == AVMEDIA_TYPE_DATA as i32 => MediaKind::Data,
1710    x if x == AVMEDIA_TYPE_ATTACHMENT as i32 => MediaKind::Attachment,
1711    _ => MediaKind::Unknown,
1712  }
1713}
1714
1715/// The medium a set of codec parameters declares, without forming an
1716/// `AVMediaType`. Replaces every `Parameters::medium()` call in this
1717/// crate — see [`MediaKind`].
1718pub(crate) fn media_kind_of(parameters: &ffmpeg_next::codec::Parameters) -> MediaKind {
1719  // SAFETY: `as_ptr` is `unsafe` only because the pointer must not
1720  // outlive `parameters`; it is used and discarded inside this call.
1721  let ptr = unsafe { parameters.as_ptr() };
1722  if ptr.is_null() {
1723    return MediaKind::Unknown;
1724  }
1725  // SAFETY: `ptr` is a live `*const AVCodecParameters`; `addr_of!`
1726  // computes the field address without forming a reference, and reading
1727  // as `i32` matches the bindgen enum's `c_int` storage.
1728  let raw = unsafe { read_unaligned(addr_of!((*ptr).codec_type) as *const i32) };
1729  media_kind_from_raw(raw)
1730}
1731
1732/// Every packet flag this build of FFmpeg names fits the byte
1733/// `PacketFlags` carries. If that stops being true, this fails the
1734/// build rather than letting a flag go missing at run time.
1735const _: () = {
1736  assert!(
1737    (ffmpeg_next::ffi::AV_PKT_FLAG_KEY
1738      | ffmpeg_next::ffi::AV_PKT_FLAG_CORRUPT
1739      | ffmpeg_next::ffi::AV_PKT_FLAG_DISCARD
1740      | ffmpeg_next::ffi::AV_PKT_FLAG_TRUSTED
1741      | ffmpeg_next::ffi::AV_PKT_FLAG_DISPOSABLE)
1742      <= u8::MAX as c_int,
1743    "FFmpeg names a packet flag outside the byte `PacketFlags` carries",
1744  );
1745};
1746
1747// ---------------------------------------------------------------------------
1748//  Empty-frame placeholders for `receive_frame` destinations.
1749// ---------------------------------------------------------------------------
1750
1751/// Constructs an empty [`mediadecode::frame::VideoFrame`] suitable as
1752/// the destination argument to
1753/// [`mediadecode::decoder::VideoStreamDecoder::receive_frame`]. The
1754/// decoder overwrites the frame on success; this just provides a
1755/// well-formed slot.
1756///
1757/// All four plane slots hold the shared empty carrier (the array shape
1758/// requires a buffer in every slot, but `plane_count = 0` reports them
1759/// as inactive).
1760///
1761/// **Infallible on both lanes, and the `try_` sibling is gone.**
1762/// Through 0.8 each slot was its own one-byte `AVBufferRef`, so
1763/// building a placeholder was four FFmpeg allocations that could fail —
1764/// hence a `try_empty_video_frame` returning `Option` and an
1765/// `empty_video_frame` that panicked on `None`. Neither lane allocates
1766/// here now: the owned lane's empty carrier is made once for the
1767/// process and cloned by refcount, and the view lane's is a null-backed
1768/// zero-length view, which is what lets the *view* lane keep the same
1769/// infallible constructor rather than reintroducing 0.8's failure mode
1770/// under a new name. A constructor with no failure mode does not get to
1771/// keep a `Result`-shaped door.
1772pub(crate) fn empty_video_frame_as<C: crate::FfmpegCarrier + crate::CarrierOps>()
1773-> VideoFrame<PixelFormat, VideoFrameExtra, C::Buffer> {
1774  VideoFrame::new(
1775    Dimensions::new(0, 0),
1776    // mediaframe 0.3's named "no format yet" member, and its
1777    // `Default` — the state a descriptor is in before a decoder has
1778    // said what it produces, which is exactly this placeholder.
1779    PixelFormat::None,
1780    core::array::from_fn(|_| Plane::new(C::empty(), 0)),
1781    0,
1782    VideoFrameExtra::default(),
1783  )
1784}
1785
1786/// Constructs an empty [`mediadecode::frame::AudioFrame`] suitable as
1787/// the destination argument to
1788/// [`mediadecode::decoder::AudioStreamDecoder::receive_frame`]. Same
1789/// behaviour as [`empty_video_frame`] — eight shared empty plane
1790/// carriers, `plane_count = 0`, and no way to fail.
1791pub(crate) fn empty_audio_frame_as<C: crate::FfmpegCarrier + crate::CarrierOps>()
1792-> AudioFrame<SampleFormat, ChannelLayoutDescription, AudioFrameExtra, C::Buffer> {
1793  AudioFrame::new(
1794    0,
1795    0,
1796    0,
1797    SampleFormat::NONE,
1798    ChannelLayoutDescription::default(),
1799    core::array::from_fn(|_| Plane::new(C::empty(), 0)),
1800    0,
1801    AudioFrameExtra::default(),
1802  )
1803}
1804
1805/// Constructs an empty [`mediadecode::frame::SubtitleFrame`] suitable
1806/// as the destination argument to
1807/// [`mediadecode::decoder::SubtitleDecoder::receive_frame`]. The
1808/// payload is an empty `Text` placeholder; the decoder overwrites it
1809/// on success. Infallible, as its two siblings are.
1810pub(crate) fn empty_subtitle_frame_as<C: crate::FfmpegCarrier + crate::CarrierOps>()
1811-> SubtitleFrame<SubtitleFrameExtra, C::Buffer> {
1812  SubtitleFrame::new(
1813    SubtitlePayload::Text(SubtitleText::new(C::empty(), None)),
1814    SubtitleFrameExtra::default(),
1815  )
1816}
1817
1818// --- The bare names, on the view lane --------------------------------
1819//
1820// **The signature says the lane, and the compiler enforces it.** A
1821// conversion that *borrows* its `AVPacket` can only copy: the packet
1822// type lends `&mut [u8]` and shares its buffer by refcount with no
1823// copy-on-write, so a caller who still holds the packet holds a mutable
1824// alias of anything a view would read. A conversion that *consumes* its
1825// packet can share, because after it returns there is no other handle.
1826//
1827// So: **borrow in, owned lane out; move in, either lane out.** The
1828// `_in` names below take the packet by value and answer with the view
1829// lane — the ordinary road, where a direct consumer reads a packet in
1830// place and drops it — while the lane-generic `_as` workers, also
1831// by value, let a caller ask for either. The borrowing doors are the
1832// bare `*_packet_from_ffmpeg` names, and they are the owned lane.
1833//
1834// A generic function has no default parameter to fall back on (defaults
1835// are used when a type is *written*, not inferred from a call), so
1836// these are monomorphic wrappers rather than a default that would never
1837// apply.
1838
1839/// [`video_packet_from_ffmpeg_as`] on the view lane, with the
1840/// stream's timebase stamped onto every timestamp.
1841///
1842/// **Takes the packet by value**, and that is the safety property, not
1843/// a style choice. The program below is the alias this signature
1844/// forbids — it was accepted when the parameter was a reference, and
1845/// `data_mut` handed out a `&mut [u8]` over bytes the returned carrier
1846/// was still lending as `&[u8]`:
1847///
1848/// ```compile_fail,E0382
1849/// use ffmpeg_next::packet::Mut;
1850/// use mediadecode::Timebase;
1851/// use mediadecode_ffmpeg::{PacketLimits, video_packet_from_ffmpeg_in};
1852///
1853/// let mut packet = ffmpeg_next::Packet::copy(&[0u8; 64]);
1854/// let viewed = video_packet_from_ffmpeg_in(packet, Timebase::default(), PacketLimits::default());
1855/// // The source is gone, so this cannot be written.
1856/// let _aliased = packet.data_mut();
1857/// ```
1858///
1859/// A caller who wants to keep the packet wants the owned lane, which
1860/// copies and may therefore borrow:
1861/// [`owned_video_packet_from_ffmpeg_in`].
1862pub fn video_packet_from_ffmpeg_in(
1863  packet: Packet,
1864  time_base: mediadecode::Timebase,
1865  limits: PacketLimits,
1866) -> Result<Option<VideoPacket<VideoPacketExtra, crate::FfmpegBuffer>>, PacketBufferError> {
1867  video_packet_from_ffmpeg_as::<crate::View>(
1868    packet,
1869    time_base,
1870    limits,
1871    crate::buffer::PayloadProvenance::CallerSupplied,
1872  )
1873}
1874
1875/// [`audio_packet_from_ffmpeg_as`] on the view lane.
1876pub fn audio_packet_from_ffmpeg_in(
1877  packet: Packet,
1878  time_base: mediadecode::Timebase,
1879  limits: PacketLimits,
1880) -> Result<Option<AudioPacket<AudioPacketExtra, crate::FfmpegBuffer>>, PacketBufferError> {
1881  audio_packet_from_ffmpeg_as::<crate::View>(
1882    packet,
1883    time_base,
1884    limits,
1885    crate::buffer::PayloadProvenance::CallerSupplied,
1886  )
1887}
1888
1889/// [`subtitle_packet_from_ffmpeg_as`] on the view lane.
1890pub fn subtitle_packet_from_ffmpeg_in(
1891  packet: Packet,
1892  time_base: mediadecode::Timebase,
1893  limits: PacketLimits,
1894) -> Result<Option<SubtitlePacket<SubtitlePacketExtra, crate::FfmpegBuffer>>, PacketBufferError> {
1895  subtitle_packet_from_ffmpeg_as::<crate::View>(
1896    packet,
1897    time_base,
1898    limits,
1899    crate::buffer::PayloadProvenance::CallerSupplied,
1900  )
1901}
1902
1903/// [`data_packet_from_ffmpeg_as`] on the view lane.
1904pub fn data_packet_from_ffmpeg_in(
1905  packet: Packet,
1906  time_base: mediadecode::Timebase,
1907  limits: PacketLimits,
1908) -> Result<Option<DataPacket<DataPacketExtra, crate::FfmpegBuffer>>, PacketBufferError> {
1909  data_packet_from_ffmpeg_as::<crate::View>(
1910    packet,
1911    time_base,
1912    limits,
1913    crate::buffer::PayloadProvenance::CallerSupplied,
1914  )
1915}
1916
1917// --- The borrowing doors, on the owned lane --------------------------
1918//
1919// The owned lane may borrow because it copies: nothing it returns
1920// points at the source, so the source's fate is its own business.
1921// These are the shapes the view lane cannot offer, and the reason it
1922// cannot is the whole of the split — see the note above the `_in`
1923// family.
1924
1925/// [`video_packet_from_ffmpeg`] with the stream's timebase and explicit
1926/// budgets.
1927pub fn owned_video_packet_from_ffmpeg_in(
1928  packet: &Packet,
1929  time_base: mediadecode::Timebase,
1930  limits: PacketLimits,
1931) -> Result<Option<VideoPacket<VideoPacketExtra, FfmpegBytes>>, PacketBufferError> {
1932  video_packet_from_borrowed::<crate::Owned>(
1933    packet,
1934    time_base,
1935    limits,
1936    crate::buffer::PayloadProvenance::CallerSupplied,
1937  )
1938}
1939
1940/// [`audio_packet_from_ffmpeg`] with the stream's timebase and explicit
1941/// budgets.
1942pub fn owned_audio_packet_from_ffmpeg_in(
1943  packet: &Packet,
1944  time_base: mediadecode::Timebase,
1945  limits: PacketLimits,
1946) -> Result<Option<AudioPacket<AudioPacketExtra, FfmpegBytes>>, PacketBufferError> {
1947  audio_packet_from_borrowed::<crate::Owned>(
1948    packet,
1949    time_base,
1950    limits,
1951    crate::buffer::PayloadProvenance::CallerSupplied,
1952  )
1953}
1954
1955/// [`subtitle_packet_from_ffmpeg`] with the stream's timebase and
1956/// explicit budgets.
1957pub fn owned_subtitle_packet_from_ffmpeg_in(
1958  packet: &Packet,
1959  time_base: mediadecode::Timebase,
1960  limits: PacketLimits,
1961) -> Result<Option<SubtitlePacket<SubtitlePacketExtra, FfmpegBytes>>, PacketBufferError> {
1962  subtitle_packet_from_borrowed::<crate::Owned>(
1963    packet,
1964    time_base,
1965    limits,
1966    crate::buffer::PayloadProvenance::CallerSupplied,
1967  )
1968}
1969
1970/// [`data_packet_from_ffmpeg_in`] on the owned lane, borrowing its
1971/// source.
1972pub fn owned_data_packet_from_ffmpeg_in(
1973  packet: &Packet,
1974  time_base: mediadecode::Timebase,
1975  limits: PacketLimits,
1976) -> Result<Option<DataPacket<DataPacketExtra, FfmpegBytes>>, PacketBufferError> {
1977  data_packet_from_borrowed::<crate::Owned>(
1978    packet,
1979    time_base,
1980    limits,
1981    crate::buffer::PayloadProvenance::CallerSupplied,
1982  )
1983}
1984
1985/// [`attachment_packet_from_ffmpeg`] on the owned lane, borrowing its
1986/// source.
1987pub fn owned_attachment_packet_from_ffmpeg(
1988  packet: &Packet,
1989  limits: PacketLimits,
1990) -> Result<Option<AttachmentPacket<AttachmentPacketExtra, FfmpegBytes>>, PacketBufferError> {
1991  attachment_packet_from_borrowed::<crate::Owned>(
1992    packet,
1993    limits,
1994    crate::buffer::PayloadProvenance::CallerSupplied,
1995  )
1996}
1997
1998/// [`empty_video_frame_as`] on the view lane — the destination a
1999/// [`FfmpegVideoStreamDecoder`](crate::FfmpegVideoStreamDecoder) fills.
2000#[must_use]
2001pub fn empty_video_frame() -> VideoFrame<PixelFormat, VideoFrameExtra, crate::FfmpegBuffer> {
2002  empty_video_frame_as::<crate::View>()
2003}
2004
2005/// [`empty_video_frame_as`] on the owned lane.
2006#[must_use]
2007pub fn empty_owned_video_frame() -> VideoFrame<PixelFormat, VideoFrameExtra, FfmpegBytes> {
2008  empty_video_frame_as::<crate::Owned>()
2009}
2010
2011/// [`empty_audio_frame_as`] on the view lane.
2012#[must_use]
2013pub fn empty_audio_frame()
2014-> AudioFrame<SampleFormat, ChannelLayoutDescription, AudioFrameExtra, crate::FfmpegBuffer> {
2015  empty_audio_frame_as::<crate::View>()
2016}
2017
2018/// [`empty_audio_frame_as`] on the owned lane.
2019#[must_use]
2020pub fn empty_owned_audio_frame()
2021-> AudioFrame<SampleFormat, ChannelLayoutDescription, AudioFrameExtra, FfmpegBytes> {
2022  empty_audio_frame_as::<crate::Owned>()
2023}
2024
2025/// [`empty_subtitle_frame_as`] on the view lane.
2026#[must_use]
2027pub fn empty_subtitle_frame() -> SubtitleFrame<SubtitleFrameExtra, crate::FfmpegBuffer> {
2028  empty_subtitle_frame_as::<crate::View>()
2029}
2030
2031/// [`empty_subtitle_frame_as`] on the owned lane.
2032#[must_use]
2033pub fn empty_owned_subtitle_frame() -> SubtitleFrame<SubtitleFrameExtra, FfmpegBytes> {
2034  empty_subtitle_frame_as::<crate::Owned>()
2035}
2036
2037/// [`attachment_packet_from_ffmpeg_as`] on the view lane.
2038pub fn attachment_packet_from_ffmpeg(
2039  packet: Packet,
2040  limits: PacketLimits,
2041) -> Result<Option<AttachmentPacket<AttachmentPacketExtra, crate::FfmpegBuffer>>, PacketBufferError>
2042{
2043  attachment_packet_from_ffmpeg_as::<crate::View>(
2044    packet,
2045    limits,
2046    crate::buffer::PayloadProvenance::CallerSupplied,
2047  )
2048}
2049
2050#[cfg(test)]
2051mod tests {
2052  use super::*;
2053
2054  /// A refcounted packet whose header claims a payload larger than the
2055  /// buffer behind it — the shape a malformed container, or an
2056  /// `av_packet_split_side_data` gone wrong, produces. Its payload is
2057  /// *there* and cannot be wrapped, which is exactly the case that must
2058  /// not read as "empty".
2059  fn out_of_bounds_packet() -> Packet {
2060    use ffmpeg_next::packet::Mut;
2061    let mut packet = Packet::copy(&[1u8, 2, 3, 4]);
2062    // SAFETY: `packet` owns a live `AVPacket` with a refcounted buffer;
2063    // `size` is a public field.
2064    unsafe {
2065      (*packet.as_mut_ptr()).size = 1 << 20;
2066    }
2067    packet
2068  }
2069
2070  #[test]
2071  fn a_payload_that_cannot_be_wrapped_is_an_error_on_every_arm() {
2072    // All five delivery arms plus the attachment path go through the
2073    // same wrapper. If one of them still folded the failure into
2074    // `None`, the demuxer would drop that kind of packet in silence.
2075    let forged = out_of_bounds_packet();
2076    let tb = mediadecode::Timebase::default();
2077    assert!(matches!(
2078      video_packet_from_borrowed::<crate::Owned>(
2079        &forged,
2080        tb,
2081        PacketLimits::default(),
2082        crate::buffer::PayloadProvenance::CallerSupplied
2083      ),
2084      Err(PacketBufferError::Bounds(_)),
2085    ));
2086    assert!(matches!(
2087      audio_packet_from_borrowed::<crate::Owned>(
2088        &forged,
2089        tb,
2090        PacketLimits::default(),
2091        crate::buffer::PayloadProvenance::CallerSupplied
2092      ),
2093      Err(PacketBufferError::Bounds(_)),
2094    ));
2095    assert!(matches!(
2096      subtitle_packet_from_borrowed::<crate::Owned>(
2097        &forged,
2098        tb,
2099        PacketLimits::default(),
2100        crate::buffer::PayloadProvenance::CallerSupplied
2101      ),
2102      Err(PacketBufferError::Bounds(_)),
2103    ));
2104    assert!(matches!(
2105      data_packet_from_borrowed::<crate::Owned>(
2106        &forged,
2107        tb,
2108        PacketLimits::default(),
2109        crate::buffer::PayloadProvenance::CallerSupplied
2110      ),
2111      Err(PacketBufferError::Bounds(_)),
2112    ));
2113    assert!(matches!(
2114      attachment_packet_from_borrowed::<crate::Owned>(
2115        &forged,
2116        PacketLimits::default(),
2117        crate::buffer::PayloadProvenance::CallerSupplied
2118      ),
2119      Err(PacketBufferError::Bounds(_)),
2120    ));
2121  }
2122
2123  /// A packet with **no body** and one side-data entry — the shape
2124  /// FFmpeg uses to hand a decoder new extradata or a parameter change.
2125  /// `size` is 0 and `buf` is null, which is exactly what used to read
2126  /// as "empty, skip it".
2127  fn side_data_only_packet() -> Packet {
2128    use ffmpeg_next::{
2129      ffi::{AVPacketSideDataType, av_packet_new_side_data},
2130      packet::Mut,
2131    };
2132    let mut packet = Packet::empty();
2133    // SAFETY: `packet` owns a live `AVPacket`; the side-data type is a
2134    // compile-time constant of this build, so no invalid discriminant
2135    // is formed. The returned pointer is valid for the four bytes just
2136    // allocated.
2137    unsafe {
2138      let ptr = av_packet_new_side_data(
2139        packet.as_mut_ptr(),
2140        AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA,
2141        4,
2142      );
2143      assert!(!ptr.is_null(), "av_packet_new_side_data");
2144      core::ptr::copy_nonoverlapping([1u8, 2, 3, 4].as_ptr(), ptr, 4);
2145    }
2146    packet
2147  }
2148
2149  const NEW_EXTRADATA: i32 =
2150    ffmpeg_next::ffi::AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA as i32;
2151
2152  use ffmpeg_next::ffi::{AVPacketSideData, AVPacketSideDataType, av_malloc};
2153
2154  /// A packet carrying one side-data entry of exactly `size` bytes,
2155  /// with a body when `body` is set.
2156  fn packet_with_side_data(size: usize, body: bool) -> Packet {
2157    use ffmpeg_next::{
2158      ffi::{AVPacketSideDataType, av_packet_new_side_data},
2159      packet::Mut,
2160    };
2161    let mut packet = if body {
2162      Packet::copy(&[1u8, 2, 3])
2163    } else {
2164      Packet::empty()
2165    };
2166    // SAFETY: `packet` owns a live `AVPacket` and the type is a
2167    // compile-time constant of this build.
2168    let ptr = unsafe {
2169      av_packet_new_side_data(
2170        packet.as_mut_ptr(),
2171        AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA,
2172        size,
2173      )
2174    };
2175    assert!(!ptr.is_null(), "av_packet_new_side_data({size})");
2176    packet
2177  }
2178
2179  /// A packet whose side-data array has been forged into a shape
2180  /// FFmpeg's own API cannot produce, and cannot free either: `Drop`
2181  /// puts the header back into a state `av_packet_free_side_data` can
2182  /// walk, so a fixture for a malformed packet cannot take the test
2183  /// process down with it.
2184  ///
2185  /// Both `av_packet_new_side_data` and `av_packet_add_side_data`
2186  /// replace an existing entry of the same type (measured: seventy
2187  /// calls leave one entry), so every shape below — an over-cap count, a
2188  /// negative count, a missing array, an entry that declares bytes it
2189  /// does not carry — is one no FFmpeg call produces. That is exactly
2190  /// what the validation is for.
2191  struct Forged {
2192    packet: Packet,
2193    freeable: i32,
2194  }
2195
2196  impl Drop for Forged {
2197    fn drop(&mut self) {
2198      use ffmpeg_next::packet::Mut;
2199      // SAFETY: `packet` owns a live `AVPacket`; putting the count back
2200      // to what the array really holds is what makes the free sound.
2201      unsafe { (*self.packet.as_mut_ptr()).side_data_elems = self.freeable };
2202    }
2203  }
2204
2205  impl Forged {
2206    /// `count` real entries of four bytes each.
2207    fn entries(count: usize, body: bool) -> Self {
2208      let mut packet = Self::carrier(body);
2209      // SAFETY: the array and every entry payload come from FFmpeg's
2210      // own allocator and are handed to the packet, which frees them in
2211      // `av_packet_free_side_data`. The carrier has no side data of its
2212      // own, so the overwrite leaks nothing.
2213      unsafe {
2214        let array = Self::array(count);
2215        for index in 0..count {
2216          let data = av_malloc(4) as *mut u8;
2217          assert!(!data.is_null(), "av_malloc");
2218          core::ptr::write_bytes(data, 7, 4);
2219          (*array.add(index)).data = data;
2220          (*array.add(index)).size = 4;
2221          (*array.add(index)).type_ = AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA;
2222        }
2223        Self::attach(&mut packet, array, count as i32);
2224      }
2225      Self {
2226        packet,
2227        freeable: count as i32,
2228      }
2229    }
2230
2231    /// A count with no array behind it at all.
2232    fn null_array(count: i32, body: bool) -> Self {
2233      let mut packet = Self::carrier(body);
2234      use ffmpeg_next::packet::Mut;
2235      // SAFETY: the packet's side data is null already; only the count
2236      // is forged, and `Drop` puts it back to zero before the free.
2237      unsafe { (*packet.as_mut_ptr()).side_data_elems = count };
2238      Self {
2239        packet,
2240        freeable: 0,
2241      }
2242    }
2243
2244    /// One entry declaring `size` bytes it does not carry.
2245    fn null_entry_data(size: usize, body: bool) -> Self {
2246      let mut packet = Self::carrier(body);
2247      // SAFETY: as in `entries`, except the payload pointer is left
2248      // null — `av_freep(&NULL)` is a no-op, so the free stays sound.
2249      unsafe {
2250        let array = Self::array(1);
2251        (*array).data = core::ptr::null_mut();
2252        (*array).size = size;
2253        (*array).type_ = AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA;
2254        Self::attach(&mut packet, array, 1);
2255      }
2256      Self {
2257        packet,
2258        freeable: 1,
2259      }
2260    }
2261
2262    /// Overrides the declared count, keeping the array intact.
2263    fn with_declared_count(mut self, count: i32) -> Self {
2264      use ffmpeg_next::packet::Mut;
2265      // SAFETY: `Drop` restores `freeable`, which still describes the
2266      // array really attached.
2267      unsafe { (*self.packet.as_mut_ptr()).side_data_elems = count };
2268      self
2269    }
2270
2271    fn carrier(body: bool) -> Packet {
2272      if body {
2273        Packet::copy(&[1u8, 2, 3])
2274      } else {
2275        Packet::empty()
2276      }
2277    }
2278
2279    /// # Safety
2280    /// The returned array is FFmpeg-allocated and uninitialised.
2281    unsafe fn array(count: usize) -> *mut AVPacketSideData {
2282      let array = unsafe { av_malloc(count * core::mem::size_of::<AVPacketSideData>()) }
2283        as *mut AVPacketSideData;
2284      assert!(!array.is_null(), "av_malloc");
2285      array
2286    }
2287
2288    /// # Safety
2289    /// `array` must hold `count` initialised entries owned by FFmpeg.
2290    unsafe fn attach(packet: &mut Packet, array: *mut AVPacketSideData, count: i32) {
2291      use ffmpeg_next::packet::Mut;
2292      unsafe {
2293        (*packet.as_mut_ptr()).side_data = array;
2294        (*packet.as_mut_ptr()).side_data_elems = count;
2295      }
2296    }
2297  }
2298
2299  /// Runs one packet through all four timed conversions, returning what
2300  /// each answered — the arms share a collector, and a fix that misses
2301  /// one of them is a fix that misses.
2302  fn every_timed_arm(packet: &Packet) -> [(&'static str, Result<bool, PacketBufferError>); 4] {
2303    let tb = mediadecode::Timebase::default();
2304    [
2305      (
2306        "video",
2307        video_packet_from_borrowed::<crate::Owned>(
2308          packet,
2309          tb,
2310          PacketLimits::default(),
2311          crate::buffer::PayloadProvenance::CallerSupplied,
2312        )
2313        .map(|p| p.is_some()),
2314      ),
2315      (
2316        "audio",
2317        audio_packet_from_borrowed::<crate::Owned>(
2318          packet,
2319          tb,
2320          PacketLimits::default(),
2321          crate::buffer::PayloadProvenance::CallerSupplied,
2322        )
2323        .map(|p| p.is_some()),
2324      ),
2325      (
2326        "subtitle",
2327        subtitle_packet_from_borrowed::<crate::Owned>(
2328          packet,
2329          tb,
2330          PacketLimits::default(),
2331          crate::buffer::PayloadProvenance::CallerSupplied,
2332        )
2333        .map(|p| p.is_some()),
2334      ),
2335      (
2336        "data",
2337        data_packet_from_borrowed::<crate::Owned>(
2338          packet,
2339          tb,
2340          PacketLimits::default(),
2341          crate::buffer::PayloadProvenance::CallerSupplied,
2342        )
2343        .map(|p| p.is_some()),
2344      ),
2345    ]
2346  }
2347
2348  #[test]
2349  fn side_data_a_packet_declares_and_does_not_carry_refuses_it() {
2350    // Two pointer paths walked straight past the all-or-error rule: a
2351    // count with no array behind it returned "no side data", and an
2352    // entry declaring bytes it did not carry became an empty entry —
2353    // charged nothing, delivered as though the packet had said so. Both
2354    // are how a body-bearing packet reached a decoder stripped of its
2355    // control data, and how a side-data-only packet became `None` and
2356    // was skipped.
2357    for body in [true, false] {
2358      let forged = Forged::null_array(3, body);
2359      for (arm, result) in every_timed_arm(&forged.packet) {
2360        match result {
2361          Err(PacketBufferError::SideDataArray(p)) => assert_eq!(p.count(), 3, "{arm}"),
2362          other => panic!("{arm} (body={body}): expected SideDataArray, got {other:?}"),
2363        }
2364      }
2365
2366      let forged = Forged::null_entry_data(64, body);
2367      for (arm, result) in every_timed_arm(&forged.packet) {
2368        match result {
2369          Err(PacketBufferError::SideDataPayload(p)) => {
2370            assert_eq!((p.index(), p.size()), (0, 64), "{arm}");
2371          }
2372          other => panic!("{arm} (body={body}): expected SideDataPayload, got {other:?}"),
2373        }
2374      }
2375
2376      // And the malformed count keeps being refused when the array is
2377      // missing too — the count is judged on its own, before the
2378      // pointer, so one cannot excuse the other.
2379      let forged = Forged::null_array(-3, body);
2380      for (arm, result) in every_timed_arm(&forged.packet) {
2381        assert!(
2382          matches!(
2383            result,
2384            Err(PacketBufferError::SideDataEntries(p)) if p.count() == -3
2385          ),
2386          "{arm} (body={body}): {result:?}",
2387        );
2388      }
2389      let forged = Forged::null_array(i32::MAX, body);
2390      for (arm, result) in every_timed_arm(&forged.packet) {
2391        assert!(
2392          matches!(result, Err(PacketBufferError::SideDataEntries(_))),
2393          "{arm} (body={body}): {result:?}",
2394        );
2395      }
2396    }
2397
2398    // A zero-size entry is a marker, not a lie: a type with no bytes is
2399    // exactly what FFmpeg emits for some side data, and it stays
2400    // welcome.
2401    let marker = Forged::null_entry_data(0, true);
2402    let packet = video_packet_from_borrowed::<crate::Owned>(
2403      &marker.packet,
2404      mediadecode::Timebase::default(),
2405      PacketLimits::default(),
2406      crate::buffer::PayloadProvenance::CallerSupplied,
2407    )
2408    .expect("a marker entry is carriable")
2409    .expect("present");
2410    assert_eq!(packet.extra().side_data().len(), 1);
2411    assert!(packet.extra().side_data()[0].data().is_empty());
2412  }
2413
2414  #[test]
2415  fn side_data_that_cannot_be_carried_whole_refuses_the_packet() {
2416    let tb = mediadecode::Timebase::default();
2417
2418    // A body-bearing packet whose side data is over the byte cap. The
2419    // caps used to truncate and warn, which handed the codec a packet
2420    // that looked complete and was not: `NEW_EXTRADATA` gone, a decoder
2421    // left on parameters the container had already replaced.
2422    let oversized = packet_with_side_data(SIDE_DATA_MAX_TOTAL_BYTES + 1, true);
2423    for (arm, result) in [
2424      (
2425        "video",
2426        video_packet_from_borrowed::<crate::Owned>(
2427          &oversized,
2428          tb,
2429          PacketLimits::default(),
2430          crate::buffer::PayloadProvenance::CallerSupplied,
2431        )
2432        .map(|p| p.is_some()),
2433      ),
2434      (
2435        "audio",
2436        audio_packet_from_borrowed::<crate::Owned>(
2437          &oversized,
2438          tb,
2439          PacketLimits::default(),
2440          crate::buffer::PayloadProvenance::CallerSupplied,
2441        )
2442        .map(|p| p.is_some()),
2443      ),
2444      (
2445        "subtitle",
2446        subtitle_packet_from_borrowed::<crate::Owned>(
2447          &oversized,
2448          tb,
2449          PacketLimits::default(),
2450          crate::buffer::PayloadProvenance::CallerSupplied,
2451        )
2452        .map(|p| p.is_some()),
2453      ),
2454      (
2455        "data",
2456        data_packet_from_borrowed::<crate::Owned>(
2457          &oversized,
2458          tb,
2459          PacketLimits::default(),
2460          crate::buffer::PayloadProvenance::CallerSupplied,
2461        )
2462        .map(|p| p.is_some()),
2463      ),
2464    ] {
2465      match result {
2466        Err(PacketBufferError::SideDataBytes(p)) => {
2467          assert_eq!(p.cap(), SIDE_DATA_MAX_TOTAL_BYTES);
2468          assert!(p.bytes() > p.cap(), "{arm}");
2469        }
2470        other => panic!("{arm}: expected SideDataBytes, got {other:?}"),
2471      }
2472    }
2473
2474    // And the same packet with no body at all. This one used to
2475    // collapse twice over: the side data was dropped, the packet
2476    // therefore looked empty, and the demuxer skipped it in silence.
2477    let oversized = packet_with_side_data(SIDE_DATA_MAX_TOTAL_BYTES + 1, false);
2478    assert!(matches!(
2479      video_packet_from_borrowed::<crate::Owned>(
2480        &oversized,
2481        tb,
2482        PacketLimits::default(),
2483        crate::buffer::PayloadProvenance::CallerSupplied
2484      )
2485      .map(|p| p.is_some()),
2486      Err(PacketBufferError::SideDataBytes(_)),
2487    ));
2488  }
2489
2490  #[test]
2491  fn the_side_data_caps_are_refusals_at_the_boundary_not_before_it() {
2492    let tb = mediadecode::Timebase::default();
2493
2494    // Exactly at the byte cap: carried, whole.
2495    let at_cap = packet_with_side_data(SIDE_DATA_MAX_TOTAL_BYTES, true);
2496    let packet = video_packet_from_borrowed::<crate::Owned>(
2497      &at_cap,
2498      tb,
2499      PacketLimits::default(),
2500      crate::buffer::PayloadProvenance::CallerSupplied,
2501    )
2502    .expect("exactly at the cap is not over it")
2503    .expect("present");
2504    assert_eq!(packet.extra().side_data().len(), 1);
2505    assert_eq!(
2506      packet.extra().side_data()[0].data().len(),
2507      SIDE_DATA_MAX_TOTAL_BYTES,
2508    );
2509
2510    // One byte past: refused.
2511    let past = packet_with_side_data(SIDE_DATA_MAX_TOTAL_BYTES + 1, true);
2512    assert!(matches!(
2513      video_packet_from_borrowed::<crate::Owned>(
2514        &past,
2515        tb,
2516        PacketLimits::default(),
2517        crate::buffer::PayloadProvenance::CallerSupplied
2518      )
2519      .map(|p| p.is_some()),
2520      Err(PacketBufferError::SideDataBytes(_)),
2521    ));
2522
2523    // The entry cap, both sides of it. FFmpeg names fewer types than
2524    // the floor today, so the effective cap is the floor; if a future
2525    // build names more, this assertion moves the boundary rather than
2526    // letting the lane quietly test nothing.
2527    let cap = side_data_entry_cap();
2528    assert_eq!(cap, SIDE_DATA_MAX_ENTRIES, "the cap this lane straddles");
2529
2530    let at_cap = Forged::entries(cap, true);
2531    let packet = video_packet_from_borrowed::<crate::Owned>(
2532      &at_cap.packet,
2533      tb,
2534      PacketLimits::default(),
2535      crate::buffer::PayloadProvenance::CallerSupplied,
2536    )
2537    .expect("exactly at the cap is not over it")
2538    .expect("present");
2539    assert_eq!(packet.extra().side_data().len(), cap);
2540
2541    let past = Forged::entries(cap + 1, true);
2542    match video_packet_from_borrowed::<crate::Owned>(
2543      &past.packet,
2544      tb,
2545      PacketLimits::default(),
2546      crate::buffer::PayloadProvenance::CallerSupplied,
2547    )
2548    .map(|p| p.is_some())
2549    {
2550      Err(PacketBufferError::SideDataEntries(p)) => {
2551        assert_eq!(p.count() as usize, cap + 1);
2552        assert_eq!(p.cap(), cap);
2553      }
2554      other => panic!("expected SideDataEntries, got {other:?}"),
2555    }
2556
2557    // A negative count is malformed, not empty — reading it as "no side
2558    // data" would be the same silent loss by another route.
2559    let corrupt = Forged::entries(1, true).with_declared_count(-3);
2560    assert!(matches!(
2561      video_packet_from_borrowed::<crate::Owned>(&corrupt.packet, tb, PacketLimits::default(), crate::buffer::PayloadProvenance::CallerSupplied).map(|p| p.is_some()),
2562      Err(PacketBufferError::SideDataEntries(p)) if p.count() == -3,
2563    ));
2564  }
2565
2566  #[test]
2567  fn a_trusted_packet_is_refused_on_both_legs() {
2568    use ffmpeg_next::packet::Mut;
2569    let tb = mediadecode::Timebase::default();
2570
2571    // Copy-out: the leg such a packet would enter the graph on.
2572    let mut packet = Packet::copy(&[1u8, 2, 3]);
2573    // SAFETY: `packet` owns a live `AVPacket`; `flags` is a public field
2574    // and this bit has no `ffmpeg_next::Flags` spelling.
2575    unsafe {
2576      (*packet.as_mut_ptr()).flags =
2577        ffmpeg_next::ffi::AV_PKT_FLAG_KEY | ffmpeg_next::ffi::AV_PKT_FLAG_TRUSTED;
2578    };
2579    for (arm, taken) in [
2580      (
2581        "video",
2582        video_packet_from_borrowed::<crate::Owned>(
2583          &packet,
2584          tb,
2585          PacketLimits::default(),
2586          crate::buffer::PayloadProvenance::CallerSupplied,
2587        )
2588        .map(|p| p.is_some()),
2589      ),
2590      (
2591        "audio",
2592        audio_packet_from_borrowed::<crate::Owned>(
2593          &packet,
2594          tb,
2595          PacketLimits::default(),
2596          crate::buffer::PayloadProvenance::CallerSupplied,
2597        )
2598        .map(|p| p.is_some()),
2599      ),
2600      (
2601        "subtitle",
2602        subtitle_packet_from_borrowed::<crate::Owned>(
2603          &packet,
2604          tb,
2605          PacketLimits::default(),
2606          crate::buffer::PayloadProvenance::CallerSupplied,
2607        )
2608        .map(|p| p.is_some()),
2609      ),
2610      (
2611        "data",
2612        data_packet_from_borrowed::<crate::Owned>(
2613          &packet,
2614          tb,
2615          PacketLimits::default(),
2616          crate::buffer::PayloadProvenance::CallerSupplied,
2617        )
2618        .map(|p| p.is_some()),
2619      ),
2620      (
2621        "attachment",
2622        attachment_packet_from_borrowed::<crate::Owned>(
2623          &packet,
2624          PacketLimits::default(),
2625          crate::buffer::PayloadProvenance::CallerSupplied,
2626        )
2627        .map(|p| p.is_some()),
2628      ),
2629    ] {
2630      match taken {
2631        Err(PacketBufferError::TrustedPayload(p)) => assert_eq!(p.len(), 3, "{arm}"),
2632        other => panic!("{arm}: a TRUSTED payload must not be copied out, got {other:?}"),
2633      }
2634    }
2635
2636    // Rebuild: the leg a flag that arrived by some other route would be
2637    // handed back to a decoder on. Built by hand, because the copy-out
2638    // leg above will no longer produce one.
2639    let clean = Packet::copy(&[1u8, 2, 3]);
2640    let video = video_packet_from_borrowed::<crate::Owned>(
2641      &clean,
2642      tb,
2643      PacketLimits::default(),
2644      crate::buffer::PayloadProvenance::CallerSupplied,
2645    )
2646    .expect("a clean packet is carriable")
2647    .expect("present");
2648    let trusted = video
2649      .clone()
2650      .with_flags(MdPacketFlags::from_bits_retain(crate::buffer::TRUSTED_BIT));
2651    assert!(
2652      matches!(
2653        ffmpeg_packet_from_owned_video_packet(&trusted, PacketLimits::default()),
2654        Err(PacketBuildError::TrustedPayload(_)),
2655      ),
2656      "a TRUSTED flag must not be written back onto an AVPacket",
2657    );
2658  }
2659
2660  #[test]
2661  fn every_flag_bit_survives_both_directions() {
2662    // `PacketFlags` is a bit set whose documented lossless door is
2663    // `from_bits_retain`, and both directions used to squeeze it
2664    // through `ffmpeg_next`'s `Flags`, which names `KEY` and `CORRUPT`
2665    // and truncates the rest away. `AV_PKT_FLAG_DISCARD` is the one
2666    // that matters most: it tells a consumer to decode a packet and
2667    // throw its output away, and without it preroll output looks like
2668    // something to keep.
2669    use ffmpeg_next::packet::{Mut, Ref};
2670    const DISCARD: i32 = ffmpeg_next::ffi::AV_PKT_FLAG_DISCARD;
2671    const TRUSTED: i32 = ffmpeg_next::ffi::AV_PKT_FLAG_TRUSTED;
2672    const DISPOSABLE: i32 = ffmpeg_next::ffi::AV_PKT_FLAG_DISPOSABLE;
2673    const UNNAMED: i32 = 0b0010_0000; // nothing names this bit yet
2674    // `TRUSTED` is deliberately **not** in this set, and the constant is
2675    // kept only to say so. It used to be — this lane asserted it made
2676    // the round trip — and that assertion was the bug: a `TRUSTED`
2677    // payload may be a structure of pointers into other live objects,
2678    // so copying it mints an owned-looking carrier that dangles when
2679    // its source drops. It is now refused on both legs, which
2680    // `a_trusted_packet_is_refused_on_both_legs` pins.
2681    let _ = TRUSTED;
2682    let raw = ffmpeg_next::ffi::AV_PKT_FLAG_KEY | DISCARD | DISPOSABLE | UNNAMED;
2683
2684    let mut packet = Packet::copy(&[1u8, 2, 3]);
2685    // SAFETY: `packet` owns a live `AVPacket`; `flags` is a public
2686    // field and this is the only way to set a bit `ffmpeg_next`'s
2687    // `Flags` cannot spell.
2688    unsafe { (*packet.as_mut_ptr()).flags = raw };
2689    assert_ne!(
2690      packet.flags().bits(),
2691      raw,
2692      "the wrapper's own accessor is what loses them",
2693    );
2694
2695    let tb = mediadecode::Timebase::default();
2696    let expected = MdPacketFlags::from_bits_retain(raw as u8);
2697
2698    let video = video_packet_from_borrowed::<crate::Owned>(
2699      &packet,
2700      tb,
2701      PacketLimits::default(),
2702      crate::buffer::PayloadProvenance::CallerSupplied,
2703    )
2704    .expect("wrappable")
2705    .expect("present");
2706    assert_eq!(video.flags(), expected);
2707    assert!(video.flags().contains(MdPacketFlags::DISCARD));
2708    let audio = audio_packet_from_borrowed::<crate::Owned>(
2709      &packet,
2710      tb,
2711      PacketLimits::default(),
2712      crate::buffer::PayloadProvenance::CallerSupplied,
2713    )
2714    .expect("wrappable")
2715    .expect("present");
2716    assert_eq!(audio.flags(), expected);
2717    let subtitle = subtitle_packet_from_borrowed::<crate::Owned>(
2718      &packet,
2719      tb,
2720      PacketLimits::default(),
2721      crate::buffer::PayloadProvenance::CallerSupplied,
2722    )
2723    .expect("wrappable")
2724    .expect("present");
2725    assert_eq!(subtitle.flags(), expected);
2726    let data = data_packet_from_borrowed::<crate::Owned>(
2727      &packet,
2728      tb,
2729      PacketLimits::default(),
2730      crate::buffer::PayloadProvenance::CallerSupplied,
2731    )
2732    .expect("wrappable")
2733    .expect("present");
2734    assert_eq!(data.flags(), expected);
2735    let attachment = attachment_packet_from_borrowed::<crate::Owned>(
2736      &packet,
2737      PacketLimits::default(),
2738      crate::buffer::PayloadProvenance::CallerSupplied,
2739    )
2740    .expect("wrappable")
2741    .expect("present");
2742    assert_eq!(attachment.flags(), expected);
2743
2744    // And back out again, on the three paths a decoder is fed from.
2745    for (arm, rebuilt) in [
2746      (
2747        "video",
2748        ffmpeg_packet_from_owned_video_packet(&video, PacketLimits::default()).expect("rebuilt"),
2749      ),
2750      (
2751        "audio",
2752        ffmpeg_packet_from_owned_audio_packet(&audio, PacketLimits::default()).expect("rebuilt"),
2753      ),
2754      (
2755        "subtitle",
2756        ffmpeg_packet_from_owned_subtitle_packet(&subtitle, PacketLimits::default())
2757          .expect("rebuilt"),
2758      ),
2759    ] {
2760      // SAFETY: `rebuilt` owns a live `AVPacket`.
2761      let carried = unsafe { (*rebuilt.as_ptr()).flags };
2762      assert_eq!(carried, raw, "{arm} rebuilt {carried:#x} from {raw:#x}");
2763    }
2764  }
2765
2766  #[test]
2767  fn a_flag_bit_the_vocabulary_cannot_hold_refuses_the_packet() {
2768    // Unreachable against this build — every flag FFmpeg names lives in
2769    // the byte `PacketFlags` carries, and a compile-time assertion says
2770    // so. It is here because the day that stops being true, a packet
2771    // must be refused rather than delivered with a bit missing.
2772    use ffmpeg_next::packet::Mut;
2773    let mut packet = Packet::copy(&[1u8]);
2774    // SAFETY: `packet` owns a live `AVPacket`.
2775    unsafe { (*packet.as_mut_ptr()).flags = 0x1_00 };
2776    let tb = mediadecode::Timebase::default();
2777    for (arm, result) in every_timed_arm(&packet) {
2778      assert!(
2779        matches!(
2780          result,
2781          Err(PacketBufferError::UnrepresentableFlags(p)) if p.raw() == 0x1_00
2782        ),
2783        "{arm}: {result:?}",
2784      );
2785    }
2786    assert!(matches!(
2787      attachment_packet_from_borrowed::<crate::Owned>(
2788        &packet,
2789        PacketLimits::default(),
2790        crate::buffer::PayloadProvenance::CallerSupplied
2791      )
2792      .map(|p| p.is_some()),
2793      Err(PacketBufferError::UnrepresentableFlags(_)),
2794    ));
2795    let _ = tb;
2796  }
2797
2798  /// A live `AVBufferRef` of `len` bytes, released when the guard drops.
2799  struct TestBuffer(*mut ffmpeg_next::ffi::AVBufferRef);
2800
2801  impl TestBuffer {
2802    fn new(len: usize) -> Self {
2803      // SAFETY: a plain allocation, checked for null, written through
2804      // its own `data` pointer.
2805      unsafe {
2806        let raw = ffmpeg_next::ffi::av_buffer_alloc(len as _);
2807        assert!(!raw.is_null(), "av_buffer_alloc");
2808        core::ptr::write_bytes((*raw).data, 0xAB, len);
2809        Self(raw)
2810      }
2811    }
2812  }
2813
2814  impl Drop for TestBuffer {
2815    fn drop(&mut self) {
2816      // SAFETY: the guard owns exactly one reference, released once.
2817      unsafe { ffmpeg_next::ffi::av_buffer_unref(&mut self.0) };
2818    }
2819  }
2820
2821  /// The payload pointer a packet's buffer currently holds.
2822  fn payload_address(packet: &Packet) -> usize {
2823    use ffmpeg_next::packet::Ref;
2824    // SAFETY: the packet is live; `data` is a public field.
2825    unsafe { (*packet.as_ptr()).data as usize }
2826  }
2827
2828  /// The refcount of a packet's payload buffer.
2829  fn payload_references(packet: &Packet) -> i32 {
2830    use ffmpeg_next::packet::Ref;
2831    // SAFETY: the packet is live and refcounted; `buf` is a public
2832    // field and `av_buffer_get_ref_count` only reads the atomic.
2833    unsafe {
2834      let buf = (*packet.as_ptr()).buf;
2835      assert!(!buf.is_null(), "the fixture must be refcounted");
2836      ffmpeg_next::ffi::av_buffer_get_ref_count(buf)
2837    }
2838  }
2839
2840  #[test]
2841  fn a_shared_packet_buffer_is_refused_without_being_read() {
2842    // **The consumption argument has a hole a dependency can open, and
2843    // the obvious patch had a worse one.** Taking the packet by value
2844    // proves no *other* handle survives only if the packet's buffer was
2845    // the caller's alone to give. An earlier round answered a shared
2846    // buffer with a silent copy — but a copy needs a read, and a
2847    // refcount above one is exactly the state in which somebody else
2848    // may be writing those bytes, from safe code, on another thread.
2849    // The read *is* the race. So the answer is a refusal that touches
2850    // nothing.
2851    let mut original = Packet::copy(&[7u8; 4096]);
2852    let mut shared = Packet::empty();
2853    // SAFETY: both packets are live; `av_packet_ref` takes a reference
2854    // to `original`'s buffer without copying it.
2855    unsafe {
2856      use ffmpeg_next::packet::{Mut, Ref};
2857      assert_eq!(
2858        ffmpeg_next::ffi::av_packet_ref(shared.as_mut_ptr(), original.as_ptr()),
2859        0,
2860      );
2861    }
2862    assert_eq!(
2863      payload_address(&shared),
2864      payload_address(&original),
2865      "the premise: the two packets share one allocation",
2866    );
2867    assert_eq!(payload_references(&original), 2);
2868
2869    // The refusal is lane-independent, because the hazard is: both
2870    // lanes read the payload, one to view it and one to copy it.
2871    match video_packet_from_ffmpeg_as::<crate::View>(
2872      shared,
2873      mediadecode::Timebase::default(),
2874      PacketLimits::default(),
2875      crate::buffer::PayloadProvenance::CallerSupplied,
2876    ) {
2877      Err(PacketBufferError::SharedPayload(refused)) => {
2878        assert_eq!(refused.references(), 2);
2879      }
2880      other => panic!("a shared payload must be refused by name, got {other:?}"),
2881    }
2882
2883    // Nothing was read and nothing was carried: the packet the caller
2884    // kept is still theirs, still writable, and now sole owner again.
2885    {
2886      let slot = original.data_mut().expect("a writable payload");
2887      slot[0] = 0xEE;
2888    }
2889    assert_eq!(
2890      payload_references(&original),
2891      1,
2892      "the refusal must not have taken a reference of its own",
2893    );
2894
2895    // And the owned lane answers the same way, for the same reason: its
2896    // copy is a read too.
2897    let mut shared_again = Packet::empty();
2898    // SAFETY: as above.
2899    unsafe {
2900      use ffmpeg_next::packet::{Mut, Ref};
2901      assert_eq!(
2902        ffmpeg_next::ffi::av_packet_ref(shared_again.as_mut_ptr(), original.as_ptr()),
2903        0,
2904      );
2905    }
2906    assert!(matches!(
2907      owned_video_packet_from_ffmpeg_in(
2908        &shared_again,
2909        mediadecode::Timebase::default(),
2910        PacketLimits::default(),
2911      ),
2912      Err(PacketBufferError::SharedPayload(_)),
2913    ));
2914  }
2915
2916  #[test]
2917  fn a_clone_that_silently_shared_is_refused_by_name() {
2918    // The exact path: `ffmpeg_next::Packet::clone` calls
2919    // `av_packet_ref` then `av_packet_make_writable` and **ignores both
2920    // return codes**. Under allocation failure the second one leaves
2921    // the "clone" sharing the source's buffer, and nothing says so.
2922    //
2923    // The cap is process-global, so the manufacture runs alone. It is
2924    // lifted before the conversion, because what is being tested is the
2925    // conversion's answer to a shared buffer — not its behaviour under
2926    // memory pressure, which the other lanes cover.
2927    crate::fault_subprocess::in_subprocess(
2928      "boundary::tests::a_clone_that_silently_shared_is_refused_by_name",
2929      || {
2930        let mut original = Packet::copy(&[3u8; 8192]);
2931
2932        // Small allocations still succeed, so `av_packet_ref` gets its
2933        // `AVBufferRef`; a payload-sized one does not, so
2934        // `av_packet_make_writable` fails and is ignored.
2935        crate::fault_subprocess::cap_ffmpeg_allocations(512);
2936        let clone = original.clone();
2937        crate::fault_subprocess::uncap_ffmpeg_allocations();
2938
2939        assert_eq!(
2940          payload_address(&clone),
2941          payload_address(&original),
2942          "the premise this test exists for: a failed `make_writable` \
2943           leaves the clone sharing, silently",
2944        );
2945        assert_eq!(payload_references(&original), 2);
2946
2947        match video_packet_from_ffmpeg_as::<crate::View>(
2948          clone,
2949          mediadecode::Timebase::default(),
2950          PacketLimits::default(),
2951          crate::buffer::PayloadProvenance::CallerSupplied,
2952        ) {
2953          Err(PacketBufferError::SharedPayload(refused)) => {
2954            assert_eq!(refused.references(), 2);
2955          }
2956          other => panic!("expected a named refusal, got {other:?}"),
2957        }
2958
2959        // The handle the caller kept is untouched and unaliased.
2960        {
2961          let slot = original.data_mut().expect("a writable payload");
2962          slot[0] = 0x5A;
2963        }
2964        assert_eq!(payload_references(&original), 1);
2965      },
2966    );
2967  }
2968
2969  #[test]
2970  fn only_a_packet_payload_with_provable_padding_is_shared_into_a_decoder() {
2971    use crate::view::Origin;
2972    use ffmpeg_next::packet::Ref;
2973
2974    const PADDING: usize = ffmpeg_next::ffi::AV_INPUT_BUFFER_PADDING_SIZE as usize;
2975    let src = TestBuffer::new(512);
2976    let body_len = 512 - PADDING;
2977
2978    // The one shape that may share: a payload captured out of an
2979    // `AVPacket`'s own buffer, with libavformat's padding behind it.
2980    // SAFETY: `src` is live for the test and the extent is inside it.
2981    let payload =
2982      unsafe { crate::FfmpegBuffer::view_of(src.0, 0, body_len, Origin::PacketPayload) }
2983        .expect("a view");
2984    let shared = share_or_copy(&payload).expect("built");
2985    // SAFETY: both are live; `data` is a public field.
2986    assert_eq!(
2987      unsafe { (*shared.as_ptr()).data as usize },
2988      payload.as_ref().as_ptr() as usize,
2989      "a packet payload with provable padding must be shared",
2990    );
2991
2992    // **The same bytes, the same slack, no provenance.** This is the
2993    // frame-origin case: a decoded plane or a resampler's output reused
2994    // as a packet body. What follows it is more pixels or more samples,
2995    // and a bitstream reader running past the payload would eat them.
2996    // SAFETY: as above.
2997    let plane =
2998      unsafe { crate::FfmpegBuffer::view_of(src.0, 0, body_len, Origin::Foreign) }.expect("a view");
2999    let copied = share_or_copy(&plane).expect("built");
3000    // SAFETY: both are live.
3001    assert_ne!(
3002      unsafe { (*copied.as_ptr()).data as usize },
3003      plane.as_ref().as_ptr() as usize,
3004      "trailing capacity is not padding provenance — this must be copied",
3005    );
3006    assert_eq!(copied.data().unwrap_or(&[]), plane.as_ref());
3007
3008    // And a payload whose slack is short of the padding is copied too,
3009    // provenance or not: the extent has to hold as well.
3010    // SAFETY: as above.
3011    let tight = unsafe { crate::FfmpegBuffer::view_of(src.0, 0, 511, Origin::PacketPayload) }
3012      .expect("a view");
3013    let copied = share_or_copy(&tight).expect("built");
3014    // SAFETY: both are live.
3015    assert_ne!(
3016      unsafe { (*copied.as_ptr()).data as usize },
3017      tight.as_ref().as_ptr() as usize,
3018      "a payload with less than the padding behind it must be copied",
3019    );
3020
3021    // A narrowed payload has lost its claim, and is copied.
3022    let mut narrowed = payload.clone();
3023    narrowed.shrink_to(16);
3024    let copied = share_or_copy(&narrowed).expect("built");
3025    // SAFETY: both are live.
3026    assert_ne!(
3027      unsafe { (*copied.as_ptr()).data as usize },
3028      narrowed.as_ref().as_ptr() as usize,
3029    );
3030  }
3031
3032  #[test]
3033  fn an_empty_view_carrier_builds_a_payload_less_packet() {
3034    // The empty carrier is backed by **no buffer at all** — that is
3035    // what makes a placeholder plane slot free — so anything that
3036    // reads its `AVBufferRef` before asking whether it is empty
3037    // dereferences null. A side-data-only packet is the ordinary way
3038    // to get here, not an exotic one.
3039    let empty = crate::FfmpegBuffer::empty();
3040    assert!(empty.as_av_buffer_ref().is_null(), "the premise");
3041    let built = share_or_copy(&empty).expect("a payload-less packet");
3042    assert_eq!(built.size(), 0);
3043    // And it is a packet side data can be attached to.
3044    let mut built = built;
3045    attach_side_data(
3046      &mut built,
3047      &[SideDataEntry::new(
3048        NEW_EXTRADATA,
3049        FfmpegBytes::copy_from_slice(&[1u8, 2, 3, 4]),
3050      )],
3051    )
3052    .expect("side data attaches to a payload-less packet");
3053  }
3054
3055  #[test]
3056  fn a_side_data_only_packet_round_trips_on_every_view_decoder_family() {
3057    // Every family, both roads: the public builder a caller can call,
3058    // and the scoped submission a decoder goes through. Neither may
3059    // touch the empty carrier's absent buffer.
3060    let tb = mediadecode::Timebase::default();
3061    let source = side_data_only_packet();
3062    let limits = PacketLimits::default();
3063
3064    let video = video_packet_from_ffmpeg_as::<crate::View>(
3065      source.clone(),
3066      tb,
3067      limits,
3068      crate::buffer::PayloadProvenance::CallerSupplied,
3069    )
3070    .expect("carried")
3071    .expect("a side-data-only packet is a packet");
3072    assert!(video.data().as_ref().is_empty(), "the premise: no payload");
3073    assert_eq!(
3074      ffmpeg_packet_from_video_packet(&video, limits)
3075        .expect("built")
3076        .size(),
3077      0,
3078    );
3079    with_ffmpeg_video_packet::<crate::View, _>(&video, limits, BodyRoute::Submission, |av| {
3080      assert_eq!(av.size(), 0);
3081    })
3082    .expect("submitted");
3083
3084    let audio = audio_packet_from_ffmpeg_as::<crate::View>(
3085      source.clone(),
3086      tb,
3087      limits,
3088      crate::buffer::PayloadProvenance::CallerSupplied,
3089    )
3090    .expect("carried")
3091    .expect("a side-data-only packet is a packet");
3092    assert!(audio.data().as_ref().is_empty());
3093    assert_eq!(
3094      ffmpeg_packet_from_audio_packet(&audio, limits)
3095        .expect("built")
3096        .size(),
3097      0,
3098    );
3099    with_ffmpeg_audio_packet::<crate::View, _>(&audio, limits, BodyRoute::Submission, |av| {
3100      assert_eq!(av.size(), 0);
3101    })
3102    .expect("submitted");
3103
3104    let subtitle = subtitle_packet_from_ffmpeg_as::<crate::View>(
3105      source.clone(),
3106      tb,
3107      limits,
3108      crate::buffer::PayloadProvenance::CallerSupplied,
3109    )
3110    .expect("carried")
3111    .expect("a side-data-only packet is a packet");
3112    assert!(subtitle.data().as_ref().is_empty());
3113    assert_eq!(
3114      ffmpeg_packet_from_subtitle_packet(&subtitle, limits)
3115        .expect("built")
3116        .size(),
3117      0,
3118    );
3119    with_ffmpeg_subtitle_packet::<crate::View, _>(&subtitle, limits, BodyRoute::Submission, |av| {
3120      assert_eq!(av.size(), 0);
3121    })
3122    .expect("submitted");
3123  }
3124
3125  #[test]
3126  fn side_data_survives_the_round_trip_on_every_decoder_bound_arm() {
3127    // The forward direction captures side data; the reverse direction
3128    // is what a decoder is actually handed. Capturing without
3129    // reattaching is theatre: `NEW_EXTRADATA` never reaches the codec,
3130    // `SKIP_SAMPLES` never trims, and nothing says so.
3131    let tb = mediadecode::Timebase::default();
3132    let mut with_body = Packet::copy(&[4u8, 5, 6]);
3133    {
3134      use ffmpeg_next::{
3135        ffi::{AVPacketSideDataType, av_packet_new_side_data},
3136        packet::Mut,
3137      };
3138      unsafe {
3139        let ptr = av_packet_new_side_data(
3140          with_body.as_mut_ptr(),
3141          AVPacketSideDataType::AV_PKT_DATA_SKIP_SAMPLES,
3142          3,
3143        );
3144        assert!(!ptr.is_null());
3145        core::ptr::copy_nonoverlapping([1u8, 2, 3].as_ptr(), ptr, 3);
3146      }
3147    }
3148    const SKIP_SAMPLES: i32 =
3149      ffmpeg_next::ffi::AVPacketSideDataType::AV_PKT_DATA_SKIP_SAMPLES as i32;
3150
3151    for (name, source) in [
3152      ("body plus side data", &with_body),
3153      ("side data only", &side_data_only_packet()),
3154    ] {
3155      let expected_kind = if name == "side data only" {
3156        NEW_EXTRADATA
3157      } else {
3158        SKIP_SAMPLES
3159      };
3160
3161      let video = video_packet_from_borrowed::<crate::Owned>(
3162        source,
3163        tb,
3164        PacketLimits::default(),
3165        crate::buffer::PayloadProvenance::CallerSupplied,
3166      )
3167      .expect("wrappable")
3168      .expect("present");
3169      let rebuilt =
3170        ffmpeg_packet_from_owned_video_packet(&video, PacketLimits::default()).expect("rebuilt");
3171      let carried = packet_side_data(&rebuilt).expect("readable");
3172      assert_eq!(carried.len(), 1, "{name}: video lost its side data");
3173      assert_eq!(carried[0].kind(), expected_kind, "{name}: video");
3174      assert_eq!(carried[0].data(), video.extra().side_data()[0].data());
3175
3176      let audio = audio_packet_from_borrowed::<crate::Owned>(
3177        source,
3178        tb,
3179        PacketLimits::default(),
3180        crate::buffer::PayloadProvenance::CallerSupplied,
3181      )
3182      .expect("wrappable")
3183      .expect("present");
3184      let rebuilt =
3185        ffmpeg_packet_from_owned_audio_packet(&audio, PacketLimits::default()).expect("rebuilt");
3186      let carried = packet_side_data(&rebuilt).expect("readable");
3187      assert_eq!(carried.len(), 1, "{name}: audio lost its side data");
3188      assert_eq!(carried[0].data(), audio.extra().side_data()[0].data());
3189
3190      let subtitle = subtitle_packet_from_borrowed::<crate::Owned>(
3191        source,
3192        tb,
3193        PacketLimits::default(),
3194        crate::buffer::PayloadProvenance::CallerSupplied,
3195      )
3196      .expect("wrappable")
3197      .expect("present");
3198      let rebuilt = ffmpeg_packet_from_owned_subtitle_packet(&subtitle, PacketLimits::default())
3199        .expect("rebuilt");
3200      let carried = packet_side_data(&rebuilt).expect("readable");
3201      assert_eq!(carried.len(), 1, "{name}: subtitle lost its side data");
3202      assert_eq!(carried[0].data(), subtitle.extra().side_data()[0].data());
3203    }
3204  }
3205
3206  #[test]
3207  fn a_side_data_type_this_build_cannot_name_is_refused_not_dropped() {
3208    // This crate carries side-data types as the raw integers they are
3209    // on the wire, so a hand-built entry can name anything. Handing an
3210    // unknown one to C would either form an invalid discriminant or
3211    // attach a type nothing downstream reads — and dropping it quietly
3212    // is the very defect this whole seam exists to close.
3213    let limit = crate::ffi::side_data_type_count();
3214    let packet = mediadecode::packet::VideoPacket::new(
3215      FfmpegBytes::copy_from_slice(&[1u8]),
3216      VideoPacketExtra::new(0).with_side_data(vec![SideDataEntry::new(
3217        limit,
3218        FfmpegBytes::copy_from_slice(&[9u8]),
3219      )]),
3220    );
3221    match ffmpeg_packet_from_owned_video_packet(&packet, PacketLimits::default()).map(|_| ()) {
3222      Err(PacketBuildError::UnknownSideData(p)) => {
3223        assert_eq!(p.kind(), limit);
3224        assert_eq!(p.limit(), limit);
3225      }
3226      other => panic!("expected UnknownSideData, got {other:?}"),
3227    }
3228    assert!(matches!(
3229      ffmpeg_packet_from_owned_video_packet(&mediadecode::packet::VideoPacket::new(
3230        FfmpegBytes::copy_from_slice(&[1u8]),
3231        VideoPacketExtra::new(0).with_side_data(vec![SideDataEntry::new(-1, FfmpegBytes::copy_from_slice(&[9u8]))]),
3232      ), PacketLimits::default())
3233      .map(|_| ()),
3234      Err(PacketBuildError::UnknownSideData(p)) if p.kind() == -1,
3235    ));
3236  }
3237
3238  #[test]
3239  fn a_side_data_only_packet_is_delivered_on_every_timed_arm() {
3240    // Codec-control data with no body is still a packet. Dropped as an
3241    // "empty marker", it leaves a decoder running on parameters the
3242    // container has already replaced — and says nothing about it.
3243    let packet = side_data_only_packet();
3244    let tb = mediadecode::Timebase::default();
3245
3246    let video = video_packet_from_borrowed::<crate::Owned>(
3247      &packet,
3248      tb,
3249      PacketLimits::default(),
3250      crate::buffer::PayloadProvenance::CallerSupplied,
3251    )
3252    .expect("wrappable")
3253    .expect("a side-data-only packet is a packet");
3254    assert!(video.data().as_ref().is_empty(), "no body, but a buffer");
3255    assert_eq!(video.extra().side_data().len(), 1);
3256    assert_eq!(video.extra().side_data()[0].kind(), NEW_EXTRADATA);
3257    assert_eq!(video.extra().side_data()[0].data(), &[1, 2, 3, 4]);
3258
3259    let audio = audio_packet_from_borrowed::<crate::Owned>(
3260      &packet,
3261      tb,
3262      PacketLimits::default(),
3263      crate::buffer::PayloadProvenance::CallerSupplied,
3264    )
3265    .expect("wrappable")
3266    .expect("present");
3267    assert!(audio.data().as_ref().is_empty());
3268    assert_eq!(audio.extra().side_data()[0].data(), &[1, 2, 3, 4]);
3269
3270    let subtitle = subtitle_packet_from_borrowed::<crate::Owned>(
3271      &packet,
3272      tb,
3273      PacketLimits::default(),
3274      crate::buffer::PayloadProvenance::CallerSupplied,
3275    )
3276    .expect("wrappable")
3277    .expect("present");
3278    assert!(subtitle.data().as_ref().is_empty());
3279    assert_eq!(subtitle.extra().side_data()[0].data(), &[1, 2, 3, 4]);
3280
3281    let data = data_packet_from_borrowed::<crate::Owned>(
3282      &packet,
3283      tb,
3284      PacketLimits::default(),
3285      crate::buffer::PayloadProvenance::CallerSupplied,
3286    )
3287    .expect("wrappable")
3288    .expect("present");
3289    assert!(data.data().as_ref().is_empty());
3290    assert_eq!(data.extra().side_data()[0].data(), &[1, 2, 3, 4]);
3291
3292    // The one arm where a payload-less packet really is nothing: an
3293    // attachment is its bytes, and there are none.
3294    assert!(
3295      attachment_packet_from_borrowed::<crate::Owned>(
3296        &packet,
3297        PacketLimits::default(),
3298        crate::buffer::PayloadProvenance::CallerSupplied
3299      )
3300      .expect("wrappable")
3301      .is_none(),
3302      "an attachment with no bytes is no attachment",
3303    );
3304  }
3305
3306  #[test]
3307  fn side_data_rides_a_packet_that_has_a_body_too() {
3308    // The seat's documented promise — "the raw side-data entries from
3309    // `AVPacket.side_data`" — was never kept by the conversion before.
3310    use ffmpeg_next::{
3311      ffi::{AVPacketSideDataType, av_packet_new_side_data},
3312      packet::Mut,
3313    };
3314    let mut packet = Packet::copy(&[7u8, 7, 7]);
3315    unsafe {
3316      let ptr = av_packet_new_side_data(
3317        packet.as_mut_ptr(),
3318        AVPacketSideDataType::AV_PKT_DATA_NEW_EXTRADATA,
3319        2,
3320      );
3321      assert!(!ptr.is_null());
3322      core::ptr::copy_nonoverlapping([9u8, 9].as_ptr(), ptr, 2);
3323    }
3324    let video = video_packet_from_borrowed::<crate::Owned>(
3325      &packet,
3326      mediadecode::Timebase::default(),
3327      PacketLimits::default(),
3328      crate::buffer::PayloadProvenance::CallerSupplied,
3329    )
3330    .expect("wrappable")
3331    .expect("present");
3332    assert_eq!(video.data().as_ref(), &[7, 7, 7]);
3333    assert_eq!(video.extra().side_data()[0].data(), &[9, 9]);
3334  }
3335
3336  #[test]
3337  fn an_empty_packet_is_absent_and_a_real_one_is_present() {
3338    let tb = mediadecode::Timebase::default();
3339    // No payload at all: the marker some demuxers emit. Absent, not an
3340    // error — this is the only thing a pull loop may skip.
3341    let empty = Packet::empty();
3342    assert!(
3343      video_packet_from_borrowed::<crate::Owned>(
3344        &empty,
3345        tb,
3346        PacketLimits::default(),
3347        crate::buffer::PayloadProvenance::CallerSupplied
3348      )
3349      .expect("not a failure")
3350      .is_none()
3351    );
3352    assert!(
3353      attachment_packet_from_borrowed::<crate::Owned>(
3354        &empty,
3355        PacketLimits::default(),
3356        crate::buffer::PayloadProvenance::CallerSupplied
3357      )
3358      .expect("not a failure")
3359      .is_none()
3360    );
3361
3362    let real = Packet::copy(&[9u8, 8, 7]);
3363    let wrapped = video_packet_from_borrowed::<crate::Owned>(
3364      &real,
3365      tb,
3366      PacketLimits::default(),
3367      crate::buffer::PayloadProvenance::CallerSupplied,
3368    )
3369    .expect("wrappable")
3370    .expect("present");
3371    assert_eq!(wrapped.data().as_ref(), &[9, 8, 7]);
3372  }
3373
3374  #[test]
3375  fn nv12_round_trips() {
3376    assert_eq!(
3377      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_NV12 as i32),
3378      PixelFormat::Nv12,
3379    );
3380  }
3381
3382  #[test]
3383  fn p010be_maps_to_p010be() {
3384    // BE must map to the BE variant — the previous "fold to LE"
3385    // mapping silently corrupted P010BE pixel data via the safe
3386    // export path. The unsupported-format gate in `convert::av_frame_to_video_frame`
3387    // is the right place to reject BE today.
3388    assert_eq!(
3389      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_P010BE as i32),
3390      PixelFormat::P010Be,
3391    );
3392  }
3393
3394  #[test]
3395  fn unnamed_raw_maps_to_none() {
3396    assert_eq!(from_av_pixel_format(-99_999), PixelFormat::None);
3397  }
3398
3399  #[test]
3400  fn av_pix_fmt_none_maps_to_none() {
3401    assert_eq!(
3402      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_NONE as i32),
3403      PixelFormat::None,
3404    );
3405  }
3406
3407  #[test]
3408  fn hw_formats_detected() {
3409    assert!(is_hardware_pix_fmt(
3410      AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32
3411    ));
3412    assert!(is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_VAAPI as i32));
3413    assert!(is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_CUDA as i32));
3414    assert!(is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_D3D11 as i32));
3415  }
3416
3417  #[test]
3418  fn cpu_formats_not_detected_as_hw() {
3419    assert!(!is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_NV12 as i32));
3420    assert!(!is_hardware_pix_fmt(
3421      AVPixelFormat::AV_PIX_FMT_YUV420P as i32
3422    ));
3423    assert!(!is_hardware_pix_fmt(AVPixelFormat::AV_PIX_FMT_NONE as i32));
3424  }
3425
3426  #[test]
3427  fn hw_formats_map_to_none_in_pixel_format() {
3428    // HW sentinels intentionally don't have a mediadecode::PixelFormat
3429    // representation — they're not CPU pixel data.
3430    assert_eq!(
3431      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as i32),
3432      PixelFormat::None,
3433    );
3434    assert_eq!(
3435      from_av_pixel_format(AVPixelFormat::AV_PIX_FMT_VAAPI as i32),
3436      PixelFormat::None,
3437    );
3438  }
3439}