use super::*;
pub(super) fn open_output_files(
outputs: &mut Vec<Output>,
copy_ts: bool,
interrupt_state: &Arc<crate::core::context::InterruptState>,
) -> Result<Vec<Muxer>> {
let mut muxs = Vec::new();
for (i, output) in outputs.iter_mut().enumerate() {
unsafe {
let result = open_output_file(i, output, copy_ts, interrupt_state);
if let Err(e) = result {
return Err(e);
}
let mux = result.unwrap();
muxs.push(mux)
}
}
Ok(muxs)
}
#[cfg(docsrs)]
unsafe fn open_output_file(
index: usize,
output: &mut Output,
copy_ts: bool,
interrupt_state: &Arc<crate::core::context::InterruptState>,
) -> Result<Muxer> {
Err(Error::Bug)
}
#[cfg(not(docsrs))]
unsafe fn open_output_file(
index: usize,
output: &mut Output,
copy_ts: bool,
interrupt_state: &Arc<crate::core::context::InterruptState>,
) -> Result<Muxer> {
for (name, rate) in [
("set_framerate", output.framerate),
("set_framerate_max", output.framerate_max),
] {
if let Some(AVRational { num, den }) = rate {
if num <= 0 || den <= 0 {
return Err(OpenOutputError::InvalidOption(format!(
"{name} requires positive numerator and denominator, got {num}/{den}"
))
.into());
}
}
}
if output.io_buffer_size == 0 || output.io_buffer_size > i32::MAX as usize {
return Err(OpenOutputError::InvalidOption(format!(
"set_io_buffer_size must be in 1..=i32::MAX, got {}",
output.io_buffer_size
))
.into());
}
if output.max_muxing_queue_size == 0 {
return Err(
OpenOutputError::InvalidOption("set_max_muxing_queue_size must be > 0".into()).into(),
);
}
if output.muxing_queue_data_threshold == 0 {
return Err(OpenOutputError::InvalidOption(
"set_muxing_queue_data_threshold must be > 0".into(),
)
.into());
}
let mut out_fmt_ctx = null_mut();
let mut ctx_guard = crate::core::context::FmtCtxGuard::disarmed();
let format = get_format(&output.format)?;
match &output.url {
None => {
if output.write_callback.is_none() {
error!(target: LOG_TARGET, "input url and write_callback is none.");
return Err(OpenOutputError::InvalidSink.into());
}
let write_callback = output.write_callback.take().unwrap();
let avio_ctx_buffer_size = output.io_buffer_size;
let mut avio_ctx_buffer = av_malloc(avio_ctx_buffer_size);
if avio_ctx_buffer.is_null() {
return Err(OpenOutputError::OutOfMemory.into());
}
let have_seek_callback = output.seek_callback.is_some();
let input_opaque = Box::new(OutputOpaque {
write: write_callback,
seek: output.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,
1,
opaque,
None,
Some(write_packet_wrapper),
if have_seek_callback {
Some(seek_output_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 OutputOpaque);
return Err(OpenOutputError::OutOfMemory.into());
}
let ret = avformat_alloc_output_context2(&mut out_fmt_ctx, format, null(), null());
if out_fmt_ctx.is_null() {
warn!(target: LOG_TARGET, "Error initializing the muxer for write_callback");
crate::core::context::free_output_opaque(avio_ctx);
return Err(AllocOutputContextError::from(ret).into());
}
if !have_seek_callback && output_requires_seek(out_fmt_ctx) {
crate::core::context::free_output_opaque(avio_ctx);
avformat_free_context(out_fmt_ctx);
warn!(target: LOG_TARGET, "The output format supports seeking, but no seek callback is provided. This may cause issues.");
return Err(OpenOutputError::SeekFunctionMissing.into());
}
(*out_fmt_ctx).pb = avio_ctx;
(*out_fmt_ctx).flags |= AVFMT_FLAG_CUSTOM_IO;
ctx_guard.arm(out_fmt_ctx, crate::raw::Mode::OutputCustomIo);
}
Some(url) => {
let url_cstr = if url == "-" {
CString::new("pipe:")?
} else {
CString::new(url.as_str())?
};
let ret =
avformat_alloc_output_context2(&mut out_fmt_ctx, format, null(), url_cstr.as_ptr());
if out_fmt_ctx.is_null() {
warn!(target: LOG_TARGET, "Error initializing the muxer for {url}");
return Err(AllocOutputContextError::from(ret).into());
}
ctx_guard.arm(out_fmt_ctx, crate::raw::Mode::Output);
(*out_fmt_ctx).interrupt_callback = ffmpeg_sys_next::AVIOInterruptCB {
callback: Some(crate::core::context::output_interrupt_cb),
opaque: Arc::as_ptr(interrupt_state) as *mut c_void,
};
}
}
let recording_time_us = match output.stop_time_us {
None => output.recording_time_us,
Some(stop_time_us) => {
let start_time_us = output.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 url = output
.url
.clone()
.unwrap_or_else(|| format!("write_callback[{index}]"));
let video_codec_opts = convert_options(output.video_codec_opts.clone())?;
let audio_codec_opts = convert_options(output.audio_codec_opts.clone())?;
let subtitle_codec_opts = convert_options(output.subtitle_codec_opts.clone())?;
let format_opts = convert_options(output.format_opts.clone())?;
let format_opts = maybe_enable_image2_update(
out_fmt_ctx,
output.url.as_deref(),
output.max_video_frames,
format_opts,
);
let pix_fmt = match &output.pix_fmt {
Some(fmt_str) => {
let cstr = CString::new(fmt_str.as_str())?;
let pf = av_get_pix_fmt(cstr.as_ptr());
if pf == AV_PIX_FMT_NONE {
return Err(OpenOutputError::UnknownPixelFormat(fmt_str.clone()).into());
} else {
Some(pf)
}
}
None => None,
};
let forced_kf_pts = match &output.forced_kf_spec {
Some(spec) => Some(
crate::core::context::output::parse_forced_key_frames(spec)
.map_err(OpenOutputError::InvalidOption)?,
),
None => None,
};
let audio_sample_fmt = match &output.audio_sample_fmt {
Some(name) => {
let cstr = CString::new(name.as_str())
.map_err(|_| OpenOutputError::UnknownSampleFormat(name.clone()))?;
let sf = ffmpeg_sys_next::av_get_sample_fmt(cstr.as_ptr());
if sf == ffmpeg_sys_next::AVSampleFormat::AV_SAMPLE_FMT_NONE {
return Err(OpenOutputError::UnknownSampleFormat(name.clone()).into());
}
Some(sf)
}
None => None,
};
let bsf_chains = crate::core::context::muxer::StreamBsfChains {
video: output.video_bsf.as_deref().map(CString::new).transpose()?,
audio: output.audio_bsf.as_deref().map(CString::new).transpose()?,
subtitle: output
.subtitle_bsf
.as_deref()
.map(CString::new)
.transpose()?,
};
let out_fmt_ctx = ctx_guard.release();
let fc = if output.url.is_none() {
crate::raw::FormatContext::from_output_custom_io(out_fmt_ctx)
} else {
crate::raw::FormatContext::from_output(out_fmt_ctx)
};
let mux = Muxer::new(
url,
fc,
output.frame_pipelines.take(),
output.stream_map_specs.clone(),
output.stream_maps.clone(),
output.video_codec.clone(),
output.audio_codec.clone(),
output.subtitle_codec.clone(),
bsf_chains,
output.start_time_us,
recording_time_us,
output.shortest,
output.shortest_buf_duration_us,
output.framerate,
output.framerate_max,
output.vsync_method,
output.bits_per_raw_sample,
output.audio_sample_rate,
output.audio_channels,
audio_sample_fmt,
output.video_qscale,
output.audio_qscale,
forced_kf_pts,
output.max_video_frames,
output.max_audio_frames,
output.max_subtitle_frames,
video_codec_opts,
audio_codec_opts,
subtitle_codec_opts,
format_opts,
copy_ts,
output.global_metadata.clone(),
output.stream_metadata.clone(),
output.chapter_metadata.clone(),
output.program_metadata.clone(),
output.metadata_map.clone(),
output.auto_copy_metadata,
output.video_disable,
output.audio_disable,
output.subtitle_disable,
output.data_disable,
pix_fmt,
crate::core::context::pre_mux_queue::PreMuxQueueConfig {
max_packets: output.max_muxing_queue_size,
data_threshold: output.muxing_queue_data_threshold,
},
output.sws_opts.clone(),
output.swr_opts.clone(),
std::mem::take(&mut output.attachments),
interrupt_state.clone(),
);
Ok(mux)
}
fn get_format(format_option: &Option<String>) -> Result<*const AVOutputFormat> {
match format_option {
None => Ok(null()),
Some(format_str) => unsafe {
let mut format_cstr = CString::new(format_str.to_string())?;
let mut format = av_guess_format(format_cstr.as_ptr(), null(), null());
if format.is_null() {
format_cstr = CString::new(format!("tmp.{format_str}"))?;
format = av_guess_format(null(), format_cstr.as_ptr(), null());
}
if format.is_null() {
return Err(OpenOutputError::FormatUnsupported(format_str.to_string()).into());
}
Ok(format)
},
}
}
unsafe fn output_requires_seek(fmt_ctx: *mut AVFormatContext) -> bool {
if fmt_ctx.is_null() {
return false;
}
let mut format_name = "unknown".to_string();
if !(*fmt_ctx).oformat.is_null() {
let oformat = (*fmt_ctx).oformat;
format_name = CStr::from_ptr((*oformat).name)
.to_string_lossy()
.into_owned();
let flags = (*oformat).flags;
let no_file = flags & AVFMT_NOFILE != 0;
let global_header = flags & AVFMT_GLOBALHEADER != 0;
log::debug!(target: LOG_TARGET,
"Output format '{format_name}' - No file: {}, Global header: {}",
if no_file { "True" } else { "False" },
if global_header { "True" } else { "False" }
);
let format_names: Vec<&str> = format_name.split(',').collect();
if format_names
.iter()
.any(|&f| matches!(f, "mp4" | "mov" | "mkv" | "avi" | "flac" | "ogg" | "webm"))
{
log::debug!(target: LOG_TARGET, "Output format '{format_name}' typically requires seeking.");
return true;
}
if format_names.iter().any(|&f| {
matches!(
f,
"mpegts" | "hls" | "m3u8" | "udp" | "rtp" | "rtp_mpegts" | "http" | "srt"
)
}) {
log::debug!(target: LOG_TARGET, "Output format '{format_name}' does not typically require seeking.");
return false;
}
if format_name == "flv" {
log::debug!(target: LOG_TARGET, "Output format 'flv' detected. It is highly recommended to set `seek_callback()` to avoid potential issues with 'Failed to update header with correct duration' and 'Failed to update header with correct filesize'.");
return false;
}
if no_file {
log::debug!(target: LOG_TARGET,
"Output format '{format_name}' uses AVFMT_NOFILE. Seeking is likely unnecessary."
);
return false;
}
if global_header {
log::debug!(target: LOG_TARGET,
"Output format '{format_name}' uses AVFMT_GLOBALHEADER. Seeking may be required."
);
return true;
}
} else {
log::debug!(target: LOG_TARGET, "Output format is null. Cannot determine if seeking is required.");
}
log::debug!(target: LOG_TARGET, "Output format '{format_name}' does not match any known rules. Assuming seeking is not required.");
false
}
pub(in crate::core::context) struct OutputOpaque {
pub(super) write: Box<dyn FnMut(&[u8]) -> i32 + Send>,
pub(super) seek: Option<Box<dyn FnMut(i64, i32) -> i64 + Send>>,
pub(super) poisoned: bool,
}
pub(super) unsafe extern "C" fn write_packet_wrapper(
opaque: *mut libc::c_void,
buf: *const 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 OutputOpaque);
if context.poisoned {
return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO);
}
let slice = std::slice::from_raw_parts(buf, buf_size as usize);
let mut written = 0usize;
while written < slice.len() {
let remaining = &slice[written..];
let ret = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
(context.write)(remaining)
})) {
Ok(ret) => ret,
Err(_) => {
context.poisoned = true;
return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO);
}
};
if ret < 0 {
return ret;
}
if ret == 0 || ret as usize > remaining.len() {
return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO);
}
written += ret as usize;
}
buf_size
}
unsafe extern "C" fn seek_output_packet_wrapper(
opaque: *mut libc::c_void,
offset: i64,
whence: libc::c_int,
) -> i64 {
let context = &mut *(opaque as *mut OutputOpaque);
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
}
}
fn maybe_enable_image2_update(
out_fmt_ctx: *mut AVFormatContext,
url: Option<&str>,
max_video_frames: Option<i64>,
format_opts: Option<HashMap<CString, CString>>,
) -> Option<HashMap<CString, CString>> {
if max_video_frames != Some(1) {
return format_opts;
}
let Some(url) = url else {
return format_opts;
};
if url.contains('%') {
return format_opts;
}
let is_image2 = unsafe {
let oformat = (*out_fmt_ctx).oformat;
!oformat.is_null()
&& !(*oformat).name.is_null()
&& std::ffi::CStr::from_ptr((*oformat).name).to_bytes() == b"image2"
};
if !is_image2 {
return format_opts;
}
let mut opts = format_opts.unwrap_or_default();
let user_configured = opts
.keys()
.any(|key| matches!(key.to_bytes(), b"update" | b"strftime" | b"frame_pts"));
if !user_configured {
info!(target: LOG_TARGET, "single-image output detected (max_video_frames=1): enabling image2 'update' mode");
opts.insert(CString::new("update").unwrap(), CString::new("1").unwrap());
}
Some(opts)
}