use libc::c_void;
use crate::error::{FfmpegError, FfmpegErrorCode};
use crate::ffi::*;
use crate::smart_object::SmartPtr;
use crate::{AVIOFlag, AVSeekWhence};
const AVERROR_IO: i32 = AVERROR(EIO);
pub(crate) unsafe extern "C" fn read_packet<T: std::io::Read>(
opaque: *mut libc::c_void,
buf: *mut u8,
buf_size: i32,
) -> i32 {
let this = unsafe { &mut *(opaque as *mut T) };
let buffer = unsafe { std::slice::from_raw_parts_mut(buf, buf_size as usize) };
let ret = this.read(buffer).map(|n| n as i32).unwrap_or(AVERROR_IO);
if ret == 0 {
return AVERROR_EOF;
}
ret
}
pub(crate) unsafe extern "C" fn write_packet<T: std::io::Write>(
opaque: *mut libc::c_void,
buf: *const u8,
buf_size: i32,
) -> i32 {
let this = unsafe { &mut *(opaque as *mut T) };
let buffer = unsafe { std::slice::from_raw_parts(buf, buf_size as usize) };
this.write(buffer).map(|n| n as i32).unwrap_or(AVERROR_IO)
}
pub(crate) unsafe extern "C" fn seek<T: std::io::Seek>(opaque: *mut libc::c_void, offset: i64, whence: i32) -> i64 {
let this = unsafe { &mut *(opaque as *mut T) };
let mut whence = AVSeekWhence(whence);
let seek_size = whence & AVSeekWhence::Size != 0;
if seek_size {
whence &= !AVSeekWhence::Size;
}
let seek_force = whence & AVSeekWhence::Force != 0;
if seek_force {
whence &= !AVSeekWhence::Force;
}
if seek_size {
let Ok(pos) = this.stream_position() else {
return AVERROR_IO as i64;
};
let Ok(end) = this.seek(std::io::SeekFrom::End(0)) else {
return AVERROR_IO as i64;
};
if end != pos {
let Ok(_) = this.seek(std::io::SeekFrom::Start(pos)) else {
return AVERROR_IO as i64;
};
}
return end as i64;
}
let whence = match whence {
AVSeekWhence::Start => std::io::SeekFrom::Start(offset as u64),
AVSeekWhence::Current => std::io::SeekFrom::Current(offset),
AVSeekWhence::End => std::io::SeekFrom::End(offset),
_ => return -1,
};
match this.seek(whence) {
Ok(pos) => pos as i64,
Err(_) => AVERROR_IO as i64,
}
}
pub(crate) struct Inner<T: Send + Sync> {
pub(crate) data: Option<Box<T>>,
pub(crate) context: SmartPtr<AVFormatContext>,
_io: SmartPtr<AVIOContext>,
}
pub(crate) struct InnerOptions {
pub(crate) buffer_size: usize,
pub(crate) read_fn: Option<unsafe extern "C" fn(*mut c_void, *mut u8, i32) -> i32>,
pub(crate) write_fn: Option<unsafe extern "C" fn(*mut c_void, *const u8, i32) -> i32>,
pub(crate) seek_fn: Option<unsafe extern "C" fn(*mut c_void, i64, i32) -> i64>,
pub(crate) output_format: *const AVOutputFormat,
}
impl Default for InnerOptions {
fn default() -> Self {
Self {
buffer_size: 4096,
read_fn: None,
write_fn: None,
seek_fn: None,
output_format: std::ptr::null(),
}
}
}
impl<T: Send + Sync> Inner<T> {
pub(crate) fn new(data: T, options: InnerOptions) -> Result<Self, FfmpegError> {
let buffer = unsafe { av_malloc(options.buffer_size) };
fn buffer_destructor(ptr: &mut *mut c_void) {
unsafe { av_free(*ptr) };
*ptr = std::ptr::null_mut();
}
let buffer = unsafe { SmartPtr::wrap_non_null(buffer, buffer_destructor) }.ok_or(FfmpegError::Alloc)?;
let mut data = Box::new(data);
let destructor = |ptr: &mut *mut AVIOContext| {
let mut_ref = unsafe { ptr.as_mut() };
if let Some(ptr) = mut_ref {
buffer_destructor(&mut (ptr.buffer as *mut c_void));
}
unsafe { avio_context_free(ptr) };
*ptr = std::ptr::null_mut();
};
let io = unsafe {
avio_alloc_context(
buffer.as_ptr() as *mut u8,
options.buffer_size as i32,
if options.write_fn.is_some() { 1 } else { 0 },
data.as_mut() as *mut _ as *mut c_void,
options.read_fn,
options.write_fn,
options.seek_fn,
)
};
let mut io = unsafe { SmartPtr::wrap_non_null(io, destructor) }.ok_or(FfmpegError::Alloc)?;
buffer.into_inner();
let mut context = if options.write_fn.is_some() {
let mut context = SmartPtr::null(|mut_ref| {
let ptr = *mut_ref;
unsafe { avformat_free_context(ptr) };
*mut_ref = std::ptr::null_mut();
});
FfmpegErrorCode(unsafe {
avformat_alloc_output_context2(
context.as_mut(),
options.output_format,
std::ptr::null(),
std::ptr::null_mut(),
)
})
.result()?;
if context.as_ptr().is_null() {
return Err(FfmpegError::Alloc);
}
context
} else {
let context = unsafe { avformat_alloc_context() };
let destructor = |mut_ref: &mut *mut AVFormatContext| {
let ptr = *mut_ref;
unsafe { avformat_free_context(ptr) };
*mut_ref = std::ptr::null_mut();
};
unsafe { SmartPtr::wrap_non_null(context, destructor) }.ok_or(FfmpegError::Alloc)?
};
context.as_deref_mut().expect("Context is null").pb = io.as_mut_ptr();
Ok(Self {
data: Some(data),
context,
_io: io,
})
}
}
impl Inner<()> {
pub(crate) unsafe fn empty() -> Self {
Self {
data: Some(Box::new(())),
context: SmartPtr::null(|mut_ref| {
let ptr = *mut_ref;
unsafe { avformat_free_context(ptr) };
*mut_ref = std::ptr::null_mut();
}),
_io: SmartPtr::null(|_| {}),
}
}
pub(crate) fn open_output(path: &str) -> Result<Self, FfmpegError> {
let path = std::ffi::CString::new(path).expect("Failed to convert path to CString");
let mut this = unsafe { Self::empty() };
FfmpegErrorCode(unsafe {
avformat_alloc_output_context2(this.context.as_mut(), std::ptr::null(), std::ptr::null(), path.as_ptr())
})
.result()?;
if this.context.as_ptr().is_null() {
return Err(FfmpegError::Alloc);
}
FfmpegErrorCode(unsafe {
avio_open(
&mut this.context.as_deref_mut_except().pb,
path.as_ptr(),
AVIOFlag::Write.into(),
)
})
.result()?;
this.context.set_destructor(|mut_ref| {
let ptr = *mut_ref;
let mut_ptr_ref = unsafe { ptr.as_mut() };
if let Some(mut_ptr_ref) = mut_ptr_ref {
unsafe { avio_closep(&mut mut_ptr_ref.pb) };
}
unsafe { avformat_free_context(ptr) };
*mut_ref = std::ptr::null_mut();
});
Ok(this)
}
}
#[cfg(test)]
#[cfg_attr(all(test, coverage_nightly), coverage(off))]
mod tests {
use std::ffi::CString;
use std::io::Cursor;
use std::sync::Once;
use std::sync::atomic::{AtomicUsize, Ordering};
use libc::c_void;
use tempfile::Builder;
use crate::AVSeekWhence;
use crate::error::FfmpegError;
use crate::ffi::av_guess_format;
use crate::io::internal::{AVERROR_EOF, Inner, InnerOptions, read_packet, seek, write_packet};
#[test]
fn test_read_packet_eof() {
let mut data: Cursor<Vec<u8>> = Cursor::new(vec![]);
let mut buf = [0u8; 10];
unsafe {
let result =
read_packet::<Cursor<Vec<u8>>>((&raw mut data) as *mut libc::c_void, buf.as_mut_ptr(), buf.len() as i32);
assert_eq!(result, AVERROR_EOF);
}
}
#[test]
fn test_write_packet_success() {
let mut data = Cursor::new(vec![0u8; 10]);
let buf = [1u8, 2, 3, 4, 5];
unsafe {
let result = write_packet::<Cursor<Vec<u8>>>((&raw mut data) as *mut c_void, buf.as_ptr(), buf.len() as i32);
assert_eq!(result, buf.len() as i32);
let written_data = data.get_ref();
assert_eq!(&written_data[..buf.len()], &buf);
}
}
#[test]
fn test_seek_force() {
let mut cursor = Cursor::new(vec![0u8; 100]);
let opaque = &raw mut cursor as *mut c_void;
assert_eq!(cursor.position(), 0);
let offset = 10;
let mut whence = AVSeekWhence::Current | AVSeekWhence::Force;
let result = unsafe { seek::<Cursor<Vec<u8>>>(opaque, offset, whence.into()) };
assert_eq!(result, { offset });
whence &= !AVSeekWhence::Force;
assert_eq!(whence, AVSeekWhence::Current);
assert_eq!(cursor.position(), offset as u64);
}
#[test]
fn test_seek_seek_end() {
let mut cursor = Cursor::new(vec![0u8; 100]);
let opaque = &raw mut cursor as *mut libc::c_void;
let offset = -10;
let result = unsafe { seek::<Cursor<Vec<u8>>>(opaque, offset, AVSeekWhence::End.into()) };
assert_eq!(result, 90);
assert_eq!(cursor.position(), 90);
}
#[test]
fn test_seek_invalid_whence() {
let mut cursor = Cursor::new(vec![0u8; 100]);
let opaque = &raw mut cursor as *mut libc::c_void;
let result = unsafe { seek::<Cursor<Vec<u8>>>(opaque, 0, 999) };
assert_eq!(result, -1);
assert_eq!(cursor.position(), 0);
}
#[test]
fn test_avformat_alloc_output_context2_error() {
static BUF_SIZE_TRACKER: AtomicUsize = AtomicUsize::new(0);
static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
static INIT: Once = Once::new();
INIT.call_once(|| {
BUF_SIZE_TRACKER.store(0, Ordering::SeqCst);
CALL_COUNT.store(0, Ordering::SeqCst);
});
unsafe extern "C" fn dummy_write_fn(_opaque: *mut libc::c_void, _buf: *const u8, _buf_size: i32) -> i32 {
CALL_COUNT.fetch_add(1, Ordering::SeqCst);
BUF_SIZE_TRACKER.store(_buf_size as usize, Ordering::SeqCst);
0 }
let invalid_format = CString::new("invalid_format").expect("Failed to create CString");
let options = InnerOptions {
buffer_size: 4096,
write_fn: Some(dummy_write_fn),
output_format: unsafe { av_guess_format(invalid_format.as_ptr(), std::ptr::null(), std::ptr::null()) },
..Default::default()
};
let data = ();
let result = Inner::new(data, options);
assert!(result.is_err(), "Expected an error but got Ok");
let call_count = CALL_COUNT.load(Ordering::SeqCst);
assert_eq!(call_count, 0, "Expected dummy_write_fn to not be called.");
if let Err(error) = result {
match error {
FfmpegError::Code(_) => {
eprintln!("Expected avformat_alloc_output_context2 error occurred.");
}
_ => panic!("Unexpected error variant: {error:?}"),
}
}
}
#[test]
fn test_open_output_valid_path() {
let temp_file = Builder::new()
.suffix(".mp4")
.tempfile()
.expect("Failed to create a temporary file");
let test_path = temp_file.path();
let result = Inner::open_output(test_path.to_str().unwrap());
assert!(result.is_ok(), "Expected success but got error");
}
#[test]
fn test_open_output_invalid_path() {
let test_path = "";
let result = Inner::open_output(test_path);
assert!(result.is_err(), "Expected Err, got Ok");
}
#[test]
fn test_open_output_avformat_alloc_error() {
let test_path = tempfile::tempdir().unwrap().path().join("restricted_output.mp4");
let test_path_str = test_path.to_str().unwrap();
let result = Inner::open_output(test_path_str);
if let Err(error) = &result {
eprintln!("Function returned an error: {error:?}");
}
assert!(
matches!(result, Err(FfmpegError::Code(_))),
"Expected FfmpegError::Code but received a different error."
);
}
}