Skip to main content

ffmpeg_next/format/context/
stream_io.rs

1use crate::Error;
2use crate::ffi;
3use libc;
4use std::any::TypeId;
5use std::convert::TryFrom;
6use std::ffi::{c_int, c_void};
7use std::io::{Read, Seek, SeekFrom, Write};
8use std::mem::ManuallyDrop;
9
10/// Default `AVIOContext` buffer size, matching libavformat's own default.
11const DEFAULT_BUFFER_SIZE: usize = 32768;
12
13/// An FFmpeg [`AVIOContext`] backed by a Rust `Read`/`Write`/`Seek` stream,
14/// for custom I/O via `format::input_from_stream` / `format::output_to_stream`.
15///
16/// `StreamIo` owns both the `AVIOContext` and the boxed stream; dropping it
17/// frees both. The stream must be `Send + 'static` (the callbacks may run on
18/// whatever thread is driving the context), but not `Sync`: callbacks never
19/// run concurrently.
20///
21/// A context is unidirectional: [`StreamIo::from_read`] /
22/// [`StreamIo::from_read_seek`] create read (demuxing) contexts,
23/// [`StreamIo::from_write`] / [`StreamIo::from_write_seek`] write (muxing)
24/// contexts; a mismatch is rejected with `EINVAL`.
25///
26/// I/O is buffered internally (32 KiB by default; tune it with the
27/// `*_with_capacity` constructors). The stream must be *blocking*: FFmpeg has
28/// no retry layer for custom I/O, so the first `WouldBlock`/`TimedOut` error
29/// poisons the context. `Interrupted` is retried internally like FFmpeg's own
30/// protocol layer retries EINTR. When the owning format context's
31/// `AVIOInterruptCB` (installed by `input_from_stream_with_interrupt`) is
32/// present, the callbacks poll it at the top of every attempt and return
33/// `AVERROR_EXIT` once it reports an abort. That poll is what lets a cancel
34/// abort even a stream that keeps returning data, and
35/// lets a LEVEL-triggered cancel (a token the stream keeps answering
36/// `Interrupted` for until the caller re-arms it) abort promptly instead of
37/// spinning. `Ok(0)` from `read` is reported as EOF.
38///
39/// Dropping a writable `StreamIo` flushes buffered data and the stream itself, discarding
40/// errors (like `std::io::BufWriter`). That keep-alive is shared with any
41/// `codec::Parameters`/`Context` derived from a stream, so the drop
42/// and its flush run on whichever thread releases the last of those owners.
43/// For well-formed output you must still call `write_trailer` first. Use
44/// [`StreamIo::into_inner`] to get the stream back.
45///
46/// [`AVIOContext`]: https://ffmpeg.org/doxygen/trunk/structAVIOContext.html
47pub struct StreamIo {
48    ptr: *mut ffi::AVIOContext,
49    drop_opaque: fn(*mut c_void),
50    flush_opaque: Option<fn(*mut c_void)>,
51    set_interrupt_opaque: fn(*mut c_void, ffi::AVIOInterruptCB),
52    stream_type: TypeId,
53}
54
55// SAFETY: every constructor requires the wrapped stream to be `Send`, the
56// `AVIOContext` and its buffer are heap allocations not tied to any thread,
57// and the stream is only ever accessed through `&mut self` / the callbacks
58// (which FFmpeg invokes from the single thread driving the I/O, never
59// concurrently — so `Send` without `Sync` is exactly right).
60//
61// This impl also backs `Send + Sync` on the `Destructor` that embeds a
62// `StreamIo` via `destructor::Mode`. A format context's keep-alive is an
63// `Arc<Destructor>`, cloned into every stream-derived `codec::{Context,
64// Parameters}`; all of those are `Send`, so the last owner to drop — and
65// hence `StreamIo::drop`, which flushes and drops the wrapped stream — may
66// run on any thread. That is sound precisely because the stream is `Send`.
67// `StreamIo` is intentionally not `Sync` and need not be: `Destructor`
68// exposes no `&`-access to the embedded `StreamIo`, so `Destructor: Sync`
69// (sharing `&Destructor`) can never reach it.
70unsafe impl Send for StreamIo {}
71
72/// The boxed `opaque` behind every `StreamIo` callback: the wrapped stream
73/// plus a copy of the owning format context's `AVIOInterruptCB` and the
74/// `read` callback's staging buffer.
75struct Opaque<T> {
76    interrupt: ffi::AVIOInterruptCB,
77    /// Staging buffer for the `read` callback. The stream must never read
78    /// straight into FFmpeg's `buf`: `buf` may be uninitialized (a safe
79    /// `Read` impl is allowed to read from its slice), and it is often
80    /// FFmpeg's live buffered window, which a failed read must leave intact -
81    /// staging through `scratch` touches `buf` only on success. Grown on demand,
82    /// zero-filled on growth; stale leftovers on reuse are sound.
83    scratch: Vec<u8>,
84    stream: T,
85}
86
87unsafe fn check_interrupt(cb: &ffi::AVIOInterruptCB) -> bool {
88    match cb.callback {
89        Some(f) => unsafe { f(cb.opaque) != 0 },
90        None => false,
91    }
92}
93
94impl StreamIo {
95    pub fn from_read<T: Read + Send + 'static>(stream: T) -> Result<Self, Error> {
96        Self::from_read_with_capacity(stream, DEFAULT_BUFFER_SIZE)
97    }
98    pub fn from_read_seek<T: Read + Seek + Send + 'static>(stream: T) -> Result<Self, Error> {
99        Self::from_read_seek_with_capacity(stream, DEFAULT_BUFFER_SIZE)
100    }
101    pub fn from_write<T: Write + Send + 'static>(stream: T) -> Result<Self, Error> {
102        Self::from_write_with_capacity(stream, DEFAULT_BUFFER_SIZE)
103    }
104    pub fn from_write_seek<T: Write + Seek + Send + 'static>(stream: T) -> Result<Self, Error> {
105        Self::from_write_seek_with_capacity(stream, DEFAULT_BUFFER_SIZE)
106    }
107
108    /// Like [`StreamIo::from_read`], with an explicit buffer size in bytes.
109    /// Fails with `EINVAL` if `capacity` is zero or exceeds `c_int::MAX`.
110    pub fn from_read_with_capacity<T: Read + Send + 'static>(
111        stream: T,
112        capacity: usize,
113    ) -> Result<Self, Error> {
114        Self::new_impl(stream, capacity, Some(read::<T>), None, None, None)
115    }
116    /// Like [`StreamIo::from_read_seek`], with an explicit buffer size in bytes.
117    /// Fails with `EINVAL` if `capacity` is zero or exceeds `c_int::MAX`.
118    pub fn from_read_seek_with_capacity<T: Read + Seek + Send + 'static>(
119        stream: T,
120        capacity: usize,
121    ) -> Result<Self, Error> {
122        Self::new_impl(
123            stream,
124            capacity,
125            Some(read::<T>),
126            None,
127            Some(seek::<T>),
128            None,
129        )
130    }
131    /// Like [`StreamIo::from_write`], with an explicit buffer size in bytes.
132    /// Fails with `EINVAL` if `capacity` is zero or exceeds `c_int::MAX`.
133    pub fn from_write_with_capacity<T: Write + Send + 'static>(
134        stream: T,
135        capacity: usize,
136    ) -> Result<Self, Error> {
137        Self::new_impl(
138            stream,
139            capacity,
140            None,
141            Some(write::<T>),
142            None,
143            Some(flush_stream::<T>),
144        )
145    }
146    /// Like [`StreamIo::from_write_seek`], with an explicit buffer size in bytes.
147    /// Fails with `EINVAL` if `capacity` is zero or exceeds `c_int::MAX`.
148    pub fn from_write_seek_with_capacity<T: Write + Seek + Send + 'static>(
149        stream: T,
150        capacity: usize,
151    ) -> Result<Self, Error> {
152        Self::new_impl(
153            stream,
154            capacity,
155            None,
156            Some(write::<T>),
157            Some(seek::<T>),
158            Some(flush_stream::<T>),
159        )
160    }
161
162    /// Returns `true` if this is a write (muxing) context.
163    pub fn is_writable(&self) -> bool {
164        unsafe { (*self.ptr).write_flag != 0 }
165    }
166
167    fn new_impl<T: Send + 'static>(
168        stream: T,
169        capacity: usize,
170        r: Option<unsafe extern "C" fn(*mut c_void, *mut u8, c_int) -> c_int>,
171        w: Option<unsafe extern "C" fn(*mut c_void, WriteBufferType, c_int) -> c_int>,
172        s: Option<unsafe extern "C" fn(*mut c_void, i64, c_int) -> i64>,
173        flush: Option<fn(*mut c_void)>,
174    ) -> Result<Self, Error> {
175        // `AVIOContext::buffer_size` is a C `int`, and a zero-size buffer
176        // would make `fill_buffer` / `flush_buffer` spin without progress.
177        if capacity == 0 || capacity > c_int::MAX as usize {
178            return Err(Error::Other { errno: ffi::EINVAL });
179        }
180        // The Rust stream never sees this buffer (the `read` callback stages
181        // through `Opaque::scratch`), but zero-init is cheap one-time
182        // insurance against FFmpeg code paths that inspect it before the
183        // first fill.
184        let buffer = unsafe { ffi::av_mallocz(capacity) };
185        if buffer.is_null() {
186            return Err(Error::Other { errno: ffi::ENOMEM });
187        }
188        let stream_box_ptr = Box::into_raw(Box::new(Opaque {
189            interrupt: ffi::AVIOInterruptCB {
190                callback: None,
191                opaque: std::ptr::null_mut(),
192            },
193            scratch: Vec::new(),
194            stream,
195        })) as *mut c_void;
196        let ptr = unsafe {
197            ffi::avio_alloc_context(
198                buffer as *mut _,
199                capacity as _,
200                w.is_some() as _,
201                stream_box_ptr,
202                r,
203                w,
204                s,
205            )
206        };
207        if ptr.is_null() {
208            // `avio_alloc_context` takes ownership of `buffer` only on success.
209            unsafe {
210                ffi::av_free(buffer);
211                drop(Box::from_raw(stream_box_ptr as *mut Opaque<T>));
212            }
213            return Err(Error::Other { errno: ffi::ENOMEM });
214        }
215
216        Ok(Self {
217            ptr,
218            drop_opaque: drop_box::<Opaque<T>>,
219            flush_opaque: flush,
220            set_interrupt_opaque: set_interrupt_impl::<T>,
221            stream_type: TypeId::of::<T>(),
222        })
223    }
224
225    pub(crate) fn set_interrupt(&mut self, cb: ffi::AVIOInterruptCB) {
226        (self.set_interrupt_opaque)(unsafe { (*self.ptr).opaque }, cb);
227    }
228
229    /// Consumes the `StreamIo` and returns the wrapped stream, flushing data
230    /// still buffered in the `AVIOContext` first. The stream's own
231    /// [`Write::flush`] is *not* called, so the caller can flush and observe
232    /// errors. Fails (returning `self`) unless `T` is the exact type the
233    /// `StreamIo` was constructed with.
234    pub fn into_inner<T: 'static>(self) -> Result<T, Self> {
235        if self.stream_type != TypeId::of::<T>() {
236            return Err(self);
237        }
238        let mut this = ManuallyDrop::new(self);
239        unsafe {
240            ffi::avio_flush(this.ptr);
241            let opaque = (*this.ptr).opaque;
242            ffi::av_freep(&raw mut (*this.ptr).buffer as *mut c_void);
243            ffi::avio_context_free(&mut this.ptr);
244            Ok(Box::from_raw(opaque as *mut Opaque<T>).stream)
245        }
246    }
247
248    /// Returns a mutable raw pointer to the underlying `AVIOContext`.
249    ///
250    /// # Safety
251    /// The returned pointer is owned by `self`. Do **not** free it or mutate its
252    /// `buffer`/`opaque` fields directly. It must not outlive `self`.
253    pub fn as_mut_ptr(&mut self) -> *mut ffi::AVIOContext {
254        self.ptr
255    }
256}
257
258impl Drop for StreamIo {
259    fn drop(&mut self) {
260        if !self.ptr.is_null() {
261            unsafe {
262                let opaque = (*self.ptr).opaque;
263                if (*self.ptr).write_flag != 0 {
264                    // Salvage data still buffered in the AVIOContext, then let
265                    // the stream flush its own buffers (a user `BufWriter`
266                    // tail would otherwise only flush in its Drop, or be lost).
267                    // Errors are unreportable from a destructor and are
268                    // discarded, like `std::io::BufWriter` does.
269                    ffi::avio_flush(self.ptr);
270                    if let Some(flush) = self.flush_opaque {
271                        flush(opaque);
272                    }
273                }
274                ffi::av_freep(&raw mut (*self.ptr).buffer as *mut c_void);
275                ffi::avio_context_free(&mut self.ptr);
276                (self.drop_opaque)(opaque);
277            }
278        }
279    }
280}
281
282impl std::fmt::Debug for StreamIo {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        f.debug_struct("StreamIo").field("ptr", &self.ptr).finish()
285    }
286}
287
288unsafe extern "C" fn read<T: Read>(opaque: *mut c_void, buf: *mut u8, buf_size: c_int) -> c_int {
289    // FFmpeg never issues zero-sized reads (and `read_packet` must not
290    // return 0 — it asserts on that), but a `Read` impl would report one as
291    // `Ok(0)`, which we translate to EOF; reject instead of lying.
292    if buf_size <= 0 {
293        return ffi::AVERROR(ffi::EINVAL);
294    }
295    let buf_size = buf_size as usize;
296    let opaque = unsafe { &mut *(opaque as *mut Opaque<T>) };
297    if opaque.scratch.len() < buf_size {
298        opaque.scratch.resize(buf_size, 0);
299    }
300    loop {
301        if unsafe { check_interrupt(&opaque.interrupt) } {
302            return ffi::AVERROR_EXIT;
303        }
304        let scratch = &mut opaque.scratch[..buf_size];
305        return match opaque.stream.read(scratch) {
306            Ok(0) => ffi::AVERROR_EOF,
307            // A buggy (but safe) `Read` impl may report more bytes than the buffer
308            // holds; FFmpeg trusts the count and would advance `buf_end` past the
309            // allocation.
310            Ok(n) if n > scratch.len() => ffi::AVERROR(ffi::EIO),
311            Ok(n) => {
312                unsafe { std::ptr::copy_nonoverlapping(scratch.as_ptr(), buf, n) };
313                n as c_int
314            }
315            // Retry interrupted reads like FFmpeg's own protocol layer does;
316            // surfacing EINTR would poison the context (see `map_io_error`)
317            // even though the read can simply be reissued.
318            Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
319            Err(e) => map_io_error(e),
320        };
321    }
322}
323unsafe extern "C" fn write<T: Write>(
324    opaque: *mut c_void,
325    buf: WriteBufferType,
326    buf_size: c_int,
327) -> c_int {
328    if buf_size < 0 {
329        return ffi::AVERROR(ffi::EINVAL);
330    }
331    let buf = unsafe { std::slice::from_raw_parts(buf, buf_size as usize) };
332    let opaque = unsafe { &mut *(opaque as *mut Opaque<T>) };
333    let mut written = 0usize;
334    while written < buf.len() {
335        if unsafe { check_interrupt(&opaque.interrupt) } {
336            return ffi::AVERROR_EXIT;
337        }
338        match opaque.stream.write(&buf[written..]) {
339            Ok(0) => return map_io_error(std::io::ErrorKind::WriteZero.into()),
340            Ok(n) if n > buf.len() - written => return ffi::AVERROR(ffi::EIO),
341            Ok(n) => written += n,
342            Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
343            Err(e) => return map_io_error(e),
344        }
345    }
346    buf_size
347}
348unsafe extern "C" fn seek<T: Seek>(opaque: *mut c_void, offset: i64, whence: c_int) -> i64 {
349    let opaque = unsafe { &mut *(opaque as *mut Opaque<T>) };
350    let stream = &mut opaque.stream;
351
352    // AVSEEK_FORCE may be OR'd into `whence` ("seek by any means"); avio.h
353    // documents it as ignored by the seek code since 2010, and FFmpeg's own
354    // dispatchers mask it off before seeking (`avio_seek`, `ffurl_seek`).
355    // Honor the flag convention instead of failing the seek with EINVAL.
356    let whence = whence & !ffi::AVSEEK_FORCE;
357
358    if whence == ffi::AVSEEK_SIZE {
359        // Return the stream size. Any negative return makes `avio_size` fall
360        // back to probing with SEEK_END, which also restores the position
361        // FFmpeg expects, so a partial failure here cannot corrupt state.
362        match stream.stream_position().and_then(|cur| {
363            let end = stream.seek(SeekFrom::End(0))?;
364            if cur != end {
365                stream.seek(SeekFrom::Start(cur))?;
366            }
367            Ok(end)
368        }) {
369            Ok(sz) => return position_to_i64(sz),
370            Err(e) => return map_io_error(e) as i64,
371        }
372    }
373
374    let pos = match whence {
375        // `avio_seek` rejects negative absolute offsets before invoking the
376        // callback, so one can only arrive from a caller driving the callback
377        // directly; `as u64` would turn it into a huge forward seek.
378        0 if offset >= 0 => SeekFrom::Start(offset as u64),
379        0 => return ffi::AVERROR(ffi::EINVAL) as i64,
380        1 => SeekFrom::Current(offset),
381        2 => SeekFrom::End(offset),
382        _ => return ffi::AVERROR(ffi::EINVAL) as i64,
383    };
384    loop {
385        if unsafe { check_interrupt(&opaque.interrupt) } {
386            return ffi::AVERROR_EXIT as i64;
387        }
388        return match stream.seek(pos) {
389            Ok(pos) => position_to_i64(pos),
390            Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
391            Err(e) => map_io_error(e) as i64,
392        };
393    }
394}
395
396// `Seek` reports positions as `u64`, but the callback returns `i64` with
397// negative values reserved for AVERROR codes; a position above `i64::MAX`
398// would wrap into (or alias) an error code.
399fn position_to_i64(pos: u64) -> i64 {
400    i64::try_from(pos).unwrap_or(ffi::AVERROR(libc::EOVERFLOW) as i64)
401}
402
403// Not a C callback: invoked from `StreamIo::drop` to flush the user stream
404// after the `AVIOContext` buffer has been written out.
405fn flush_stream<T: Write>(opaque: *mut c_void) {
406    let _ = unsafe { &mut *(opaque as *mut Opaque<T>) }.stream.flush();
407}
408
409// Not a C callback: invoked from `StreamIo::drop` to free the boxed stream.
410// `opaque` must be the `Box<Opaque<T>>` created in `new_impl` (instantiated
411// there as `drop_box::<Opaque<T>>`).
412fn drop_box<T>(opaque: *mut c_void) {
413    drop(unsafe { Box::from_raw(opaque as *mut T) });
414}
415
416fn set_interrupt_impl<T>(opaque: *mut c_void, cb: ffi::AVIOInterruptCB) {
417    unsafe { (*(opaque as *mut Opaque<T>)).interrupt = cb };
418}
419
420fn map_io_error(e: std::io::Error) -> i32 {
421    use std::io::ErrorKind::*;
422    // On Unix the raw OS error *is* an errno value, which is exactly what
423    // AVERROR encodes; pass it through to preserve detail (EACCES, ENOSPC,
424    // ...). On Windows it is a Win32 error code, not an errno, so it cannot
425    // be used and we fall back to mapping the `ErrorKind`.
426    #[cfg(unix)]
427    if let Some(errno) = e.raw_os_error()
428        && errno > 0
429    {
430        return ffi::AVERROR(errno);
431    }
432    // Errors returned from the read/write callbacks are sticky: there is no
433    // retry layer above a custom AVIOContext (FFmpeg retries EINTR/EAGAIN
434    // only inside its own URL protocols), so `fill_buffer`/`writeout` latch
435    // whatever we return into `s->error` and no further I/O happens. That is
436    // why `Interrupted` is retried in the callbacks (aborting with
437    // `AVERROR_EXIT` when the format context's interrupt callback fires)
438    // instead of being mapped here - the `Interrupted` arm below stays
439    // reachable only from the `AVSEEK_SIZE` size probe, whose failure triggers
440    // `avio_size`'s SEEK_END fallback (self-healing on the next absolute seek;
441    // see the `AVSEEK_SIZE` branch in `seek`) - and why `WouldBlock`/`TimedOut`
442    // - while given their truthful codes - are fatal: see the "Blocking I/O"
443    // notes on `StreamIo`.
444    //
445    // The errno constants come from `libc`, not the generated bindings: they
446    // must agree with the `util::error` re-exports users match `Error::Other`
447    // against (and with the platform CRT the FFmpeg binary itself was built
448    // with), whereas bindgen has been observed emitting glibc values on
449    // Windows (ETIMEDOUT 110 vs the CRT's 138).
450    match e.kind() {
451        UnexpectedEof => ffi::AVERROR_EOF,
452        Interrupted => ffi::AVERROR(libc::EINTR),
453        WouldBlock => ffi::AVERROR(libc::EAGAIN),
454        TimedOut => ffi::AVERROR(libc::ETIMEDOUT),
455        Unsupported => ffi::AVERROR(libc::ENOSYS),
456        _ => ffi::AVERROR(libc::EIO),
457    }
458}
459
460#[cfg(not(feature = "ffmpeg_7_0"))]
461type WriteBufferType = *mut u8;
462
463#[cfg(feature = "ffmpeg_7_0")]
464type WriteBufferType = *const u8;