Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/format/context/destructor.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use super::StreamIo;
use ffi::*;

#[derive(Copy, Clone, Debug)]
#[derive(Debug)]
pub enum Mode {
Input,
Output,
InputCustomIo(StreamIo),
OutputCustomIo(StreamIo),
}

pub struct Destructor {
Expand All @@ -21,6 +24,14 @@ impl Drop for Destructor {
fn drop(&mut self) {
unsafe {
match self.mode {
Mode::InputCustomIo(ref _io) => {
avformat_close_input(&mut self.ptr);
// Custom io will just be dropped here
}
Mode::OutputCustomIo(ref _io) => {
avformat_free_context(self.ptr);
// Custom io will just be dropped here
}
Mode::Input => avformat_close_input(&mut self.ptr),

Mode::Output => {
Expand Down
9 changes: 9 additions & 0 deletions src/format/context/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ impl Input {
ctx: Context::wrap(ptr, destructor::Mode::Input),
}
}
pub unsafe fn wrap_with_custom_io(
ptr: *mut AVFormatContext,
custom_io: format::context::StreamIo,
) -> Self {
Input {
ptr,
ctx: Context::wrap(ptr, destructor::Mode::InputCustomIo(custom_io)),
}
}

pub unsafe fn as_ptr(&self) -> *const AVFormatContext {
self.ptr as *const _
Expand Down
3 changes: 3 additions & 0 deletions src/format/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ pub use self::input::Input;
pub mod output;
pub use self::output::Output;

pub mod stream_io;
pub use self::stream_io::StreamIo;

#[doc(hidden)]
pub mod common;

Expand Down
9 changes: 9 additions & 0 deletions src/format/context/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ impl Output {
ctx: Context::wrap(ptr, destructor::Mode::Output),
}
}
pub unsafe fn wrap_with_custom_io(
ptr: *mut AVFormatContext,
custom_io: format::context::StreamIo,
) -> Self {
Output {
ptr,
ctx: Context::wrap(ptr, destructor::Mode::OutputCustomIo(custom_io)),
}
}

pub unsafe fn as_ptr(&self) -> *const AVFormatContext {
self.ptr as *const _
Expand Down
204 changes: 204 additions & 0 deletions src/format/context/stream_io.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
use ffi;
use std::ffi::{c_int, c_void};
use std::io::{Read, Seek, SeekFrom, Write};
use Error;

/// Default internal I/O buffer size used by the underlying `AVIOContext`.
const BUFFER_SIZE: usize = 16384;

/// A safe Rust wrapper that creates an FFmpeg [`AVIOContext`] backed by any
/// Rust `Read` / `Write` / `Seek` stream.
///
/// This type allocates and owns an `AVIOContext` whose callbacks bridge to a
/// user-provided Rust stream. The stream is boxed and stored in the `opaque`
/// field of `AVIOContext` and is automatically dropped when `StreamIo` is
/// dropped.
///
/// # What this is for
///
/// FFmpeg allows you to supply custom I/O by passing an `AVIOContext` to
/// demuxers/muxers instead of a filename/URL. `StreamIo` lets you do that
/// with ordinary Rust I/O types like `File`, `Cursor<Vec<u8>>`, network
/// streams, etc.
///
/// # Ownership & lifetime
///
/// - `StreamIo` **owns** both the C `AVIOContext` and the boxed Rust stream.
/// - Dropping `StreamIo` frees the internal buffer, the `AVIOContext`,
/// and the boxed stream in the correct order.
/// - You must ensure the `AVIOContext*` returned by [`StreamIo::as_mut_ptr`]
/// does not outlive the `StreamIo` that created it.
///
/// # Thread-safety
///
/// The underlying Rust stream is not synchronized; callbacks are invoked
/// by FFmpeg on the calling thread. Do not share the same `StreamIo`
/// across threads unless the wrapped stream itself is thread-safe and FFmpeg
/// will not call the callbacks concurrently.
///
/// # EOF
///
/// - A `Read` that returns `Ok(0)` is translated to `AVERROR_EOF`.
///
/// # Safety notes
///
/// - `as_mut_ptr` exposes a raw `*mut AVIOContext` for integration with FFmpeg C APIs.
/// You must make sure this pointer does not outlive the `StreamIo` instance.
///
/// [`AVIOContext`]: https://ffmpeg.org/doxygen/trunk/structAVIOContext.html
pub struct StreamIo {
ptr: *mut ffi::AVIOContext,
drop_opaque: fn(*mut c_void),
}
impl StreamIo {
pub fn from_read<T: Read>(stream: T) -> Result<Self, Error> {
Self::new_impl(stream, Some(read::<T>), None, None)
}
pub fn from_read_seek<T: Read + Seek>(stream: T) -> Result<Self, Error> {
Self::new_impl(stream, Some(read::<T>), None, Some(seek::<T>))
}
pub fn from_read_write_seek<T: Read + Write + Seek>(stream: T) -> Result<Self, Error> {
Self::new_impl(stream, Some(read::<T>), Some(write::<T>), Some(seek::<T>))
}
pub fn from_read_write<T: Read + Write>(stream: T) -> Result<Self, Error> {
Self::new_impl(stream, Some(read::<T>), Some(write::<T>), None)
}
pub fn from_write<T: Write>(stream: T) -> Result<Self, Error> {
Self::new_impl(stream, None, Some(write::<T>), None)
}
pub fn from_write_seek<T: Write + Seek>(stream: T) -> Result<Self, Error> {
Self::new_impl(stream, None, Some(write::<T>), Some(seek::<T>))
}

fn new_impl<T>(
stream: T,
r: Option<unsafe extern "C" fn(*mut c_void, *mut u8, c_int) -> c_int>,
w: Option<unsafe extern "C" fn(*mut c_void, WriteBufferType, c_int) -> c_int>,
s: Option<unsafe extern "C" fn(*mut c_void, i64, c_int) -> i64>,
) -> Result<Self, Error> {
let buffer = unsafe { ffi::av_malloc(BUFFER_SIZE) };
if buffer.is_null() {
return Err(Error::Other { errno: ffi::ENOMEM });
}
let stream_box_ptr = Box::into_raw(Box::new(stream)) as *mut c_void;
let ptr = unsafe {
ffi::avio_alloc_context(
buffer as *mut _,
BUFFER_SIZE as _,
w.is_some() as _,
stream_box_ptr,
r,
w,
s,
)
};
if ptr.is_null() {
unsafe {
drop(Box::from_raw(stream_box_ptr as *mut T));
}
return Err(Error::Other { errno: ffi::ENOMEM });
}

fn drop_box<T>(p: *mut c_void) {
drop(unsafe { Box::from_raw(p as *mut T) });
}
Ok(Self {
ptr,
drop_opaque: drop_box::<T>,
})
}

/// Returns a mutable raw pointer to the underlying `AVIOContext`.
///
/// # Safety
/// The returned pointer is owned by `self`. Do **not** free it or mutate its
/// `buffer`/`opaque` fields directly. It must not outlive `self`.
pub fn as_mut_ptr(&mut self) -> *mut ffi::AVIOContext {
self.ptr
}
}

impl Drop for StreamIo {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe {
let opaque = (*self.ptr).opaque;
ffi::av_freep(&raw mut (*self.ptr).buffer as *mut c_void);
ffi::avio_context_free(&mut self.ptr);
(self.drop_opaque)(opaque);
}
}
}
}

impl std::fmt::Debug for StreamIo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StreamIo").field("ptr", &self.ptr).finish()
}
}

unsafe extern "C" fn read<T: Read>(opaque: *mut c_void, buf: *mut u8, buf_size: c_int) -> c_int {
let buf = unsafe { std::slice::from_raw_parts_mut(buf, buf_size as usize) };
let stream = unsafe { &mut *(opaque as *mut T) };
match stream.read(buf) {
Ok(0) => ffi::AVERROR_EOF,
Ok(n) => n as c_int,
Err(e) => map_io_error(e),
}
}
unsafe extern "C" fn write<T: Write>(
opaque: *mut c_void,
buf: WriteBufferType,
buf_size: c_int,
) -> c_int {
let buf = unsafe { std::slice::from_raw_parts(buf, buf_size as usize) };
let stream = unsafe { &mut *(opaque as *mut T) };
match stream.write(buf) {
Ok(n) => n as c_int,
Err(e) => map_io_error(e),
}
}
unsafe extern "C" fn seek<T: Seek>(opaque: *mut c_void, offset: i64, whence: c_int) -> i64 {
let stream = unsafe { &mut *(opaque as *mut T) };

if whence == ffi::AVSEEK_SIZE {
// Return stream size
match stream.stream_position().and_then(|cur| {
let end = stream.seek(SeekFrom::End(0))?;
if cur != end {
stream.seek(SeekFrom::Start(cur))?;
}
Ok(end)
}) {
Ok(sz) => return sz as i64,
Err(_) => return ffi::AVERROR(ffi::ENOSYS) as i64,
}
}

let pos = match whence {
0 => SeekFrom::Start(offset as u64),
1 => SeekFrom::Current(offset),
2 => SeekFrom::End(offset),
_ => return ffi::AVERROR(ffi::EINVAL) as i64,
};
match stream.seek(pos) {
Ok(pos) => pos as i64,
Err(_) => ffi::AVERROR(ffi::EIO) as i64,
}
}

fn map_io_error(e: std::io::Error) -> i32 {
use std::io::ErrorKind::*;
match e.kind() {
UnexpectedEof => ffi::AVERROR_EOF,
Interrupted => ffi::AVERROR(ffi::EINTR),
WouldBlock | TimedOut => ffi::AVERROR(ffi::EAGAIN),
_ => ffi::AVERROR(ffi::EIO),
}
}

#[cfg(not(feature = "ffmpeg_7_0"))]
type WriteBufferType = *mut u8;

#[cfg(feature = "ffmpeg_7_0")]
type WriteBufferType = *const u8;
71 changes: 71 additions & 0 deletions src/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,46 @@ where
}
}

