Skip to main content

alpha2_ffmpeg_bridge/
container.rs

1//! Opening containers: what `FFmpeg` may open when it reads one, and how a
2//! written one is closed.
3//!
4//! rsmpeg's own constructors give neither. Its input builder allocates the
5//! format context itself, so an interrupt callback cannot be installed before
6//! `avformat_open_input` reads the file; and its output keeps the file's
7//! `AVIOContext` in a value whose `Drop` unwraps `avio_close`, which returns
8//! the error a failed write left behind, so dropping an output after a full
9//! disk panics.
10
11use std::{
12    ffi::{CStr, c_int, c_void},
13    fs::File,
14    io::Read,
15    mem::ManuallyDrop,
16    ops::{Deref, DerefMut},
17    path::Path,
18    ptr::{self, NonNull},
19    sync::{Arc, Mutex, PoisonError},
20};
21
22use rsmpeg::{
23    UnsafeDerefMut,
24    avcodec::AVPacket,
25    avformat::{AVFormatContextInput, AVFormatContextOutput, AVIOContextContainer},
26    avutil::AVDictionary,
27    error::RsmpegError,
28    ffi,
29};
30
31use crate::BridgeError;
32
33/// What an interrupt callback asks: whether the call it interrupts should
34/// stop now.
35type InterruptCheck = Arc<dyn Fn() -> bool + Send + Sync>;
36
37/// The state an input's `AVIOInterruptCB` reads. `FFmpeg` holds a raw pointer
38/// to it, so it lives in an `Arc` the input keeps for as long as its context.
39#[derive(Default)]
40struct InputInterrupt {
41    check: Mutex<Option<InterruptCheck>>,
42}
43
44impl InputInterrupt {
45    fn requested(&self) -> bool {
46        let check = self
47            .check
48            .lock()
49            .unwrap_or_else(PoisonError::into_inner)
50            .clone();
51        check.is_some_and(|check| check())
52    }
53}
54
55/// `AVIOInterruptCB::callback`: nonzero stops the blocking call in progress
56/// with `AVERROR_EXIT`.
57unsafe extern "C" fn interrupt_requested(opaque: *mut c_void) -> c_int {
58    // SAFETY: `opaque` is the `InputInterrupt` of the `DemuxInput` that owns this format context
59    // and the I/O it opened; the input keeps it alive until after the context is closed, and only
60    // shared access is taken.
61    let interrupt = unsafe { &*opaque.cast::<InputInterrupt>() };
62    c_int::from(interrupt.requested())
63}
64
65/// A container opened for reading under an input policy: only the demuxers
66/// it names, only the `file` protocol, and an interrupt callback the reader
67/// binds to its own cancellation.
68///
69/// The format context is read through [`Deref`], and moved only by the
70/// methods here. `FFmpeg` holds a pointer to this input's interrupt state,
71/// which lives exactly as long as the input, so the context can never be
72/// taken out of it and outlive that state:
73///
74/// ```compile_fail,E0596
75/// # fn steal(mut demux: alpha2_ffmpeg_bridge::DemuxInput, other: rsmpeg::avformat::AVFormatContextInput) {
76/// let stolen = std::mem::replace(&mut *demux, other);
77/// drop(demux);
78/// # }
79/// ```
80pub struct DemuxInput {
81    // Declared first, so it is closed before the interrupt state it points to is released.
82    input: AVFormatContextInput,
83    interrupt: Arc<InputInterrupt>,
84}
85
86impl DemuxInput {
87    /// Makes every `FFmpeg` call on this input that waits on I/O, from now
88    /// until the next call to this, stop with `AVERROR_EXIT` once `check`
89    /// returns true.
90    pub fn interrupt_when(&self, check: impl Fn() -> bool + Send + Sync + 'static) {
91        *self
92            .interrupt
93            .check
94            .lock()
95            .unwrap_or_else(PoisonError::into_inner) = Some(Arc::new(check));
96    }
97
98    /// The stream of `media_type` a reader shows, by its `AVStream` index:
99    /// the one `av_find_best_stream` picks among those with a decoder, never
100    /// an attached picture (a song's cover art is metadata, not its video).
101    /// When `FFmpeg` picks an attached picture, the first other stream of the
102    /// type that has a decoder is chosen, in stream order.
103    ///
104    /// `None` when the input has no such stream, or none that this runtime
105    /// can decode: a stream without a decoder leaves the type unselected
106    /// rather than failing the input, whose other streams may decode.
107    ///
108    /// # Errors
109    /// Any other `av_find_best_stream` failure.
110    pub fn best_stream(&self, media_type: ffi::AVMediaType) -> Result<Option<usize>, RsmpegError> {
111        let attached = |index: usize| {
112            // `bindgen` types the flag by the target's C ABI; widening both sides compiles
113            // everywhere.
114            self.streams().get(index).is_some_and(|stream| {
115                i64::from(stream.disposition) & i64::from(ffi::AV_DISPOSITION_ATTACHED_PIC) != 0
116            })
117        };
118        let find = |wanted: c_int| {
119            let mut decoder: *const ffi::AVCodec = ptr::null();
120            // SAFETY: the context is an open input this value owns; `av_find_best_stream` only
121            // reads it, and writes the decoder it found into `decoder`, a local.
122            let found = unsafe {
123                ffi::av_find_best_stream(
124                    self.input.as_ptr().cast_mut(),
125                    media_type,
126                    wanted,
127                    -1,
128                    &raw mut decoder,
129                    0,
130                )
131            };
132            match found {
133                index if index >= 0 => Ok(usize::try_from(index).ok()),
134                ffi::AVERROR_STREAM_NOT_FOUND | ffi::AVERROR_DECODER_NOT_FOUND => Ok(None),
135                error => Err(RsmpegError::AVError(error)),
136            }
137        };
138        match find(-1)? {
139            Some(index) if attached(index) => {}
140            found => return Ok(found),
141        }
142        for (index, stream) in self.streams().iter().enumerate() {
143            if stream.codecpar().codec_type != media_type || attached(index) {
144                continue;
145            }
146            if let Some(found) = find(c_int::try_from(index).unwrap_or(c_int::MAX))? {
147                return Ok(Some(found));
148            }
149        }
150        Ok(None)
151    }
152
153    /// The next packet in file order (`av_read_frame`), or `None` at the
154    /// end of the file.
155    ///
156    /// # Errors
157    /// The `FFmpeg` read failure, or `AVERROR_EXIT` when the interrupt
158    /// check stopped it.
159    pub fn read_packet(&mut self) -> Result<Option<AVPacket>, RsmpegError> {
160        self.input.read_packet()
161    }
162
163    /// Seeks to the key frame at `timestamp` of `stream_index`
164    /// (`av_seek_frame`; -1 for `AV_TIME_BASE` units).
165    ///
166    /// # Errors
167    /// The `FFmpeg` seek failure, or `AVERROR_EXIT` when the interrupt check
168    /// stopped it.
169    pub fn seek(
170        &mut self,
171        stream_index: i32,
172        timestamp: i64,
173        flags: i32,
174    ) -> Result<(), RsmpegError> {
175        self.input.seek(stream_index, timestamp, flags)
176    }
177
178    /// Has the demuxer skip every stream but `index`: a reader of one stream
179    /// then neither reads nor copies the others' packets (`AVDISCARD_ALL`),
180    /// which demuxers such as `mov` do not even fetch from the file.
181    pub fn discard_all_but(&mut self, index: usize) {
182        self.keep_only(&[index]);
183    }
184
185    /// [`Self::discard_all_but`] for a reader of the streams `kept`.
186    pub fn keep_only(&mut self, kept: &[usize]) {
187        for (at, stream) in self.streams().iter().enumerate() {
188            let discard = if kept.contains(&at) {
189                ffi::AVDISCARD_DEFAULT
190            } else {
191                ffi::AVDISCARD_ALL
192            };
193            // SAFETY: the stream belongs to the open input this value owns exclusively (`&mut
194            // self`); `discard` is a plain field the demuxer reads on its next read or seek.
195            unsafe { (*stream.as_ptr().cast_mut()).discard = discard };
196        }
197    }
198}
199
200impl Deref for DemuxInput {
201    type Target = AVFormatContextInput;
202
203    fn deref(&self) -> &Self::Target {
204        &self.input
205    }
206}
207
208/// Why [`open_input`] opened nothing.
209#[derive(Debug)]
210pub enum OpenInputError {
211    /// The file is a container `formats` does not name: the name of the
212    /// demuxer that claims it.
213    Refused(String),
214    /// `FFmpeg` could not open or read it.
215    Ffmpeg(RsmpegError),
216}
217
218impl std::fmt::Display for OpenInputError {
219    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        match self {
221            Self::Refused(format) => write!(formatter, "the file is {format} input"),
222            Self::Ffmpeg(error) => write!(formatter, "{error}"),
223        }
224    }
225}
226
227impl std::error::Error for OpenInputError {}
228
229/// Opens `url` and reads its stream information with only the demuxers
230/// named in `formats` (comma-separated `FFmpeg` short names) and only the
231/// `file` protocol, for the input and anything it opens in turn.
232///
233/// A still image opens with its own demuxer, chosen by its signature, when
234/// `formats` names it. `FFmpeg` otherwise picks from the file's name as well
235/// as its first bytes, and stops reading bytes once a demuxer is fairly sure:
236/// the `image2` demuxer, for numbered image files, claims any file named like
237/// an image on its name alone, above the JPEG demuxer, which is sure of a
238/// JPEG only once it has seen the whole picture. A JPEG larger than the first
239/// probe was `image2` input, which `formats` leaves out.
240///
241/// # Errors
242/// [`OpenInputError::Refused`] for a container `formats` leaves out, else the
243/// `FFmpeg` failure.
244pub fn open_input(url: &CStr, formats: &CStr) -> Result<DemuxInput, OpenInputError> {
245    let still = still_image_format(url)
246        .filter(|name| allowed_name(name, formats))
247        // SAFETY: `name` is a NUL-terminated demuxer name; the result is null or one of `FFmpeg`'s
248        // static demuxers.
249        .map_or(ptr::null(), |name| unsafe {
250            ffi::av_find_input_format(name.as_ptr())
251        });
252    open_with(url, formats, still).map_err(|failure| match failure {
253        OpenFailure::Open(opened) => refused_format(url, formats).map_or(
254            OpenInputError::Ffmpeg(RsmpegError::OpenInputError(opened)),
255            OpenInputError::Refused,
256        ),
257        OpenFailure::Other(error) => error,
258    })
259}
260
261/// The demuxer of the still image format whose signature `url` starts with.
262fn still_image_format(url: &CStr) -> Option<&'static CStr> {
263    let mut signature = [0_u8; 12];
264    File::open(Path::new(url.to_str().ok()?))
265        .ok()?
266        .read_exact(&mut signature)
267        .ok()?;
268    if signature.starts_with(&[0xff, 0xd8, 0xff]) {
269        Some(c"jpeg_pipe")
270    } else if signature.starts_with(b"\x89PNG\r\n\x1a\n") {
271        Some(c"png_pipe")
272    } else if signature.starts_with(b"RIFF") && signature.ends_with(b"WEBP") {
273        Some(c"webp_pipe")
274    } else {
275        None
276    }
277}
278
279enum OpenFailure {
280    Open(c_int),
281    Other(OpenInputError),
282}
283
284/// Opens `url` with `format`, or the demuxer `FFmpeg` picks when it is null.
285fn open_with(
286    url: &CStr,
287    formats: &CStr,
288    format: *const ffi::AVInputFormat,
289) -> Result<DemuxInput, OpenFailure> {
290    let interrupt = Arc::new(InputInterrupt::default());
291    // SAFETY: allocation has no preconditions; a null result is handled below.
292    let mut context = unsafe { ffi::avformat_alloc_context() };
293    if context.is_null() {
294        return Err(OpenFailure::Other(OpenInputError::Ffmpeg(
295            RsmpegError::AVError(ffi::AVERROR(ffi::ENOMEM)),
296        )));
297    }
298    // SAFETY: the context was just allocated and nothing else refers to it. The opaque pointer
299    // stays valid for the context's life: on success the returned input keeps the `Arc`, and on
300    // failure `avformat_open_input` frees the context before this returns.
301    unsafe {
302        (*context).interrupt_callback = ffi::AVIOInterruptCB {
303            callback: Some(interrupt_requested),
304            opaque: Arc::as_ptr(&interrupt).cast_mut().cast(),
305        };
306    }
307    let options =
308        AVDictionary::new(c"format_whitelist", formats, 0).set(c"protocol_whitelist", c"file", 0);
309    let mut options = options.into_raw().as_ptr();
310    // SAFETY: `context` is a fresh context, `url` a NUL-terminated string, `format` null or one of
311    // `FFmpeg`'s static demuxers, and `options` a dictionary this call takes and leaves the unused
312    // entries of; it frees and nulls `context` on failure.
313    let opened = unsafe {
314        ffi::avformat_open_input(&raw mut context, url.as_ptr(), format, &raw mut options)
315    };
316    // SAFETY: `options` is null or the dictionary `avformat_open_input` handed back, which this
317    // owns.
318    unsafe { ffi::av_dict_free(&raw mut options) };
319    if opened < 0 {
320        return Err(OpenFailure::Open(opened));
321    }
322    let Some(context) = NonNull::new(context) else {
323        return Err(OpenFailure::Open(opened));
324    };
325    // SAFETY: `avformat_open_input` succeeded, so `context` is an open input context this now owns;
326    // its `Drop` closes it with `avformat_close_input`.
327    let mut input = unsafe { AVFormatContextInput::from_raw(context) };
328    // SAFETY: the context is open and exclusively owned; null options read no per-stream
329    // dictionaries.
330    let found = unsafe { ffi::avformat_find_stream_info(input.as_mut_ptr(), ptr::null_mut()) };
331    if found < 0 {
332        return Err(OpenFailure::Other(OpenInputError::Ffmpeg(
333            RsmpegError::FindStreamInfoError(found),
334        )));
335    }
336    Ok(DemuxInput { input, interrupt })
337}
338
339/// The demuxer `FFmpeg` would pick for `url`, when `formats` does not name
340/// it: what a failed open is told so its error says why, rather than
341/// `EINVAL`. It probes the bytes the way an open does, without opening the
342/// container.
343fn refused_format(url: &CStr, formats: &CStr) -> Option<String> {
344    const PROBE_BYTES: u64 = 1 << 20;
345    let path = Path::new(url.to_str().ok()?);
346    let mut bytes = Vec::new();
347    File::open(path)
348        .ok()?
349        .take(PROBE_BYTES)
350        .read_to_end(&mut bytes)
351        .ok()?;
352    let size = c_int::try_from(bytes.len()).ok()?;
353    bytes.resize(
354        bytes.len() + usize::try_from(ffi::AVPROBE_PADDING_SIZE).ok()?,
355        0,
356    );
357    let probe = ffi::AVProbeData {
358        filename: url.as_ptr(),
359        buf: bytes.as_mut_ptr(),
360        buf_size: size,
361        mime_type: ptr::null(),
362    };
363    let mut score = 0;
364    // SAFETY: `probe` points at `size` readable bytes followed by the zeroed padding `FFmpeg`
365    // requires, and at a NUL-terminated filename; both outlive the call.
366    let format = unsafe { ffi::av_probe_input_format3(&raw const probe, 1, &raw mut score) };
367    if format.is_null() {
368        return None;
369    }
370    // SAFETY: a non-null result is one of `FFmpeg`'s static demuxer descriptions, whose name is a
371    // static NUL-terminated string.
372    let name = unsafe { CStr::from_ptr((*format).name) };
373    (!allowed_name(name, formats)).then(|| name.to_string_lossy().into_owned())
374}
375
376/// Whether `formats` names the demuxer `name`, under any of its
377/// comma-separated names.
378fn allowed_name(name: &CStr, formats: &CStr) -> bool {
379    let allowed = formats.to_string_lossy();
380    name.to_string_lossy()
381        .split(',')
382        .any(|part| allowed.split(',').any(|format| format == part))
383}
384
385/// A container opened for writing whose file is closed without panicking.
386///
387/// [`Self::close`] reports the error closing the file returns: a write that
388/// failed earlier, or one the file system only reports at `close(2)`.
389/// Dropping it closes the file and discards that error, which is what an
390/// abandoned output wants.
391pub struct OutputFile(AVFormatContextOutput);
392
393impl OutputFile {
394    /// Creates the output with the `FFmpeg` muxer named `format` (`mp4`,
395    /// `mov`, `matroska`, `webm`), and opens `filename` for it. Nothing is
396    /// guessed from the file name.
397    ///
398    /// # Errors
399    /// When there is no such muxer or the file cannot be opened.
400    pub fn create(filename: &CStr, format: &CStr) -> Result<Self, RsmpegError> {
401        AVFormatContextOutput::builder()
402            .format_name(format)
403            .filename(filename)
404            .build()
405            .map(Self)
406    }
407
408    /// Closes the file, after the trailer.
409    ///
410    /// # Errors
411    /// The error the file's I/O reports on closing.
412    pub fn close(mut self) -> Result<(), BridgeError> {
413        close_io(&mut self.0)
414    }
415}
416
417impl Deref for OutputFile {
418    type Target = AVFormatContextOutput;
419
420    fn deref(&self) -> &Self::Target {
421        &self.0
422    }
423}
424
425impl DerefMut for OutputFile {
426    fn deref_mut(&mut self) -> &mut Self::Target {
427        &mut self.0
428    }
429}
430
431impl Drop for OutputFile {
432    fn drop(&mut self) {
433        let _ = close_io(&mut self.0);
434    }
435}
436
437/// Closes `output`'s file, if it has one open, and detaches it, so the
438/// format context is freed without it.
439fn close_io(output: &mut AVFormatContextOutput) -> Result<(), BridgeError> {
440    let Some(io) = output.io_context.take() else {
441        return Ok(());
442    };
443    // SAFETY: the context is live and exclusively borrowed. `pb` is the I/O just taken, which is
444    // closed below; `avformat_free_context` and rsmpeg's `Drop` skip a null `pb`.
445    unsafe {
446        output.deref_mut().pb = ptr::null_mut();
447    }
448    match io {
449        AVIOContextContainer::Url(url) => {
450            // rsmpeg's `Drop` for it would close it again and unwrap the result.
451            let mut url = ManuallyDrop::new(url);
452            let mut raw = url.as_mut_ptr();
453            // SAFETY: `raw` is the I/O context `avio_open2` opened for this output, closed nowhere
454            // else: its owner is never dropped.
455            let closed = unsafe { ffi::avio_closep(&raw mut raw) };
456            if closed < 0 {
457                return Err(BridgeError::Ffmpeg(RsmpegError::AVError(closed)));
458            }
459            Ok(())
460        }
461        AVIOContextContainer::Custom(custom) => {
462            drop(custom);
463            Ok(())
464        }
465    }
466}
467
468#[cfg(test)]
469#[path = "container_tests.rs"]
470mod tests;