Skip to main content

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}