use super::*;
pub(in crate::core::context) struct InputOpaque {
pub(super) read: Box<dyn FnMut(&mut [u8]) -> i32 + Send>,
pub(super) seek: Option<Box<dyn FnMut(i64, i32) -> i64 + Send>>,
pub(super) poisoned: bool,
}
pub(super) unsafe extern "C" fn read_packet_wrapper(
opaque: *mut libc::c_void,
buf: *mut u8,
buf_size: libc::c_int,
) -> libc::c_int {
if buf.is_null() || buf_size <= 0 {
return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO);
}
let context = &mut *(opaque as *mut InputOpaque);
if context.poisoned {
return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO);
}
let slice = std::slice::from_raw_parts_mut(buf, buf_size as usize);
let ret = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (context.read)(slice)))
{
Ok(ret) => ret,
Err(_) => {
context.poisoned = true;
return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO);
}
};
if ret > buf_size {
return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO);
}
ret
}
pub(super) unsafe extern "C" fn seek_input_packet_wrapper(
opaque: *mut libc::c_void,
offset: i64,
whence: libc::c_int,
) -> i64 {
let context = &mut *(opaque as *mut InputOpaque);
if context.poisoned {
return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64;
}
if let Some(seek_func) = &mut context.seek {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
(*seek_func)(offset, whence)
})) {
Ok(ret) => ret,
Err(_) => {
context.poisoned = true;
ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64
}
}
} else {
ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ESPIPE) as i64
}
}
pub(super) fn open_input_files(
inputs: &mut Vec<Input>,
copy_ts: bool,
interrupt_state: &Arc<crate::core::context::InterruptState>,
) -> Result<Vec<Demuxer>> {
let mut demuxs = Vec::new();
for (i, input) in inputs.iter_mut().enumerate() {
unsafe {
let result = open_input_file(i, input, copy_ts, interrupt_state);
if let Err(e) = result {
return Err(e);
}
let demux = result.unwrap();
demuxs.push(demux)
}
}
Ok(demuxs)
}
#[cfg(docsrs)]
unsafe fn open_input_file(
index: usize,
input: &mut Input,
copy_ts: bool,
interrupt_state: &Arc<crate::core::context::InterruptState>,
) -> Result<Demuxer> {
Err(Error::Bug)
}
#[cfg(not(docsrs))]
unsafe fn open_input_file(
index: usize,
input: &mut Input,
copy_ts: bool,
interrupt_state: &Arc<crate::core::context::InterruptState>,
) -> Result<Demuxer> {
if let Some((num, den)) = input.framerate {
if num <= 0 || den <= 0 {
return Err(OpenInputError::InvalidOption(format!(
"set_framerate requires positive numerator and denominator, got {num}/{den}"
))
.into());
}
}
if let Some(scale) = input.ts_scale {
if !scale.is_finite() || scale <= 0.0 {
return Err(OpenInputError::InvalidOption(format!(
"set_ts_scale requires a positive finite value, got {scale}"
))
.into());
}
}
if input.io_buffer_size == 0 || input.io_buffer_size > i32::MAX as usize {
return Err(OpenInputError::InvalidOption(format!(
"set_io_buffer_size must be in 1..=i32::MAX, got {}",
input.io_buffer_size
))
.into());
}
let mut in_fmt_ctx = avformat_alloc_context();
if in_fmt_ctx.is_null() {
return Err(OpenInputError::OutOfMemory.into());
}
let mut ctx_guard = crate::core::context::FmtCtxGuard::disarmed();
ctx_guard.arm(in_fmt_ctx, crate::raw::Mode::Input);
(*in_fmt_ctx).interrupt_callback = ffmpeg_sys_next::AVIOInterruptCB {
callback: Some(crate::core::context::input_interrupt_cb),
opaque: Arc::as_ptr(interrupt_state) as *mut c_void,
};
let recording_time_us = match input.stop_time_us {
None => input.recording_time_us,
Some(stop_time_us) => {
let start_time_us = input.start_time_us.unwrap_or(0);
if stop_time_us <= start_time_us {
error!(target: LOG_TARGET, "stop_time_us value smaller than start_time_us; aborting.");
return Err(OpenOutputError::InvalidArgument.into());
} else {
Some(stop_time_us - start_time_us)
}
}
};
let file_iformat = if let Some(format) = &input.format {
let format_cstr = CString::new(format.clone())?;
let file_iformat = ffmpeg_sys_next::av_find_input_format(format_cstr.as_ptr());
if file_iformat.is_null() {
error!(target: LOG_TARGET, "Unknown input format: '{format}'");
return Err(OpenInputError::InvalidFormat(format.clone()).into());
}
file_iformat
} else {
null()
};
let input_opts = convert_options(input.input_opts.clone())?;
let mut input_opts = DictGuard::new(hashmap_to_avdictionary(&input_opts));
let mut injected_scan_all_pmts = false;
ctx_guard.release();
match &input.url {
None => {
if input.read_callback.is_none() {
error!(target: LOG_TARGET, "input url and read_callback is none.");
avformat_close_input(&mut in_fmt_ctx);
return Err(OpenInputError::InvalidSource.into());
}
let avio_ctx_buffer_size = input.io_buffer_size;
let mut avio_ctx_buffer = av_malloc(avio_ctx_buffer_size);
if avio_ctx_buffer.is_null() {
avformat_close_input(&mut in_fmt_ctx);
return Err(OpenInputError::OutOfMemory.into());
}
let have_seek_callback = input.seek_callback.is_some();
let input_opaque = Box::new(InputOpaque {
read: input.read_callback.take().unwrap(),
seek: input.seek_callback.take(),
poisoned: false,
});
let opaque = Box::into_raw(input_opaque) as *mut libc::c_void;
let avio_ctx = avio_alloc_context(
avio_ctx_buffer as *mut libc::c_uchar,
avio_ctx_buffer_size as i32,
0,
opaque,
Some(read_packet_wrapper),
None,
if have_seek_callback {
Some(seek_input_packet_wrapper)
} else {
None
},
);
if avio_ctx.is_null() {
av_freep(&mut avio_ctx_buffer as *mut _ as *mut c_void);
let _ = Box::from_raw(opaque as *mut InputOpaque);
avformat_close_input(&mut in_fmt_ctx);
return Err(OpenInputError::OutOfMemory.into());
}
(*in_fmt_ctx).pb = avio_ctx;
(*in_fmt_ctx).flags = AVFMT_FLAG_CUSTOM_IO;
let ret = avformat_open_input(
&mut in_fmt_ctx,
null(),
file_iformat,
input_opts.as_double_ptr(),
);
if ret < 0 {
avformat_close_input(&mut in_fmt_ctx);
crate::core::context::free_input_opaque(avio_ctx);
return Err(OpenInputError::from(ret).into());
}
if let Err(e) = find_stream_info_if_enabled(in_fmt_ctx, input, index) {
avformat_close_input(&mut in_fmt_ctx);
crate::core::context::free_input_opaque(avio_ctx);
return Err(e);
}
if !have_seek_callback && input_requires_seek(in_fmt_ctx) {
avformat_close_input(&mut in_fmt_ctx);
crate::core::context::free_input_opaque(avio_ctx);
warn!(target: LOG_TARGET, "The input format supports seeking, but no seek callback is provided. This may cause issues.");
return Err(OpenInputError::SeekFunctionMissing.into());
}
}
Some(url) => {
ctx_guard.arm(in_fmt_ctx, crate::raw::Mode::Input);
let url_cstr = CString::new(url.as_str())?;
let scan_all_pmts_key = CString::new("scan_all_pmts")?;
if ffmpeg_sys_next::av_dict_get(
input_opts.as_ptr(),
scan_all_pmts_key.as_ptr(),
null(),
ffmpeg_sys_next::AV_DICT_MATCH_CASE,
)
.is_null()
{
let scan_all_pmts_value = CString::new("1")?;
ffmpeg_sys_next::av_dict_set(
input_opts.as_double_ptr(),
scan_all_pmts_key.as_ptr(),
scan_all_pmts_value.as_ptr(),
ffmpeg_sys_next::AV_DICT_DONT_OVERWRITE,
);
injected_scan_all_pmts = true;
};
(*in_fmt_ctx).flags |= ffmpeg_sys_next::AVFMT_FLAG_NONBLOCK;
ctx_guard.release();
let ret = avformat_open_input(
&mut in_fmt_ctx,
url_cstr.as_ptr(),
file_iformat,
input_opts.as_double_ptr(),
);
if ret < 0 {
avformat_close_input(&mut in_fmt_ctx);
return Err(OpenInputError::from(ret).into());
}
if let Err(e) = find_stream_info_if_enabled(in_fmt_ctx, input, index) {
avformat_close_input(&mut in_fmt_ctx);
return Err(e);
}
}
}
let fc = if input.url.is_none() {
crate::raw::FormatContext::from_input_custom_io(in_fmt_ctx)
} else {
crate::raw::FormatContext::from_input(in_fmt_ctx)
};
if (*fc.as_ptr()).nb_streams == 0 {
error!(target: LOG_TARGET, "No streams found in input {index}");
return Err(FindStreamError::NoStreamFound.into());
}
if injected_scan_all_pmts {
input_opts.remove(&CString::new("scan_all_pmts")?);
}
for key in input_opts.leftover_keys() {
warn!(target: LOG_TARGET, "Option '{key}' was not recognized by input {index}");
}
let mut timestamp = input.start_time_us.unwrap_or(0);
let in_fmt_ctx = fc.as_ptr();
if (*in_fmt_ctx).start_time != ffmpeg_sys_next::AV_NOPTS_VALUE {
timestamp += (*in_fmt_ctx).start_time;
}
if let Some(start_time_us) = input.start_time_us {
let mut seek_timestamp = timestamp;
if (*(*in_fmt_ctx).iformat).flags & ffmpeg_sys_next::AVFMT_SEEK_TO_PTS == 0 {
let mut dts_heuristic = false;
let stream_count = (*in_fmt_ctx).nb_streams;
for i in 0..stream_count {
let stream = *(*in_fmt_ctx).streams.add(i as usize);
let par = (*stream).codecpar;
if (*par).video_delay != 0 {
dts_heuristic = true;
break;
}
}
if dts_heuristic {
seek_timestamp -= 3 * AV_TIME_BASE as i64 / 23;
}
}
let ret = ffmpeg_sys_next::avformat_seek_file(
in_fmt_ctx,
-1,
i64::MIN,
seek_timestamp,
seek_timestamp,
0,
);
if ret < 0 {
warn!(target: LOG_TARGET,
"could not seek to position {:.3}",
start_time_us as f64 / AV_TIME_BASE as f64
);
}
}
let url = input
.url
.clone()
.unwrap_or_else(|| format!("read_callback[{index}]"));
let video_codec_opts = convert_options(input.video_codec_opts.clone())?;
let audio_codec_opts = convert_options(input.audio_codec_opts.clone())?;
let subtitle_codec_opts = convert_options(input.subtitle_codec_opts.clone())?;
let demux = Demuxer::new(
url,
fc,
0 - if copy_ts { 0 } else { timestamp },
input.frame_pipelines.take(),
input.video_codec.clone(),
input.audio_codec.clone(),
input.subtitle_codec.clone(),
video_codec_opts,
audio_codec_opts,
subtitle_codec_opts,
input.readrate,
input.start_time_us,
recording_time_us,
input.exit_on_error,
input.stream_loop,
input.hwaccel.clone(),
input.hwaccel_device.clone(),
input.hwaccel_output_format.clone(),
copy_ts,
input.autorotate.unwrap_or(true), input.ts_scale.unwrap_or(1.0), match input.framerate {
Some((num, den)) => AVRational { num, den },
None => AVRational { num: 0, den: 0 },
},
input.log_level_offset.unwrap_or(0),
)?;
Ok(demux)
}
#[cfg(not(docsrs))]
struct FindStreamInfoOptions {
dicts: Vec<*mut ffmpeg_sys_next::AVDictionary>,
}
#[cfg(not(docsrs))]
impl FindStreamInfoOptions {
fn new(
nb_streams: usize,
configured: Option<&HashMap<usize, HashMap<String, String>>>,
) -> Result<Self> {
let mut this = Self {
dicts: vec![null_mut(); nb_streams],
};
if let Some(configured) = configured {
for (&stream_index, opts) in configured {
if stream_index >= nb_streams {
error!(target: LOG_TARGET,
"find_stream_info codec opts reference stream {stream_index}, \
but the input only has {nb_streams} stream(s)"
);
return Err(FindStreamError::InvalidArgument.into());
}
for (key, value) in opts {
let key = CString::new(key.as_str())?;
let value = CString::new(value.as_str())?;
unsafe {
ffmpeg_sys_next::av_dict_set(
&mut this.dicts[stream_index],
key.as_ptr(),
value.as_ptr(),
0,
);
}
}
}
}
Ok(this)
}
fn as_mut_ptr_or_null(&mut self) -> *mut *mut ffmpeg_sys_next::AVDictionary {
if self.dicts.is_empty() {
null_mut()
} else {
self.dicts.as_mut_ptr()
}
}
fn leftover_keys(&self) -> Vec<(usize, String)> {
let mut keys = Vec::new();
for (stream_index, dict) in self.dicts.iter().enumerate() {
let mut entry = null();
unsafe {
loop {
entry = ffmpeg_sys_next::av_dict_iterate(*dict, entry);
if entry.is_null() {
break;
}
keys.push((
stream_index,
CStr::from_ptr((*entry).key).to_string_lossy().into_owned(),
));
}
}
}
keys
}
}
#[cfg(not(docsrs))]
impl Drop for FindStreamInfoOptions {
fn drop(&mut self) {
for dict in &mut self.dicts {
unsafe { ffmpeg_sys_next::av_dict_free(dict) };
}
}
}
#[cfg(not(docsrs))]
unsafe fn find_stream_info_if_enabled(
in_fmt_ctx: *mut AVFormatContext,
input: &Input,
index: usize,
) -> Result<()> {
if !input.find_stream_info {
if input.find_stream_info_codec_opts.is_some() {
warn!(target: LOG_TARGET,
"find_stream_info is disabled for input {index}; \
ignoring its find_stream_info codec opts"
);
}
return Ok(());
}
let mut probe_opts = FindStreamInfoOptions::new(
(*in_fmt_ctx).nb_streams as usize,
input.find_stream_info_codec_opts.as_ref(),
)?;
let ret = avformat_find_stream_info(in_fmt_ctx, probe_opts.as_mut_ptr_or_null());
if ret < 0 {
return Err(FindStreamError::from(ret).into());
}
for (stream_index, key) in probe_opts.leftover_keys() {
if matches!(key.as_str(), "threads" | "lowres" | "codec_whitelist") {
continue;
}
warn!(target: LOG_TARGET,
"Option '{key}' was not recognized while probing stream {stream_index} of input {index}"
);
}
Ok(())
}
#[cfg(all(test, not(docsrs)))]
mod find_stream_info_tests {
use super::FindStreamInfoOptions;
use crate::error::{Error, FindStreamError};
use std::collections::HashMap;
use std::ffi::{CStr, CString};
fn probing_opts(
entries: &[(usize, &[(&str, &str)])],
) -> HashMap<usize, HashMap<String, String>> {
entries
.iter()
.map(|(index, pairs)| {
(
*index,
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
)
})
.collect()
}
fn dict_value(dict: *mut ffmpeg_sys_next::AVDictionary, key: &str) -> Option<String> {
let key = CString::new(key).unwrap();
unsafe {
let entry = ffmpeg_sys_next::av_dict_get(
dict,
key.as_ptr(),
std::ptr::null(),
ffmpeg_sys_next::AV_DICT_MATCH_CASE,
);
if entry.is_null() {
None
} else {
Some(
CStr::from_ptr((*entry).value)
.to_string_lossy()
.into_owned(),
)
}
}
}
#[test]
fn no_configured_opts_yields_all_null_dicts() {
let mut opts = FindStreamInfoOptions::new(3, None).unwrap();
assert_eq!(opts.dicts.len(), 3);
assert!(opts.dicts.iter().all(|dict| dict.is_null()));
assert!(!opts.as_mut_ptr_or_null().is_null());
assert!(opts.leftover_keys().is_empty());
}
#[test]
fn zero_streams_passes_null_array() {
let mut opts = FindStreamInfoOptions::new(0, None).unwrap();
assert!(opts.as_mut_ptr_or_null().is_null());
}
#[test]
fn sparse_indices_leave_null_dicts_for_missing_streams() {
let configured = probing_opts(&[
(0, &[("skip_frame", "nokey")]),
(2, &[("lowres", "1"), ("skip_frame", "all")]),
]);
let opts = FindStreamInfoOptions::new(3, Some(&configured)).unwrap();
assert!(!opts.dicts[0].is_null());
assert!(
opts.dicts[1].is_null(),
"unconfigured stream must stay null"
);
assert!(!opts.dicts[2].is_null());
assert_eq!(
dict_value(opts.dicts[0], "skip_frame").as_deref(),
Some("nokey")
);
assert_eq!(dict_value(opts.dicts[2], "lowres").as_deref(), Some("1"));
assert_eq!(
dict_value(opts.dicts[2], "skip_frame").as_deref(),
Some("all")
);
let mut leftovers = opts.leftover_keys();
leftovers.sort();
assert_eq!(
leftovers,
vec![
(0, "skip_frame".to_string()),
(2, "lowres".to_string()),
(2, "skip_frame".to_string()),
]
);
drop(opts);
}
#[test]
fn out_of_range_stream_index_is_invalid_argument() {
let configured = probing_opts(&[(0, &[("skip_frame", "nokey")]), (5, &[("lowres", "1")])]);
let err = match FindStreamInfoOptions::new(2, Some(&configured)) {
Ok(_) => panic!("expected InvalidArgument for out-of-range stream index"),
Err(err) => err,
};
assert!(
matches!(err, Error::FindStream(FindStreamError::InvalidArgument)),
"expected InvalidArgument, got {err:?}"
);
}
#[test]
fn drop_after_partial_build_is_leak_free() {
let configured = probing_opts(&[(0, &[("skip_frame", "nokey")])]);
let opts = FindStreamInfoOptions::new(1, Some(&configured)).unwrap();
assert!(!opts.dicts[0].is_null());
drop(opts);
}
}
unsafe fn input_requires_seek(fmt_ctx: *mut AVFormatContext) -> bool {
if fmt_ctx.is_null() {
return false;
}
let mut format_name = "unknown".to_string();
let mut format_names: Vec<&str> = Vec::with_capacity(0);
if !(*fmt_ctx).iformat.is_null() {
let iformat = (*fmt_ctx).iformat;
format_name = CStr::from_ptr((*iformat).name)
.to_string_lossy()
.into_owned();
let flags = (*iformat).flags;
let no_binsearch = flags & AVFMT_NOBINSEARCH != 0;
let no_gensearch = flags & AVFMT_NOGENSEARCH != 0;
log::debug!(target: LOG_TARGET,
"Input format '{format_name}' - Binary search: {}, Generic search: {}",
if no_binsearch { "Disabled" } else { "Enabled" },
if no_gensearch { "Disabled" } else { "Enabled" }
);
format_names = format_name.split(',').collect();
if format_names.iter().any(|&f| {
matches!(
f,
"mp4" | "mkv" | "avi" | "mov" | "flac" | "wav" | "aac" | "ogg" | "mp3" | "webm"
)
}) && !no_binsearch
&& !no_gensearch
{
return true;
}
if format_names.iter().any(|&f| {
matches!(
f,
"hls" | "m3u8" | "mpegts" | "mms" | "udp" | "rtp" | "rtp_mpegts" | "http" | "srt"
)
}) {
log::debug!(target: LOG_TARGET, "Live stream detected ({format_name}). Seeking is not possible.");
return false;
}
if no_binsearch && no_gensearch {
log::debug!(target: LOG_TARGET, "Input format '{format_name}' has both NOBINSEARCH and NOGENSEARCH set. Seeking is likely restricted.");
}
}
let format_duration = (*fmt_ctx).duration;
if format_names.contains(&"flv") {
if format_duration <= 0 {
log::debug!(target: LOG_TARGET,
"Input format 'flv' detected with no valid duration. Seeking is not possible."
);
} else {
log::warn!(target: LOG_TARGET, "Input format 'flv' detected with a valid duration. While seeking may still be possible, it is highly recommended to add a `seek_callback()` for optimal input handling, especially when seeking or random access to specific segments is required.");
}
return false;
}
if format_duration > 0 {
log::debug!(target: LOG_TARGET, "Format '{format_name}' has a duration of {format_duration}. Seeking is likely possible.");
return true;
}
let mut video_stream_index = -1;
for i in 0..(*fmt_ctx).nb_streams {
let stream = *(*fmt_ctx).streams.offset(i as isize);
if (*stream).codecpar.is_null() {
continue;
}
if (*(*stream).codecpar).codec_type == AVMEDIA_TYPE_VIDEO {
video_stream_index = i as i32;
break;
}
}
let stream_index = if video_stream_index >= 0 {
video_stream_index
} else {
-1
};
let original_pos = if !(*fmt_ctx).pb.is_null() {
(*(*fmt_ctx).pb).pos
} else {
-1
};
if original_pos >= 0 {
let seek_target = AV_TIME_BASE as i64;
let seek_result = av_seek_frame(fmt_ctx, stream_index, seek_target, AVSEEK_FLAG_BACKWARD);
if seek_result >= 0 {
log::debug!(target: LOG_TARGET, "Seek test successful.");
(*(*fmt_ctx).pb).pos = original_pos;
avformat_flush(fmt_ctx);
log::debug!(target: LOG_TARGET, "Restored fmt_ctx.pb.pos to {original_pos} and flushed format context.",);
return true;
} else {
log::debug!(target: LOG_TARGET, "Seek test failed (return code {seek_result}). This format likely does not support seeking.");
}
}
false
}