use core::{fmt, slice};
use derive_more::{IsVariant, TryUnwrap, Unwrap};
use ffmpeg_next::ffi::{AVBufferRef, av_buffer_ref, av_buffer_unref};
pub struct FfmpegBuffer {
inner: *mut AVBufferRef,
offset: usize,
len: usize,
}
unsafe impl Send for FfmpegBuffer {}
impl FfmpegBuffer {
#[inline]
pub unsafe fn from_ref(buf: *mut AVBufferRef) -> Option<Self> {
if buf.is_null() {
return None;
}
let new_ref = unsafe { av_buffer_ref(buf) };
if new_ref.is_null() {
return None;
}
let len = unsafe { (*new_ref).size as usize };
Some(Self {
inner: new_ref,
offset: 0,
len,
})
}
#[inline]
pub unsafe fn from_ref_view(buf: *mut AVBufferRef, offset: usize, len: usize) -> Option<Self> {
if buf.is_null() {
return None;
}
let buf_size = unsafe { (*buf).size };
let end = offset.checked_add(len)?;
if end > buf_size {
return None;
}
let new_ref = unsafe { av_buffer_ref(buf) };
if new_ref.is_null() {
return None;
}
Some(Self {
inner: new_ref,
offset,
len,
})
}
#[inline]
pub fn empty() -> Self {
Self::try_empty().expect("FfmpegBuffer::empty: av_buffer_alloc returned null (OOM)")
}
#[inline]
pub fn try_empty() -> Option<Self> {
use ffmpeg_next::ffi::av_buffer_alloc;
let raw = unsafe { av_buffer_alloc(1) };
if raw.is_null() {
return None;
}
let mut buf = unsafe { Self::take(raw) }?;
buf.len = 0;
Some(buf)
}
#[inline]
pub fn from_packet(packet: &ffmpeg_next::Packet) -> Result<Option<Self>, PacketBufferError> {
use ffmpeg_next::packet::Ref;
unsafe { payload_of(packet.as_ptr()) }
}
#[inline]
pub fn from_frame_plane(frame: &ffmpeg_next::Frame, plane_idx: usize) -> Option<Self> {
if plane_idx >= 8 {
return None;
}
let buf_ptr = unsafe { (*frame.as_ptr()).buf[plane_idx] };
unsafe { Self::from_ref(buf_ptr) }
}
#[inline]
pub fn copy_from_slice(bytes: &[u8]) -> Option<Self> {
use ffmpeg_next::ffi::av_buffer_alloc;
let len = bytes.len();
let alloc_size = len.max(1);
let raw = unsafe { av_buffer_alloc(alloc_size as _) };
if raw.is_null() {
return None;
}
if len > 0 {
unsafe {
core::ptr::copy_nonoverlapping(bytes.as_ptr(), (*raw).data, len);
}
}
Some(Self {
inner: raw,
offset: 0,
len,
})
}
#[inline]
pub unsafe fn take(buf: *mut AVBufferRef) -> Option<Self> {
if buf.is_null() {
return None;
}
let len = unsafe { (*buf).size };
Some(Self {
inner: buf,
offset: 0,
len,
})
}
#[inline]
pub fn len(&self) -> usize {
self.len
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
pub fn as_ptr(&self) -> *const u8 {
unsafe {
let data = (*self.inner).data;
if data.is_null() {
return core::ptr::NonNull::<u8>::dangling().as_ptr();
}
(data as *const u8).add(self.offset)
}
}
#[inline]
pub fn as_av_buffer_ref(&self) -> *const AVBufferRef {
self.inner as *const _
}
#[inline]
pub fn offset(&self) -> usize {
self.offset
}
#[cfg(feature = "resample")]
#[inline]
pub(crate) fn shrink_to(&mut self, len: usize) {
self.len = self.len.min(len);
}
#[inline]
pub fn try_clone(&self) -> Option<Self> {
let new_ref = unsafe { av_buffer_ref(self.inner) };
if new_ref.is_null() {
return None;
}
Some(Self {
inner: new_ref,
offset: self.offset,
len: self.len,
})
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("out of memory referencing a {len}-byte packet payload")]
pub struct Refcount {
len: usize,
}
impl Refcount {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(len: usize) -> Self {
Self { len }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn len(&self) -> usize {
self.len
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("a {len}-byte payload at offset {offset} does not lie inside a {size}-byte buffer")]
pub struct Bounds {
offset: usize,
len: usize,
size: usize,
}
impl Bounds {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(offset: usize, len: usize, size: usize) -> Self {
Self { offset, len, size }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn offset(&self) -> usize {
self.offset
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn len(&self) -> usize {
self.len
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn size(&self) -> usize {
self.size
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("a packet declaring {count} side-data entries cannot be carried (limit {cap})")]
pub struct SideDataEntries {
count: i32,
cap: usize,
}
impl SideDataEntries {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(count: i32, cap: usize) -> Self {
Self { count, cap }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn count(&self) -> i32 {
self.count
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn cap(&self) -> usize {
self.cap
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("a packet declaring {count} side-data entries carries no array")]
pub struct SideDataArray {
count: i32,
}
impl SideDataArray {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(count: i32) -> Self {
Self { count }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn count(&self) -> i32 {
self.count
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("side-data entry {index} declares {size} bytes and carries no data")]
pub struct SideDataPayload {
index: usize,
size: usize,
}
impl SideDataPayload {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(index: usize, size: usize) -> Self {
Self { index, size }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn index(&self) -> usize {
self.index
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn size(&self) -> usize {
self.size
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{bytes} bytes of side data cannot be carried (limit {cap})")]
pub struct SideDataBytes {
bytes: usize,
cap: usize,
}
impl SideDataBytes {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(bytes: usize, cap: usize) -> Self {
Self { bytes, cap }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn bytes(&self) -> usize {
self.bytes
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn cap(&self) -> usize {
self.cap
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("packet flags {raw:#x} do not fit the portable flag set")]
pub struct UnrepresentableFlags {
raw: i32,
}
impl UnrepresentableFlags {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(raw: i32) -> Self {
Self { raw }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn raw(&self) -> i32 {
self.raw
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("out of memory copying {size} bytes of side data")]
pub struct SideDataAlloc {
size: usize,
}
impl SideDataAlloc {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(size: usize) -> Self {
Self { size }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn size(&self) -> usize {
self.size
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error, IsVariant, Unwrap, TryUnwrap)]
#[unwrap(ref, ref_mut)]
#[try_unwrap(ref, ref_mut)]
pub enum PacketBufferError {
#[error(transparent)]
Refcount(#[from] Refcount),
#[error(transparent)]
Bounds(#[from] Bounds),
#[error(transparent)]
SideDataEntries(#[from] SideDataEntries),
#[error(transparent)]
SideDataArray(#[from] SideDataArray),
#[error(transparent)]
SideDataPayload(#[from] SideDataPayload),
#[error(transparent)]
SideDataBytes(#[from] SideDataBytes),
#[error(transparent)]
UnrepresentableFlags(#[from] UnrepresentableFlags),
#[error(transparent)]
SideDataAlloc(#[from] SideDataAlloc),
}
pub(crate) unsafe fn payload_of(
pkt: *const ffmpeg_next::ffi::AVPacket,
) -> Result<Option<FfmpegBuffer>, PacketBufferError> {
let buf_ptr = unsafe { (*pkt).buf };
let data_ptr = unsafe { (*pkt).data };
let size_raw = unsafe { (*pkt).size };
if buf_ptr.is_null() || data_ptr.is_null() || size_raw <= 0 {
return Ok(None);
}
let len = size_raw as usize;
let buf_data = unsafe { (*buf_ptr).data };
let size = unsafe { (*buf_ptr).size };
if buf_data.is_null() {
return Err(PacketBufferError::Bounds(Bounds::new(0, len, size)));
}
let offset = (data_ptr as usize).wrapping_sub(buf_data as usize);
match offset.checked_add(len) {
Some(end) if end <= size => {}
_ => {
return Err(PacketBufferError::Bounds(Bounds::new(offset, len, size)));
}
}
unsafe { FfmpegBuffer::from_ref_view(buf_ptr, offset, len) }
.map(Some)
.ok_or(PacketBufferError::Refcount(Refcount::new(len)))
}
impl Clone for FfmpegBuffer {
fn clone(&self) -> Self {
self
.try_clone()
.expect("FfmpegBuffer::clone: av_buffer_ref returned null (OOM)")
}
}
impl Drop for FfmpegBuffer {
fn drop(&mut self) {
unsafe { av_buffer_unref(&mut self.inner) };
}
}
impl AsRef<[u8]> for FfmpegBuffer {
#[inline]
fn as_ref(&self) -> &[u8] {
unsafe {
let data = (*self.inner).data as *const u8;
if data.is_null() || self.len == 0 {
return &[];
}
slice::from_raw_parts(data.add(self.offset), self.len)
}
}
}
impl fmt::Debug for FfmpegBuffer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FfmpegBuffer")
.field("len", &self.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use ffmpeg_next::ffi::av_buffer_alloc;
fn make_buffer(size: usize, fill: u8) -> FfmpegBuffer {
let raw = unsafe { av_buffer_alloc(size as _) };
assert!(!raw.is_null(), "av_buffer_alloc failed");
unsafe {
let data = (*raw).data;
core::ptr::write_bytes(data, fill, size);
}
unsafe { FfmpegBuffer::take(raw) }.expect("non-null take")
}
#[test]
fn null_take_returns_none() {
assert!(unsafe { FfmpegBuffer::take(core::ptr::null_mut()) }.is_none());
}
#[test]
fn null_from_ref_returns_none() {
assert!(unsafe { FfmpegBuffer::from_ref(core::ptr::null_mut()) }.is_none());
}
#[test]
fn allocated_buffer_round_trips_bytes() {
let buf = make_buffer(16, 0xAB);
assert_eq!(buf.len(), 16);
assert!(!buf.is_empty());
let slice = buf.as_ref();
assert_eq!(slice.len(), 16);
assert!(slice.iter().all(|&b| b == 0xAB));
}
#[test]
fn clone_bumps_refcount_and_keeps_data_alive() {
let original = make_buffer(8, 0x5A);
let cloned = original.clone();
assert_eq!(original.as_ref(), cloned.as_ref());
assert_eq!(original.as_ptr(), cloned.as_ptr());
drop(original);
assert_eq!(cloned.len(), 8);
assert!(cloned.as_ref().iter().all(|&b| b == 0x5A));
}
#[test]
fn debug_shows_length() {
let buf = make_buffer(42, 0);
let s = format!("{buf:?}");
assert!(s.contains("len: 42"), "got {s}");
}
#[test]
fn from_ref_view_carves_out_subregion() {
let raw = unsafe { av_buffer_alloc(24) };
assert!(!raw.is_null());
unsafe {
let data = (*raw).data;
core::ptr::write_bytes(data, 0xAA, 8);
core::ptr::write_bytes(data.add(8), 0xBB, 8);
core::ptr::write_bytes(data.add(16), 0xCC, 8);
}
let view_a = unsafe { FfmpegBuffer::from_ref_view(raw, 0, 8) }.expect("view_a");
let view_b = unsafe { FfmpegBuffer::from_ref_view(raw, 8, 8) }.expect("view_b");
let view_c = unsafe { FfmpegBuffer::from_ref_view(raw, 16, 8) }.expect("view_c");
assert!(view_a.as_ref().iter().all(|&b| b == 0xAA));
assert!(view_b.as_ref().iter().all(|&b| b == 0xBB));
assert!(view_c.as_ref().iter().all(|&b| b == 0xCC));
assert_eq!(view_a.offset(), 0);
assert_eq!(view_b.offset(), 8);
assert_eq!(view_c.offset(), 16);
assert_eq!(view_a.len(), 8);
unsafe { av_buffer_unref(&mut { raw }) };
let _ = (view_a, view_b, view_c);
}
#[test]
fn from_ref_view_rejects_out_of_bounds() {
let raw = unsafe { av_buffer_alloc(16) };
assert!(!raw.is_null());
assert!(unsafe { FfmpegBuffer::from_ref_view(raw, 10, 8) }.is_none());
assert!(unsafe { FfmpegBuffer::from_ref_view(raw, usize::MAX, 1) }.is_none());
unsafe { av_buffer_unref(&mut { raw }) };
}
#[test]
fn empty_buffer_returns_empty_slice() {
let raw = unsafe { av_buffer_alloc(0) };
if raw.is_null() {
return;
}
let buf = unsafe { FfmpegBuffer::take(raw) }.expect("non-null take");
assert_eq!(buf.len(), 0);
assert!(buf.is_empty());
assert_eq!(buf.as_ref(), &[] as &[u8]);
}
}