mediadecode_ffmpeg/buffer.rs
1//! `FfmpegBuffer` — owned, refcounted handle to an `AVBufferRef`.
2//!
3//! Both `AVPacket.buf` and `AVFrame.buf[i]` are FFmpeg's refcounted
4//! buffers. This crate's adapter exposes them through a `Bytes`-like
5//! type that implements `AsRef<[u8]>` so the buffer can be used as the
6//! `B` parameter on `mediadecode::Packet<A, B>` / `Frame<A, B>` without
7//! copying. Cloning bumps the refcount; dropping releases one
8//! reference and lets FFmpeg free the memory when the last reference
9//! goes away.
10
11use core::{fmt, slice};
12
13use ffmpeg_next::ffi::{AVBufferRef, av_buffer_ref, av_buffer_unref};
14
15/// Owned, refcounted handle to a contiguous byte range inside an
16/// `AVBufferRef`.
17///
18/// Holds one reference to the underlying `AVBufferRef`. The `view`
19/// (offset + length) carves out a sub-region of the buffer's data —
20/// useful when an `AVFrame` packs multiple planes into a single
21/// allocation (e.g. NV12 with `data[1] == data[0] + Y_size`). Each
22/// plane gets its own `FfmpegBuffer` view at a different offset,
23/// every view bumps the refcount, and dropping one doesn't free the
24/// underlying buffer until the last view goes away.
25///
26/// `Clone` shares the same view (offset + length unchanged). `Drop`
27/// releases one reference via `av_buffer_unref`.
28pub struct FfmpegBuffer {
29 inner: *mut AVBufferRef,
30 /// Offset from `inner.data` where this view starts.
31 offset: usize,
32 /// Byte length of this view. Always `<= inner.size - offset`.
33 len: usize,
34}
35
36// SAFETY: `AVBufferRef`'s refcount is atomically managed by FFmpeg, so
37// transferring ownership of an `FfmpegBuffer` across threads is sound —
38// `Drop` (which is the only operation that mutates the refcount) calls
39// `av_buffer_unref` which uses atomic decrement.
40//
41// We deliberately do **not** implement `Sync`. Decoder-output buffers
42// from FFmpeg are immutable in practice, but the underlying
43// `AVBufferRef.data` is reachable through `as_av_buffer_ref` and
44// nothing in this type's contract prevents a caller from passing the
45// pointer to an FFmpeg API that mutates the bytes — concurrent reads
46// from another thread would then race. `Send`-only is the conservative
47// stance.
48unsafe impl Send for FfmpegBuffer {}
49
50impl FfmpegBuffer {
51 /// Constructs an `FfmpegBuffer` by **incrementing** the refcount of
52 /// an existing `AVBufferRef`. The view covers the buffer's full
53 /// `size` (offset 0). The caller's `*mut AVBufferRef` is unchanged —
54 /// it still owns its own reference and must be released independently.
55 ///
56 /// Returns `None` if `buf` is null or `av_buffer_ref` fails (out of
57 /// memory).
58 ///
59 /// # Safety
60 ///
61 /// `buf` must either be null or point to a live `AVBufferRef` for
62 /// the duration of this call.
63 #[inline]
64 pub unsafe fn from_ref(buf: *mut AVBufferRef) -> Option<Self> {
65 if buf.is_null() {
66 return None;
67 }
68 // SAFETY: caller upholds liveness; av_buffer_ref handles atomicity.
69 let new_ref = unsafe { av_buffer_ref(buf) };
70 if new_ref.is_null() {
71 return None;
72 }
73 let len = unsafe { (*new_ref).size as usize };
74 Some(Self {
75 inner: new_ref,
76 offset: 0,
77 len,
78 })
79 }
80
81 /// Constructs an `FfmpegBuffer` view over a sub-region of an existing
82 /// `AVBufferRef`. The refcount is incremented; the view runs from
83 /// `offset` for `len` bytes inside `(*buf).data`.
84 ///
85 /// Returns `None` if `buf` is null, `av_buffer_ref` fails, or
86 /// `offset + len > (*buf).size`.
87 ///
88 /// # Safety
89 ///
90 /// `buf` must either be null or point to a live `AVBufferRef` for
91 /// the duration of this call.
92 #[inline]
93 pub unsafe fn from_ref_view(buf: *mut AVBufferRef, offset: usize, len: usize) -> Option<Self> {
94 if buf.is_null() {
95 return None;
96 }
97 let buf_size = unsafe { (*buf).size };
98 let end = offset.checked_add(len)?;
99 if end > buf_size {
100 return None;
101 }
102 let new_ref = unsafe { av_buffer_ref(buf) };
103 if new_ref.is_null() {
104 return None;
105 }
106 Some(Self {
107 inner: new_ref,
108 offset,
109 len,
110 })
111 }
112
113 /// Allocates a 1-byte refcounted `AVBufferRef` and exposes a
114 /// zero-length view over it. Useful as a placeholder when
115 /// constructing an "empty" `mediadecode::VideoFrame` /
116 /// `AudioFrame` to pass to a decoder's `receive_frame` — the
117 /// decoder overwrites the planes on success, but the slot needs a
118 /// non-null buffer to satisfy the array shape.
119 ///
120 /// # Panics
121 ///
122 /// Panics if FFmpeg fails to allocate (out-of-memory). Allocations
123 /// of one byte never realistically fail; this matches the
124 /// behaviour of `Clone` on a populated `FfmpegBuffer`. Callers who
125 /// need to recover from OOM should use [`Self::try_empty`].
126 #[inline]
127 pub fn empty() -> Self {
128 Self::try_empty().expect("FfmpegBuffer::empty: av_buffer_alloc returned null (OOM)")
129 }
130
131 /// Fallible counterpart to [`Self::empty`]. Returns `None` if the
132 /// 1-byte `av_buffer_alloc` fails (out-of-memory). Use this when
133 /// you'd rather propagate an error than panic.
134 #[inline]
135 pub fn try_empty() -> Option<Self> {
136 use ffmpeg_next::ffi::av_buffer_alloc;
137 let raw = unsafe { av_buffer_alloc(1) };
138 if raw.is_null() {
139 return None;
140 }
141 // SAFETY: `raw` is non-null and freshly allocated; we transfer
142 // its single reference to the new `FfmpegBuffer`.
143 let mut buf = unsafe { Self::take(raw) }?;
144 buf.len = 0;
145 Some(buf)
146 }
147
148 /// Borrows the refcounted payload of an `ffmpeg::Packet` as an
149 /// `FfmpegBuffer` view. The packet's `AVBufferRef` is shared via
150 /// refcount bump — no copy. The view spans exactly
151 /// `(*packet.as_ptr()).data .. data + size` (the *payload*) — not
152 /// the entire underlying allocation: `AVPacket.buf` can be larger
153 /// than the payload (encoder padding, oversized buffers, sub-range
154 /// references after `av_packet_split_side_data`), so exposing the
155 /// whole AVBufferRef would corrupt downstream consumers that
156 /// trust the buffer to be just the compressed bytes.
157 ///
158 /// Returns `None` when the packet has no refcounted buffer
159 /// (`buf == NULL`) — callers needing universal coverage of stack-
160 /// or arena-allocated AVPackets can fall back to
161 /// [`Self::copy_from_slice`] over `packet.data()`.
162 #[inline]
163 pub fn from_packet(packet: &ffmpeg_next::Packet) -> Option<Self> {
164 use ffmpeg_next::packet::Ref;
165 // SAFETY: `packet` keeps the AVPacket live for the duration of
166 // this call; `.buf`, `.data`, `.size` are public fields on
167 // AVPacket. `buf` may be null (stack-allocated packets).
168 let buf_ptr = unsafe { (*packet.as_ptr()).buf };
169 if buf_ptr.is_null() {
170 return None;
171 }
172 let data_ptr = unsafe { (*packet.as_ptr()).data };
173 let size_raw = unsafe { (*packet.as_ptr()).size };
174 if data_ptr.is_null() || size_raw <= 0 {
175 return None;
176 }
177 let payload_len = size_raw as usize;
178 // Compute the offset of `data` inside `buf`. AVPacket guarantees
179 // `data` lies within `buf->data .. buf->data + buf->size`, but
180 // we verify defensively with `from_ref_view` (which bounds-
181 // checks against `buf->size`).
182 let buf_data = unsafe { (*buf_ptr).data };
183 if buf_data.is_null() {
184 return None;
185 }
186 let offset = (data_ptr as usize).wrapping_sub(buf_data as usize);
187 unsafe { Self::from_ref_view(buf_ptr, offset, payload_len) }
188 }
189
190 /// Borrows one of an `ffmpeg::Frame`'s plane buffers
191 /// (`AVFrame.buf[plane_idx]`) as an `FfmpegBuffer` view. The view
192 /// covers the underlying `AVBufferRef`'s full size; for
193 /// per-plane subviews into a multi-plane shared allocation see
194 /// [`crate::convert::video_frame_from`].
195 ///
196 /// Returns `None` when `plane_idx >= 8` or the plane has no
197 /// buffer attached.
198 #[inline]
199 pub fn from_frame_plane(frame: &ffmpeg_next::Frame, plane_idx: usize) -> Option<Self> {
200 if plane_idx >= 8 {
201 return None;
202 }
203 // SAFETY: `frame` keeps the AVFrame live for the duration of
204 // this call; `buf[]` is a public fixed-size array on AVFrame.
205 let buf_ptr = unsafe { (*frame.as_ptr()).buf[plane_idx] };
206 unsafe { Self::from_ref(buf_ptr) }
207 }
208
209 /// Allocates a fresh refcounted `AVBufferRef` and copies `bytes` into
210 /// it. Returns `None` if the FFmpeg allocation fails.
211 ///
212 /// Useful for adapting non-refcounted FFmpeg payloads (e.g. subtitle
213 /// `AVSubtitleRect.text` / `.ass` / `.data[0]`) into the refcounted
214 /// `FfmpegBuffer` shape the rest of the crate carries.
215 #[inline]
216 pub fn copy_from_slice(bytes: &[u8]) -> Option<Self> {
217 use ffmpeg_next::ffi::av_buffer_alloc;
218 let len = bytes.len();
219 // av_buffer_alloc(0) is allowed on most platforms but isn't
220 // portable; force a 1-byte allocation in that case so the resulting
221 // buffer is non-null.
222 let alloc_size = len.max(1);
223 let raw = unsafe { av_buffer_alloc(alloc_size as _) };
224 if raw.is_null() {
225 return None;
226 }
227 if len > 0 {
228 // SAFETY: raw is non-null and freshly allocated with `alloc_size >= len`
229 // bytes; the source slice is valid for `len` reads.
230 unsafe {
231 core::ptr::copy_nonoverlapping(bytes.as_ptr(), (*raw).data, len);
232 }
233 }
234 Some(Self {
235 inner: raw,
236 offset: 0,
237 len,
238 })
239 }
240
241 /// Takes ownership of an existing `AVBufferRef` without bumping the
242 /// refcount. The view covers the buffer's full size. Use this when
243 /// the caller's reference will be dropped (e.g. transferring out of
244 /// an `AVPacket`/`AVFrame`).
245 ///
246 /// Returns `None` if `buf` is null.
247 ///
248 /// # Safety
249 ///
250 /// `buf` must be either null or a live `AVBufferRef` whose reference
251 /// the caller is willing to give up. After a successful call, the
252 /// caller MUST NOT call `av_buffer_unref` on the same pointer.
253 #[inline]
254 pub unsafe fn take(buf: *mut AVBufferRef) -> Option<Self> {
255 if buf.is_null() {
256 return None;
257 }
258 let len = unsafe { (*buf).size };
259 Some(Self {
260 inner: buf,
261 offset: 0,
262 len,
263 })
264 }
265
266 /// Number of bytes visible through this view.
267 #[inline]
268 pub fn len(&self) -> usize {
269 self.len
270 }
271
272 /// True when the view is zero bytes long.
273 #[inline]
274 pub fn is_empty(&self) -> bool {
275 self.len == 0
276 }
277
278 /// Raw pointer to the start of this view. Valid for [`Self::len`]
279 /// bytes for the lifetime of `self`. Returns a dangling-but-aligned
280 /// pointer when the view is empty (parallel to `core::ptr::NonNull::dangling`)
281 /// — the caller must respect [`Self::len`] before any read.
282 #[inline]
283 pub fn as_ptr(&self) -> *const u8 {
284 // SAFETY: inner is non-null per constructor invariant. We guard
285 // against null `data` (possible when the underlying AVBufferRef
286 // was created with size 0) before doing pointer arithmetic, since
287 // `null.add(offset)` is UB for offset > 0 even before any deref.
288 unsafe {
289 let data = (*self.inner).data;
290 if data.is_null() {
291 // Safe sentinel for empty/dataless buffers. The caller must
292 // gate any read on `len() == 0`.
293 return core::ptr::NonNull::<u8>::dangling().as_ptr();
294 }
295 (data as *const u8).add(self.offset)
296 }
297 }
298
299 /// Underlying `*const AVBufferRef`. Useful when handing the buffer
300 /// back to an FFmpeg API that expects a borrowed pointer (do **not**
301 /// call `av_buffer_unref` on the result — `self` still owns the ref).
302 /// The returned pointer references the **whole** buffer, not just
303 /// this view's sub-region.
304 ///
305 /// This intentionally returns `*const`, not `*mut`. FFmpeg APIs that
306 /// mutate via the buffer (e.g. `av_buffer_make_writable`) should be
307 /// reached through the unsafe constructors which transfer ownership;
308 /// shared `&self` access must not allow aliased writes.
309 #[inline]
310 pub fn as_av_buffer_ref(&self) -> *const AVBufferRef {
311 self.inner as *const _
312 }
313
314 /// Byte offset of this view's start within the underlying buffer.
315 #[inline]
316 pub fn offset(&self) -> usize {
317 self.offset
318 }
319
320 /// Fallible counterpart to [`Clone::clone`]. Returns `None` if
321 /// `av_buffer_ref` fails (out-of-memory) instead of panicking.
322 /// Use this in OOM-recoverable paths; the `Clone` impl panics on
323 /// the same failure to match Rust's standard `Clone` contract.
324 #[inline]
325 pub fn try_clone(&self) -> Option<Self> {
326 // SAFETY: inner is non-null per invariant; av_buffer_ref
327 // atomically bumps the refcount and returns null on OOM only.
328 let new_ref = unsafe { av_buffer_ref(self.inner) };
329 if new_ref.is_null() {
330 return None;
331 }
332 Some(Self {
333 inner: new_ref,
334 offset: self.offset,
335 len: self.len,
336 })
337 }
338}
339
340impl Clone for FfmpegBuffer {
341 /// Refcounts the underlying `AVBufferRef`. **Panics** on OOM (see
342 /// [`Self::try_clone`] for the fallible variant).
343 fn clone(&self) -> Self {
344 self
345 .try_clone()
346 .expect("FfmpegBuffer::clone: av_buffer_ref returned null (OOM)")
347 }
348}
349
350impl Drop for FfmpegBuffer {
351 fn drop(&mut self) {
352 // SAFETY: inner is a live AVBufferRef per invariant. `av_buffer_unref`
353 // takes `**mut AVBufferRef` and zeroes the pointer; we don't read
354 // self.inner after this.
355 unsafe { av_buffer_unref(&mut self.inner) };
356 }
357}
358
359impl AsRef<[u8]> for FfmpegBuffer {
360 #[inline]
361 fn as_ref(&self) -> &[u8] {
362 // SAFETY:
363 // - `inner` is non-null (constructor invariant).
364 // - The data pointer is non-null and valid for the underlying
365 // buffer's `size` bytes per FFmpeg's contract.
366 // - `offset + len <= buffer size` is established at construction
367 // (and preserved by Clone), so the view stays in-bounds.
368 // - The buffer is immutable for the lifetime we hold the refcount.
369 unsafe {
370 let data = (*self.inner).data as *const u8;
371 if data.is_null() || self.len == 0 {
372 return &[];
373 }
374 // `offset + len <= buffer size` was established at construction
375 // (and preserved by Clone), so the resulting pointer + length
376 // stays inside the AVBufferRef's allocation.
377 slice::from_raw_parts(data.add(self.offset), self.len)
378 }
379 }
380}
381
382impl fmt::Debug for FfmpegBuffer {
383 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384 f.debug_struct("FfmpegBuffer")
385 .field("len", &self.len())
386 .finish()
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use ffmpeg_next::ffi::av_buffer_alloc;
394
395 /// Allocate a fresh AVBufferRef of `size` bytes, fill it with `fill`,
396 /// and wrap it in our type via `take` (taking ownership of the
397 /// caller's reference).
398 fn make_buffer(size: usize, fill: u8) -> FfmpegBuffer {
399 let raw = unsafe { av_buffer_alloc(size as _) };
400 assert!(!raw.is_null(), "av_buffer_alloc failed");
401 unsafe {
402 let data = (*raw).data;
403 core::ptr::write_bytes(data, fill, size);
404 }
405 unsafe { FfmpegBuffer::take(raw) }.expect("non-null take")
406 }
407
408 #[test]
409 fn null_take_returns_none() {
410 assert!(unsafe { FfmpegBuffer::take(core::ptr::null_mut()) }.is_none());
411 }
412
413 #[test]
414 fn null_from_ref_returns_none() {
415 assert!(unsafe { FfmpegBuffer::from_ref(core::ptr::null_mut()) }.is_none());
416 }
417
418 #[test]
419 fn allocated_buffer_round_trips_bytes() {
420 let buf = make_buffer(16, 0xAB);
421 assert_eq!(buf.len(), 16);
422 assert!(!buf.is_empty());
423 let slice = buf.as_ref();
424 assert_eq!(slice.len(), 16);
425 assert!(slice.iter().all(|&b| b == 0xAB));
426 }
427
428 #[test]
429 fn clone_bumps_refcount_and_keeps_data_alive() {
430 let original = make_buffer(8, 0x5A);
431 let cloned = original.clone();
432 // Both references see the same bytes.
433 assert_eq!(original.as_ref(), cloned.as_ref());
434 assert_eq!(original.as_ptr(), cloned.as_ptr());
435 // Drop one — the other must still be valid.
436 drop(original);
437 assert_eq!(cloned.len(), 8);
438 assert!(cloned.as_ref().iter().all(|&b| b == 0x5A));
439 }
440
441 #[test]
442 fn debug_shows_length() {
443 let buf = make_buffer(42, 0);
444 let s = format!("{buf:?}");
445 assert!(s.contains("len: 42"), "got {s}");
446 }
447
448 #[test]
449 fn from_ref_view_carves_out_subregion() {
450 // 24-byte buffer: bytes 0..8 = 0xAA, 8..16 = 0xBB, 16..24 = 0xCC.
451 let raw = unsafe { av_buffer_alloc(24) };
452 assert!(!raw.is_null());
453 unsafe {
454 let data = (*raw).data;
455 core::ptr::write_bytes(data, 0xAA, 8);
456 core::ptr::write_bytes(data.add(8), 0xBB, 8);
457 core::ptr::write_bytes(data.add(16), 0xCC, 8);
458 }
459
460 // Three independent views, each with its own refcount.
461 let view_a = unsafe { FfmpegBuffer::from_ref_view(raw, 0, 8) }.expect("view_a");
462 let view_b = unsafe { FfmpegBuffer::from_ref_view(raw, 8, 8) }.expect("view_b");
463 let view_c = unsafe { FfmpegBuffer::from_ref_view(raw, 16, 8) }.expect("view_c");
464 assert!(view_a.as_ref().iter().all(|&b| b == 0xAA));
465 assert!(view_b.as_ref().iter().all(|&b| b == 0xBB));
466 assert!(view_c.as_ref().iter().all(|&b| b == 0xCC));
467 assert_eq!(view_a.offset(), 0);
468 assert_eq!(view_b.offset(), 8);
469 assert_eq!(view_c.offset(), 16);
470 assert_eq!(view_a.len(), 8);
471
472 // Drop the original; the views still keep the buffer alive.
473 unsafe { av_buffer_unref(&mut { raw }) };
474 let _ = (view_a, view_b, view_c);
475 }
476
477 #[test]
478 fn from_ref_view_rejects_out_of_bounds() {
479 let raw = unsafe { av_buffer_alloc(16) };
480 assert!(!raw.is_null());
481 // Past the end:
482 assert!(unsafe { FfmpegBuffer::from_ref_view(raw, 10, 8) }.is_none());
483 // Overflow protection (offset + len overflows usize):
484 assert!(unsafe { FfmpegBuffer::from_ref_view(raw, usize::MAX, 1) }.is_none());
485 unsafe { av_buffer_unref(&mut { raw }) };
486 }
487
488 #[test]
489 fn empty_buffer_returns_empty_slice() {
490 // av_buffer_alloc(0) is valid in FFmpeg; some platforms return a
491 // non-null buf with data == null and size == 0. Either way, our
492 // as_ref must return an empty slice without dereferencing data.
493 let raw = unsafe { av_buffer_alloc(0) };
494 if raw.is_null() {
495 // Some allocators refuse 0; skip the test in that case.
496 return;
497 }
498 let buf = unsafe { FfmpegBuffer::take(raw) }.expect("non-null take");
499 assert_eq!(buf.len(), 0);
500 assert!(buf.is_empty());
501 assert_eq!(buf.as_ref(), &[] as &[u8]);
502 }
503}