//! Stream a demuxer from an arbitrary `Read + Seek` byte source via a custom //! FFmpeg `AVIOContext`. //! //! `ffmpeg-next`'s high-level API can only open inputs by filesystem path. This //! crate builds an `AVFormatContext` whose I/O is backed by a Rust reader (e.g. a //! SQLite `BlobReader`), then hands back a normal [`ffmpeg_next::format::context::Input`] //! that the rest of the codebase decodes exactly as a file-backed input. //! //! All of the `unsafe` FFI and ffmpeg ABI coupling lives here, isolated behind the //! safe [`BlobInput`] type. See `BEAM_FILE_FORMAT.md` / `STREAMING_TO_DISK_PLAN.md`. //! //! # Safety model //! - The reader is boxed and leaked to FFmpeg as the AVIO `opaque` pointer; the //! read/seek callbacks reconstitute it as `&mut`. It is never aliased — only the //! owning [`BlobInput`] (on one thread) drives it. //! - [`BlobInput`] is `Send` but **not** `Sync`: the typical reader (`BlobReader`) //! owns a `!Sync` SQLite connection. Each decoder / seek-reopen / off-thread scan //! must construct its **own** `BlobInput` from a **fresh** reader. //! - `Drop` tears down in the one correct order (Input first, then the AVIO buffer + //! context, then the reader) so there is no use-after-free, double-free, or leak. use std::io::{Read, Seek, SeekFrom}; use std::os::raw::{c_char, c_int, c_void}; use std::ptr; use ffmpeg_next::format::context::Input; use ffmpeg_sys_next as sys; /// A byte source FFmpeg can stream from. Blanket-implemented for any /// `Read + Seek + Send` (e.g. `std::io::Cursor`, a SQLite `BlobReader`). pub trait BlobSource: Read + Seek + Send {} impl BlobSource for T {} // Stable FFmpeg ABI constants (avoid depending on whether bindgen exported the // corresponding `#define`s). These are fixed across the libavformat 58–61 ABIs. const AVSEEK_SIZE: c_int = 0x10000; const AVSEEK_FORCE: c_int = 0x20000; const AVFMT_FLAG_CUSTOM_IO: c_int = 0x0080; /// Size of the AVIO read buffer handed to FFmpeg. FFmpeg may grow/replace it. const IO_BUFFER_SIZE: usize = 32 * 1024; /// A demuxer input streaming from a boxed `Read + Seek` source. /// /// Deref to [`Input`] for the usual ffmpeg-next decode API. pub struct BlobInput { // Dropped FIRST in `Drop` (avformat_close_input). `Option` so we can drop it // explicitly before freeing the AVIO resources it points at. input: Option, // The custom AVIOContext. With AVFMT_FLAG_CUSTOM_IO set, avformat_close_input // does NOT free this — we own it. Its `opaque` field is the boxed reader. avio: *mut sys::AVIOContext, } // The reader is `Send` and never aliased; we manage its lifetime manually. unsafe impl Send for BlobInput {} // Intentionally NOT `Sync`: see the module-level safety notes. impl BlobInput { /// Open a demuxer over `src`. /// /// `format_hint` is an optional container short-name / extension (e.g. `"mp4"`, /// `"mov"`, `"matroska"`) passed to `av_find_input_format` to skip probe /// ambiguity; `None` lets FFmpeg probe from the stream. pub fn open(src: Box, format_hint: Option<&str>) -> Result { ffmpeg_next::init().map_err(|e| format!("ffmpeg init failed: {e}"))?; unsafe { // 1. IO buffer, allocated with FFmpeg's allocator (freed with av_freep). let buffer = sys::av_malloc(IO_BUFFER_SIZE) as *mut u8; if buffer.is_null() { return Err("av_malloc failed for AVIO buffer".into()); } // 2. Box the reader and leak it as the opaque pointer. Double-box so the // raw pointer is thin (a `*mut dyn` would be a fat pointer). let opaque = Box::into_raw(Box::new(src)) as *mut c_void; // 3. AVIOContext over our callbacks (read-only: write_flag = 0). let avio = sys::avio_alloc_context( buffer, IO_BUFFER_SIZE as c_int, 0, opaque, Some(read_cb), None, Some(seek_cb), ); if avio.is_null() { sys::av_free(buffer as *mut c_void); drop(Box::from_raw(opaque as *mut Box)); return Err("avio_alloc_context failed".into()); } // 4. Format context wired to the custom IO. let mut fmt = sys::avformat_alloc_context(); if fmt.is_null() { destroy_io(avio); return Err("avformat_alloc_context failed".into()); } (*fmt).pb = avio; (*fmt).flags |= AVFMT_FLAG_CUSTOM_IO; // 5. Optional format hint. let hint_cstr = format_hint.and_then(|s| std::ffi::CString::new(s).ok()); let infmt: *const sys::AVInputFormat = match &hint_cstr { Some(c) => sys::av_find_input_format(c.as_ptr() as *const c_char), None => ptr::null(), }; // 6. Open. On failure avformat_open_input frees `fmt` itself (and, with // CUSTOM_IO, leaves our pb), so we still own avio+buffer+reader. let ret = sys::avformat_open_input(&mut fmt, ptr::null(), infmt, ptr::null_mut()); if ret < 0 { destroy_io(avio); return Err(format!("avformat_open_input failed (error {ret})")); } // 7. Probe streams. On failure close the now-open input, then free IO. let ret = sys::avformat_find_stream_info(fmt, ptr::null_mut()); if ret < 0 { sys::avformat_close_input(&mut fmt); destroy_io(avio); return Err(format!("avformat_find_stream_info failed (error {ret})")); } // 8. Hand ownership of `fmt` to ffmpeg-next's Input (closes on drop). let input = Input::wrap(fmt); Ok(BlobInput { input: Some(input), avio }) } } /// The underlying demuxer input. pub fn input(&self) -> &Input { self.input.as_ref().expect("BlobInput input present until drop") } /// The underlying demuxer input, mutably (for `seek`, `packets`, …). pub fn input_mut(&mut self) -> &mut Input { self.input.as_mut().expect("BlobInput input present until drop") } } impl std::ops::Deref for BlobInput { type Target = Input; fn deref(&self) -> &Input { self.input() } } impl std::ops::DerefMut for BlobInput { fn deref_mut(&mut self) -> &mut Input { self.input_mut() } } impl Drop for BlobInput { fn drop(&mut self) { unsafe { // 1. Drop the Input first: avformat_close_input. With CUSTOM_IO this does // NOT touch self.avio or its buffer. self.input = None; // 2..4. Free the AVIO buffer + context and reclaim the boxed reader. if !self.avio.is_null() { destroy_io(self.avio); self.avio = ptr::null_mut(); } } } } /// Free a standalone custom AVIOContext: its (possibly reallocated) buffer, the /// boxed reader behind `opaque`, then the context itself — in that order. Only /// call when the owning AVFormatContext has already been closed (or never opened). unsafe fn destroy_io(avio: *mut sys::AVIOContext) { if avio.is_null() { return; } // Reclaim the reader box *before* freeing the context (we need `opaque`). let opaque = (*avio).opaque; // Free the current IO buffer (FFmpeg may have replaced the original). sys::av_freep(&mut (*avio).buffer as *mut _ as *mut c_void); // Free the AVIOContext struct (nulls the local). let mut avio = avio; sys::avio_context_free(&mut avio); // Drop the reader last — no callback can fire now. if !opaque.is_null() { drop(Box::from_raw(opaque as *mut Box)); } } /// FFmpeg read callback: fill `buf` from the Rust reader. Returns bytes read, /// `AVERROR_EOF` at end of stream, or a negative error. unsafe extern "C" fn read_cb(opaque: *mut c_void, buf: *mut u8, buf_size: c_int) -> c_int { if opaque.is_null() || buf.is_null() || buf_size <= 0 { return sys::AVERROR_EOF; } let reader = &mut *(opaque as *mut Box); let slice = std::slice::from_raw_parts_mut(buf, buf_size as usize); match reader.read(slice) { Ok(0) => sys::AVERROR_EOF, Ok(n) => n as c_int, // AVERROR(EIO) == -EIO on Unix; a negative value signals a read error. Err(_) => -(libc::EIO as c_int), } } /// FFmpeg seek callback. Handles `SEEK_SET/CUR/END` and `AVSEEK_SIZE` (report total /// length). Returns the new position, the size, or `-1` on error. unsafe extern "C" fn seek_cb(opaque: *mut c_void, offset: i64, whence: c_int) -> i64 { if opaque.is_null() { return -1; } let reader = &mut *(opaque as *mut Box); let whence = whence & !AVSEEK_FORCE; if whence & AVSEEK_SIZE != 0 { // Report total length WITHOUT moving the logical position. FFmpeg does not // restore the position after an AVSEEK_SIZE query, so we must: measure by // seeking to the end, then seek back. (Failing to restore corrupts reads of // containers whose index lives at the end of the file, e.g. MP4 `moov`.) let cur = match reader.stream_position() { Ok(p) => p, Err(_) => return -1, }; let size = match reader.seek(SeekFrom::End(0)) { Ok(s) => s, Err(_) => return -1, }; if reader.seek(SeekFrom::Start(cur)).is_err() { return -1; } return size as i64; } let from = match whence { libc::SEEK_SET => SeekFrom::Start(offset as u64), libc::SEEK_CUR => SeekFrom::Current(offset), libc::SEEK_END => SeekFrom::End(offset), _ => return -1, }; reader.seek(from).map(|n| n as i64).unwrap_or(-1) }