1use std::{
7 ffi::{CStr, CString, c_int, c_void},
8 fs::File,
9 io::Read,
10 mem::ManuallyDrop,
11 ops::{Deref, DerefMut},
12 path::Path,
13 ptr::{self, NonNull},
14 sync::{Arc, Mutex, PoisonError},
15};
16
17use rsmpeg::{
18 UnsafeDerefMut,
19 avcodec::AVPacket,
20 avformat::{AVFormatContextInput, AVFormatContextOutput, AVIOContextContainer},
21 avutil::AVDictionary,
22 error::RsmpegError,
23 ffi,
24};
25
26use crate::BridgeError;
27
28type InterruptCheck = Arc<dyn Fn() -> bool + Send + Sync>;
30
31#[derive(Default)]
33struct InputInterrupt {
34 check: Mutex<Option<InterruptCheck>>,
35}
36
37impl InputInterrupt {
38 fn requested(&self) -> bool {
39 let check = self
40 .check
41 .lock()
42 .unwrap_or_else(PoisonError::into_inner)
43 .clone();
44 check.is_some_and(|check| check())
45 }
46}
47
48unsafe extern "C" fn interrupt_requested(opaque: *mut c_void) -> c_int {
50 let interrupt = unsafe { &*opaque.cast::<InputInterrupt>() };
53 c_int::from(interrupt.requested())
54}
55
56pub struct DemuxInput {
69 input: AVFormatContextInput,
71 interrupt: Arc<InputInterrupt>,
72}
73
74impl DemuxInput {
75 pub fn interrupt_when(&self, check: impl Fn() -> bool + Send + Sync + 'static) {
77 *self
78 .interrupt
79 .check
80 .lock()
81 .unwrap_or_else(PoisonError::into_inner) = Some(Arc::new(check));
82 }
83
84 pub fn best_stream(&self, media_type: ffi::AVMediaType) -> Result<Option<usize>, RsmpegError> {
92 let attached = |index: usize| {
93 self.streams().get(index).is_some_and(|stream| {
95 i64::from(stream.disposition) & i64::from(ffi::AV_DISPOSITION_ATTACHED_PIC) != 0
96 })
97 };
98 let find = |wanted: c_int| {
99 let mut decoder: *const ffi::AVCodec = ptr::null();
100 let found = unsafe {
103 ffi::av_find_best_stream(
104 self.input.as_ptr().cast_mut(),
105 media_type,
106 wanted,
107 -1,
108 &raw mut decoder,
109 0,
110 )
111 };
112 match found {
113 index if index >= 0 => Ok(usize::try_from(index).ok()),
114 ffi::AVERROR_STREAM_NOT_FOUND | ffi::AVERROR_DECODER_NOT_FOUND => Ok(None),
115 error => Err(RsmpegError::AVError(error)),
116 }
117 };
118 match find(-1)? {
119 Some(index) if attached(index) => {}
120 found => return Ok(found),
121 }
122 for (index, stream) in self.streams().iter().enumerate() {
123 if stream.codecpar().codec_type != media_type || attached(index) {
124 continue;
125 }
126 if let Some(found) = find(c_int::try_from(index).unwrap_or(c_int::MAX))? {
127 return Ok(Some(found));
128 }
129 }
130 Ok(None)
131 }
132
133 pub fn read_packet(&mut self) -> Result<Option<AVPacket>, RsmpegError> {
138 self.input.read_packet()
139 }
140
141 pub fn seek(
146 &mut self,
147 stream_index: i32,
148 timestamp: i64,
149 flags: i32,
150 ) -> Result<(), RsmpegError> {
151 self.input.seek(stream_index, timestamp, flags)
152 }
153
154 pub fn discard_all_but(&mut self, index: usize) {
156 self.keep_only(&[index]);
157 }
158
159 pub fn keep_only(&mut self, kept: &[usize]) {
161 for (at, stream) in self.streams().iter().enumerate() {
162 let discard = if kept.contains(&at) {
163 ffi::AVDISCARD_DEFAULT
164 } else {
165 ffi::AVDISCARD_ALL
166 };
167 unsafe { (*stream.as_ptr().cast_mut()).discard = discard };
170 }
171 }
172}
173
174impl Deref for DemuxInput {
175 type Target = AVFormatContextInput;
176
177 fn deref(&self) -> &Self::Target {
178 &self.input
179 }
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum PathError {
185 NotUtf8,
187 Nul,
189}
190
191impl std::fmt::Display for PathError {
192 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 formatter.write_str(match self {
194 Self::NotUtf8 => "is not UTF-8",
195 Self::Nul => "contains a NUL byte",
196 })
197 }
198}
199
200impl std::error::Error for PathError {}
201
202pub fn file_url(path: &Path) -> Result<CString, PathError> {
207 let absolute = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
209 let text = absolute.to_str().ok_or(PathError::NotUtf8)?;
210 CString::new(format!("file:{text}")).map_err(|_| PathError::Nul)
211}
212
213#[derive(Debug)]
215pub enum OpenInputError {
216 Path(PathError),
218 Refused(String),
220 Ffmpeg(RsmpegError),
222}
223
224impl std::fmt::Display for OpenInputError {
225 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 match self {
227 Self::Path(error) => write!(formatter, "the path {error}"),
228 Self::Refused(format) => write!(formatter, "the file is {format} input"),
229 Self::Ffmpeg(error) => write!(formatter, "{error}"),
230 }
231 }
232}
233
234impl std::error::Error for OpenInputError {}
235
236pub fn open_input(path: &Path, formats: &CStr) -> Result<DemuxInput, OpenInputError> {
246 let url = file_url(path).map_err(OpenInputError::Path)?;
247 let url = url.as_c_str();
248 crate::install_log_capture();
249 let still = still_image_format(path)
250 .filter(|name| allowed_name(name, formats))
251 .map_or(ptr::null(), |name| unsafe {
254 ffi::av_find_input_format(name.as_ptr())
255 });
256 open_with(url, formats, still).map_err(|failure| match failure {
257 OpenFailure::Open(opened) => refused_format(path, url, formats).map_or(
258 OpenInputError::Ffmpeg(RsmpegError::OpenInputError(opened)),
259 OpenInputError::Refused,
260 ),
261 OpenFailure::Other(error) => error,
262 })
263}
264
265fn still_image_format(path: &Path) -> Option<&'static CStr> {
266 let mut signature = [0_u8; 12];
267 File::open(path).ok()?.read_exact(&mut signature).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
284fn 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 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 unsafe {
301 (*context).interrupt_callback = ffi::AVIOInterruptCB {
302 callback: Some(interrupt_requested),
303 opaque: Arc::as_ptr(&interrupt).cast_mut().cast(),
304 };
305 }
306 let options =
307 AVDictionary::new(c"format_whitelist", formats, 0).set(c"protocol_whitelist", c"file", 0);
308 let mut options = options.into_raw().as_ptr();
309 let opened = unsafe {
312 ffi::avformat_open_input(&raw mut context, url.as_ptr(), format, &raw mut options)
313 };
314 unsafe { ffi::av_dict_free(&raw mut options) };
317 if opened < 0 {
318 return Err(OpenFailure::Open(opened));
319 }
320 let Some(context) = NonNull::new(context) else {
321 return Err(OpenFailure::Open(opened));
322 };
323 let mut input = unsafe { AVFormatContextInput::from_raw(context) };
326 let found = unsafe { ffi::avformat_find_stream_info(input.as_mut_ptr(), ptr::null_mut()) };
329 if found < 0 {
330 return Err(OpenFailure::Other(OpenInputError::Ffmpeg(
331 RsmpegError::FindStreamInfoError(found),
332 )));
333 }
334 Ok(DemuxInput { input, interrupt })
335}
336
337fn refused_format(path: &Path, url: &CStr, formats: &CStr) -> Option<String> {
340 const PROBE_BYTES: u64 = 1 << 20;
341 let mut bytes = Vec::new();
342 File::open(path)
343 .ok()?
344 .take(PROBE_BYTES)
345 .read_to_end(&mut bytes)
346 .ok()?;
347 let size = c_int::try_from(bytes.len()).ok()?;
348 bytes.resize(
349 bytes.len() + usize::try_from(ffi::AVPROBE_PADDING_SIZE).ok()?,
350 0,
351 );
352 let probe = ffi::AVProbeData {
353 filename: url.as_ptr(),
354 buf: bytes.as_mut_ptr(),
355 buf_size: size,
356 mime_type: ptr::null(),
357 };
358 let mut score = 0;
359 let format = unsafe { ffi::av_probe_input_format3(&raw const probe, 1, &raw mut score) };
362 if format.is_null() {
363 return None;
364 }
365 let name = unsafe { CStr::from_ptr((*format).name) };
368 (!allowed_name(name, formats)).then(|| name.to_string_lossy().into_owned())
369}
370
371fn allowed_name(name: &CStr, formats: &CStr) -> bool {
372 let allowed = formats.to_string_lossy();
373 name.to_string_lossy()
374 .split(',')
375 .any(|part| allowed.split(',').any(|format| format == part))
376}
377
378pub struct OutputFile(AVFormatContextOutput);
383
384#[derive(Debug)]
386pub enum CreateOutputError {
387 Path(PathError),
389 Ffmpeg(RsmpegError),
391}
392
393impl std::fmt::Display for CreateOutputError {
394 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395 match self {
396 Self::Path(error) => write!(formatter, "the path {error}"),
397 Self::Ffmpeg(error) => write!(formatter, "{error}"),
398 }
399 }
400}
401
402impl std::error::Error for CreateOutputError {}
403
404impl OutputFile {
405 pub fn create(path: &Path, format: &CStr) -> Result<Self, CreateOutputError> {
411 let url = file_url(path).map_err(CreateOutputError::Path)?;
412 let mut options = Some(AVDictionary::new(c"protocol_whitelist", c"file", 0));
413 AVFormatContextOutput::builder()
414 .format_name(format)
415 .filename(&url)
416 .options(&mut options)
417 .build()
418 .map(Self)
419 .map_err(CreateOutputError::Ffmpeg)
420 }
421
422 pub fn close(mut self) -> Result<(), BridgeError> {
427 close_io(&mut self.0)
428 }
429}
430
431impl Deref for OutputFile {
432 type Target = AVFormatContextOutput;
433
434 fn deref(&self) -> &Self::Target {
435 &self.0
436 }
437}
438
439impl DerefMut for OutputFile {
440 fn deref_mut(&mut self) -> &mut Self::Target {
441 &mut self.0
442 }
443}
444
445impl Drop for OutputFile {
446 fn drop(&mut self) {
447 let _ = close_io(&mut self.0);
448 }
449}
450
451fn close_io(output: &mut AVFormatContextOutput) -> Result<(), BridgeError> {
453 let Some(io) = output.io_context.take() else {
454 return Ok(());
455 };
456 unsafe {
459 output.deref_mut().pb = ptr::null_mut();
460 }
461 match io {
462 AVIOContextContainer::Url(url) => {
463 let mut url = ManuallyDrop::new(url);
465 let mut raw = url.as_mut_ptr();
466 let closed = unsafe { ffi::avio_closep(&raw mut raw) };
469 if closed < 0 {
470 return Err(BridgeError::Ffmpeg(RsmpegError::AVError(closed)));
471 }
472 Ok(())
473 }
474 AVIOContextContainer::Custom(custom) => {
475 drop(custom);
476 Ok(())
477 }
478 }
479}
480
481#[cfg(test)]
482#[path = "container_tests.rs"]
483mod tests;