From 298db45fd19c0eed4569f73e3a85a7abe02f9baa Mon Sep 17 00:00:00 2001 From: AdrianEddy Date: Thu, 23 Oct 2025 04:10:22 +0200 Subject: [PATCH 1/3] Implement custom stream IO --- src/format/context/destructor.rs | 13 +- src/format/context/input.rs | 9 ++ src/format/context/mod.rs | 3 + src/format/context/output.rs | 9 ++ src/format/context/stream_io.rs | 198 +++++++++++++++++++++++++++++++ src/format/mod.rs | 76 ++++++++++++ 6 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 src/format/context/stream_io.rs diff --git a/src/format/context/destructor.rs b/src/format/context/destructor.rs index fa36d429..d1f75696 100644 --- a/src/format/context/destructor.rs +++ b/src/format/context/destructor.rs @@ -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 { @@ -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 => { diff --git a/src/format/context/input.rs b/src/format/context/input.rs index c73f985e..d1c54192 100644 --- a/src/format/context/input.rs +++ b/src/format/context/input.rs @@ -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 _ diff --git a/src/format/context/mod.rs b/src/format/context/mod.rs index 96dd845d..2f73eca4 100644 --- a/src/format/context/mod.rs +++ b/src/format/context/mod.rs @@ -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; diff --git a/src/format/context/output.rs b/src/format/context/output.rs index 11fafb14..0bf78a70 100644 --- a/src/format/context/output.rs +++ b/src/format/context/output.rs @@ -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 _ diff --git a/src/format/context/stream_io.rs b/src/format/context/stream_io.rs new file mode 100644 index 00000000..26c20475 --- /dev/null +++ b/src/format/context/stream_io.rs @@ -0,0 +1,198 @@ +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>`, 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(stream: T) -> Result { + Self::new_impl(stream, Some(read::), None, None) + } + pub fn from_read_seek(stream: T) -> Result { + Self::new_impl(stream, Some(read::), None, Some(seek::)) + } + pub fn from_read_write_seek(stream: T) -> Result { + Self::new_impl(stream, Some(read::), Some(write::), Some(seek::)) + } + pub fn from_read_write(stream: T) -> Result { + Self::new_impl(stream, Some(read::), Some(write::), None) + } + pub fn from_write(stream: T) -> Result { + Self::new_impl(stream, None, Some(write::), None) + } + pub fn from_write_seek(stream: T) -> Result { + Self::new_impl(stream, None, Some(write::), Some(seek::)) + } + + fn new_impl( + stream: T, + r: Option c_int>, + w: Option c_int>, + s: Option i64>, + ) -> Result { + 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(p: *mut c_void) { + drop(unsafe { Box::from_raw(p as *mut T) }); + } + Ok(Self { + ptr, + drop_opaque: drop_box::, + }) + } + + /// 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(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( + opaque: *mut c_void, + buf: *const u8, + 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(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), + } +} diff --git a/src/format/mod.rs b/src/format/mod.rs index df389827..4aa211b2 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -219,6 +219,51 @@ 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, +) -> Result { + 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(std::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, std::ptr::null(), &mut opts); + Dictionary::own(opts); + res + } else { + avformat_open_input( + &mut ps, + filename_ptr, + std::ptr::null(), + std::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 + ?Sized>(path: &P) -> Result { unsafe { let mut ps = ptr::null_mut(); @@ -330,3 +375,34 @@ pub fn output_as_with + ?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 { + unsafe { + let mut ps = ptr::null_mut(); + + let filename = filename.map(|f| CString::new(f).unwrap()); + let filename_ptr = filename.as_ref().map_or(std::ptr::null(), |f| f.as_ptr()); + + let format = format.map(|f| CString::new(f).unwrap()); + let format_ptr = format.as_ref().map_or(std::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)), + } + } +} From 61956add520f7344a487b9f19cb8df6f2f2ca02a Mon Sep 17 00:00:00 2001 From: AdrianEddy Date: Thu, 23 Oct 2025 04:24:54 +0200 Subject: [PATCH 2/3] Fix build with older ffmpeg --- src/format/context/stream_io.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/format/context/stream_io.rs b/src/format/context/stream_io.rs index 26c20475..b5ba5a1f 100644 --- a/src/format/context/stream_io.rs +++ b/src/format/context/stream_io.rs @@ -73,7 +73,7 @@ impl StreamIo { fn new_impl( stream: T, r: Option c_int>, - w: Option c_int>, + w: Option c_int>, s: Option i64>, ) -> Result { let buffer = unsafe { ffi::av_malloc(BUFFER_SIZE) }; @@ -148,7 +148,7 @@ unsafe extern "C" fn read(opaque: *mut c_void, buf: *mut u8, buf_size: } unsafe extern "C" fn write( opaque: *mut c_void, - buf: *const u8, + buf: WriteBufferType, buf_size: c_int, ) -> c_int { let buf = unsafe { std::slice::from_raw_parts(buf, buf_size as usize) }; @@ -196,3 +196,9 @@ fn map_io_error(e: std::io::Error) -> i32 { _ => ffi::AVERROR(ffi::EIO), } } + +#[cfg(not(feature = "ffmpeg_7_0"))] +type WriteBufferType = *mut u8; + +#[cfg(feature = "ffmpeg_7_0")] +type WriteBufferType = *const u8; From ca9b4bc207f0609f74339ed63a41a1794557ee69 Mon Sep 17 00:00:00 2001 From: AdrianEddy Date: Thu, 23 Oct 2025 04:28:24 +0200 Subject: [PATCH 3/3] Fix build with older ffmpeg --- src/format/mod.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/format/mod.rs b/src/format/mod.rs index 4aa211b2..c5abafcc 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -234,20 +234,15 @@ pub fn input_from_stream( (*ps).pb = custom_io.as_mut_ptr(); let filename = filename.map(|f| CString::new(f).unwrap()); - let filename_ptr = filename.as_ref().map_or(std::ptr::null(), |f| f.as_ptr()); + 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, std::ptr::null(), &mut opts); + 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, - std::ptr::null(), - std::ptr::null_mut(), - ) + avformat_open_input(&mut ps, filename_ptr, ptr::null_mut(), ptr::null_mut()) }; match result { @@ -390,10 +385,10 @@ pub fn output_to_stream( let mut ps = ptr::null_mut(); let filename = filename.map(|f| CString::new(f).unwrap()); - let filename_ptr = filename.as_ref().map_or(std::ptr::null(), |f| f.as_ptr()); + 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(std::ptr::null(), |f| f.as_ptr()); + 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 => {