/// Opens an input file using a custom I/O stream.
/// Create `format::context::StreamIo` first, then pass to this function.
///
/// You can optionally include a filename to help with format detection,
/// and a dictionary of options to configure the format context.
pub fn input_from_stream(
mut custom_io: context::StreamIo,
filename: Option<&str>,
options: Option<Dictionary>,
) -> Result<context::Input, Error> {
unsafe {
let mut ps = avformat_alloc_context();
(*ps).pb = custom_io.as_mut_ptr();

let filename = filename.map(|f| CString::new(f).unwrap());
let filename_ptr = filename.as_ref().map_or(ptr::null(), |f| f.as_ptr());

let result = if let Some(opts) = options {
let mut opts = opts.disown();
let res = avformat_open_input(&mut ps, filename_ptr, ptr::null_mut(), &mut opts);
Dictionary::own(opts);
res
} else {
avformat_open_input(&mut ps, filename_ptr, ptr::null_mut(), ptr::null_mut())
};

match result {
0 => match avformat_find_stream_info(ps, ptr::null_mut()) {
r if r >= 0 => Ok(context::Input::wrap_with_custom_io(ps, custom_io)),
e => {
avformat_close_input(&mut ps);
Err(Error::from(e))
}
},

e => Err(Error::from(e)),
}
}
}

