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;