use std::{
ffi::{CStr, c_int, c_void},
fs::File,
io::Read,
mem::ManuallyDrop,
ops::{Deref, DerefMut},
path::Path,
ptr::{self, NonNull},
sync::{Arc, Mutex, PoisonError},
};
use rsmpeg::{
UnsafeDerefMut,
avcodec::AVPacket,
avformat::{AVFormatContextInput, AVFormatContextOutput, AVIOContextContainer},
avutil::AVDictionary,
error::RsmpegError,
ffi,
};
use crate::BridgeError;
type InterruptCheck = Arc<dyn Fn() -> bool + Send + Sync>;
#[derive(Default)]
struct InputInterrupt {
check: Mutex<Option<InterruptCheck>>,
}
impl InputInterrupt {
fn requested(&self) -> bool {
let check = self
.check
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
check.is_some_and(|check| check())
}
}
unsafe extern "C" fn interrupt_requested(opaque: *mut c_void) -> c_int {
let interrupt = unsafe { &*opaque.cast::<InputInterrupt>() };
c_int::from(interrupt.requested())
}
pub struct DemuxInput {
input: AVFormatContextInput,
interrupt: Arc<InputInterrupt>,
}
impl DemuxInput {
pub fn interrupt_when(&self, check: impl Fn() -> bool + Send + Sync + 'static) {
*self
.interrupt
.check
.lock()
.unwrap_or_else(PoisonError::into_inner) = Some(Arc::new(check));
}
pub fn best_stream(&self, media_type: ffi::AVMediaType) -> Result<Option<usize>, RsmpegError> {
let attached = |index: usize| {
self.streams().get(index).is_some_and(|stream| {
i64::from(stream.disposition) & i64::from(ffi::AV_DISPOSITION_ATTACHED_PIC) != 0
})
};
let find = |wanted: c_int| {
let mut decoder: *const ffi::AVCodec = ptr::null();
let found = unsafe {
ffi::av_find_best_stream(
self.input.as_ptr().cast_mut(),
media_type,
wanted,
-1,
&raw mut decoder,
0,
)
};
match found {
index if index >= 0 => Ok(usize::try_from(index).ok()),
ffi::AVERROR_STREAM_NOT_FOUND | ffi::AVERROR_DECODER_NOT_FOUND => Ok(None),
error => Err(RsmpegError::AVError(error)),
}
};
match find(-1)? {
Some(index) if attached(index) => {}
found => return Ok(found),
}
for (index, stream) in self.streams().iter().enumerate() {
if stream.codecpar().codec_type != media_type || attached(index) {
continue;
}
if let Some(found) = find(c_int::try_from(index).unwrap_or(c_int::MAX))? {
return Ok(Some(found));
}
}
Ok(None)
}
pub fn read_packet(&mut self) -> Result<Option<AVPacket>, RsmpegError> {
self.input.read_packet()
}
pub fn seek(
&mut self,
stream_index: i32,
timestamp: i64,
flags: i32,
) -> Result<(), RsmpegError> {
self.input.seek(stream_index, timestamp, flags)
}
pub fn discard_all_but(&mut self, index: usize) {
self.keep_only(&[index]);
}
pub fn keep_only(&mut self, kept: &[usize]) {
for (at, stream) in self.streams().iter().enumerate() {
let discard = if kept.contains(&at) {
ffi::AVDISCARD_DEFAULT
} else {
ffi::AVDISCARD_ALL
};
unsafe { (*stream.as_ptr().cast_mut()).discard = discard };
}
}
}
impl Deref for DemuxInput {
type Target = AVFormatContextInput;
fn deref(&self) -> &Self::Target {
&self.input
}
}
#[derive(Debug)]
pub enum OpenInputError {
Refused(String),
Ffmpeg(RsmpegError),
}
impl std::fmt::Display for OpenInputError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Refused(format) => write!(formatter, "the file is {format} input"),
Self::Ffmpeg(error) => write!(formatter, "{error}"),
}
}
}
impl std::error::Error for OpenInputError {}
pub fn open_input(url: &CStr, formats: &CStr) -> Result<DemuxInput, OpenInputError> {
let still = still_image_format(url)
.filter(|name| allowed_name(name, formats))
.map_or(ptr::null(), |name| unsafe {
ffi::av_find_input_format(name.as_ptr())
});
open_with(url, formats, still).map_err(|failure| match failure {
OpenFailure::Open(opened) => refused_format(url, formats).map_or(
OpenInputError::Ffmpeg(RsmpegError::OpenInputError(opened)),
OpenInputError::Refused,
),
OpenFailure::Other(error) => error,
})
}
fn still_image_format(url: &CStr) -> Option<&'static CStr> {
let mut signature = [0_u8; 12];
File::open(Path::new(url.to_str().ok()?))
.ok()?
.read_exact(&mut signature)
.ok()?;
if signature.starts_with(&[0xff, 0xd8, 0xff]) {
Some(c"jpeg_pipe")
} else if signature.starts_with(b"\x89PNG\r\n\x1a\n") {
Some(c"png_pipe")
} else if signature.starts_with(b"RIFF") && signature.ends_with(b"WEBP") {
Some(c"webp_pipe")
} else {
None
}
}
enum OpenFailure {
Open(c_int),
Other(OpenInputError),
}
fn open_with(
url: &CStr,
formats: &CStr,
format: *const ffi::AVInputFormat,
) -> Result<DemuxInput, OpenFailure> {
let interrupt = Arc::new(InputInterrupt::default());
let mut context = unsafe { ffi::avformat_alloc_context() };
if context.is_null() {
return Err(OpenFailure::Other(OpenInputError::Ffmpeg(
RsmpegError::AVError(ffi::AVERROR(ffi::ENOMEM)),
)));
}
unsafe {
(*context).interrupt_callback = ffi::AVIOInterruptCB {
callback: Some(interrupt_requested),
opaque: Arc::as_ptr(&interrupt).cast_mut().cast(),
};
}
let options =
AVDictionary::new(c"format_whitelist", formats, 0).set(c"protocol_whitelist", c"file", 0);
let mut options = options.into_raw().as_ptr();
let opened = unsafe {
ffi::avformat_open_input(&raw mut context, url.as_ptr(), format, &raw mut options)
};
unsafe { ffi::av_dict_free(&raw mut options) };
if opened < 0 {
return Err(OpenFailure::Open(opened));
}
let Some(context) = NonNull::new(context) else {
return Err(OpenFailure::Open(opened));
};
let mut input = unsafe { AVFormatContextInput::from_raw(context) };
let found = unsafe { ffi::avformat_find_stream_info(input.as_mut_ptr(), ptr::null_mut()) };
if found < 0 {
return Err(OpenFailure::Other(OpenInputError::Ffmpeg(
RsmpegError::FindStreamInfoError(found),
)));
}
Ok(DemuxInput { input, interrupt })
}
fn refused_format(url: &CStr, formats: &CStr) -> Option<String> {
const PROBE_BYTES: u64 = 1 << 20;
let path = Path::new(url.to_str().ok()?);
let mut bytes = Vec::new();
File::open(path)
.ok()?
.take(PROBE_BYTES)
.read_to_end(&mut bytes)
.ok()?;
let size = c_int::try_from(bytes.len()).ok()?;
bytes.resize(
bytes.len() + usize::try_from(ffi::AVPROBE_PADDING_SIZE).ok()?,
0,
);
let probe = ffi::AVProbeData {
filename: url.as_ptr(),
buf: bytes.as_mut_ptr(),
buf_size: size,
mime_type: ptr::null(),
};
let mut score = 0;
let format = unsafe { ffi::av_probe_input_format3(&raw const probe, 1, &raw mut score) };
if format.is_null() {
return None;
}
let name = unsafe { CStr::from_ptr((*format).name) };
(!allowed_name(name, formats)).then(|| name.to_string_lossy().into_owned())
}
fn allowed_name(name: &CStr, formats: &CStr) -> bool {
let allowed = formats.to_string_lossy();
name.to_string_lossy()
.split(',')
.any(|part| allowed.split(',').any(|format| format == part))
}
pub struct OutputFile(AVFormatContextOutput);
impl OutputFile {
pub fn create(filename: &CStr, format: &CStr) -> Result<Self, RsmpegError> {
AVFormatContextOutput::builder()
.format_name(format)
.filename(filename)
.build()
.map(Self)
}
pub fn close(mut self) -> Result<(), BridgeError> {
close_io(&mut self.0)
}
}
impl Deref for OutputFile {
type Target = AVFormatContextOutput;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for OutputFile {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Drop for OutputFile {
fn drop(&mut self) {
let _ = close_io(&mut self.0);
}
}
fn close_io(output: &mut AVFormatContextOutput) -> Result<(), BridgeError> {
let Some(io) = output.io_context.take() else {
return Ok(());
};
unsafe {
output.deref_mut().pb = ptr::null_mut();
}
match io {
AVIOContextContainer::Url(url) => {
let mut url = ManuallyDrop::new(url);
let mut raw = url.as_mut_ptr();
let closed = unsafe { ffi::avio_closep(&raw mut raw) };
if closed < 0 {
return Err(BridgeError::Ffmpeg(RsmpegError::AVError(closed)));
}
Ok(())
}
AVIOContextContainer::Custom(custom) => {
drop(custom);
Ok(())
}
}
}
#[cfg(test)]
#[path = "container_tests.rs"]
mod tests;