pub fn output<P: AsRef<Path> + ?Sized>(path: &P) -> Result<context::Output, Error> {
unsafe {
let mut ps = ptr::null_mut();
Expand Down Expand Up @@ -330,3 +370,34 @@ pub fn output_as_with<P: AsRef<Path> + ?Sized>(
}
}
}

/// Creates the output context where the result is written to the provided Stream.
/// Create a writable `format::context::StreamIo` first, then pass to this function.
///
/// You can optionally include a filename to infer the output format from that,
/// or specify the format explicitly.
pub fn output_to_stream(
mut custom_io: context::StreamIo,
filename: Option<&str>,
format: Option<&str>,
) -> Result<context::Output, Error> {
unsafe {
let mut ps = ptr::null_mut();

let filename = filename.map(|f| CString::new(f).unwrap());
let filename_ptr = filename.as_ref().map_or(ptr::null(), |f| f.as_ptr());

let format = format.map(|f| CString::new(f).unwrap());
let format_ptr = format.as_ref().map_or(ptr::null(), |f| f.as_ptr());

match avformat_alloc_output_context2(&mut ps, ptr::null_mut(), format_ptr, filename_ptr) {
0 => {
(*ps).pb = custom_io.as_mut_ptr();

Ok(context::Output::wrap_with_custom_io(ps, custom_io))
}

e => Err(Error::from(e)),
}
}
}
Loading