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;