#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::doc_markdown)]
use std::path::Path;
use crate::error::RemuxError;
pub(crate) fn run_audio_replacement(
video_input: &Path,
audio_input: &Path,
output: &Path,
) -> Result<(), RemuxError> {
let mut vid_ctx = ff_sys::InputFormatContext::open(video_input)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
vid_ctx
.find_stream_info()
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
let Some(video_stream_idx) = vid_ctx
.streams()
.find(|s| s.codecpar().codec_type() == ff_sys::AVMediaType_AVMEDIA_TYPE_VIDEO)
.map(|s| s.index() as usize)
else {
return Err(RemuxError::OperationFailed {
reason: format!(
"no video stream found in video input path={}",
video_input.display()
),
});
};
let mut aud_ctx = ff_sys::InputFormatContext::open(audio_input)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
aud_ctx
.find_stream_info()
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
let Some(audio_stream_idx) = aud_ctx
.streams()
.find(|s| s.codecpar().codec_type() == ff_sys::AVMediaType_AVMEDIA_TYPE_AUDIO)
.map(|s| s.index() as usize)
else {
return Err(RemuxError::OperationFailed {
reason: format!(
"no audio stream found in audio input path={}",
audio_input.display()
),
});
};
let mut out_ctx = ff_sys::OutputFormatContext::new(None, output)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
let vid_out_idx = out_ctx.new_stream(None).map_err(|_| RemuxError::Ffmpeg {
code: 0,
message: "avformat_new_stream failed for video".to_string(),
})?;
let vid_in_tb = {
let vid_in =
vid_ctx
.stream(video_stream_idx)
.ok_or_else(|| RemuxError::OperationFailed {
reason: "video input stream missing".to_string(),
})?;
out_ctx
.copy_stream_params(vid_out_idx, vid_in.codecpar())
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
vid_in.time_base()
};
let aud_out_idx = out_ctx.new_stream(None).map_err(|_| RemuxError::Ffmpeg {
code: 0,
message: "avformat_new_stream failed for audio".to_string(),
})?;
let aud_in_tb = {
let aud_in =
aud_ctx
.stream(audio_stream_idx)
.ok_or_else(|| RemuxError::OperationFailed {
reason: "audio input stream missing".to_string(),
})?;
out_ctx
.copy_stream_params(aud_out_idx, aud_in.codecpar())
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
aud_in.time_base()
};
out_ctx
.open_io(output)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
out_ctx
.write_header()
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
let vid_out_tb = out_ctx.stream_time_base(vid_out_idx);
let aud_out_tb = out_ctx.stream_time_base(aud_out_idx);
log::debug!(
"audio replacement header written \
video_stream_idx={video_stream_idx} audio_stream_idx={audio_stream_idx}"
);
let Ok(mut pkt) = ff_sys::Packet::new() else {
let _ = out_ctx.write_trailer();
return Err(RemuxError::Ffmpeg {
code: 0,
message: "av_packet_alloc failed".to_string(),
});
};
let mut loop_err: Option<RemuxError> = None;
let mut vid_eof = false;
let mut aud_eof = false;
'copy: loop {
if !vid_eof {
match vid_ctx.read_frame(&mut pkt) {
Err(e) if e.is_eof() => {
vid_eof = true;
}
Err(e) => {
loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break 'copy;
}
Ok(()) => {
if pkt.stream_index() as usize == video_stream_idx {
pkt.rescale_ts(vid_in_tb, vid_out_tb);
pkt.set_stream_index(0);
let write_res = out_ctx.write_interleaved(&mut pkt);
pkt.unref();
if let Err(e) = write_res {
loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break 'copy;
}
} else {
pkt.unref();
}
}
}
}
if !aud_eof {
match aud_ctx.read_frame(&mut pkt) {
Err(e) if e.is_eof() => {
aud_eof = true;
}
Err(e) => {
loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break 'copy;
}
Ok(()) => {
if pkt.stream_index() as usize == audio_stream_idx {
pkt.rescale_ts(aud_in_tb, aud_out_tb);
pkt.set_stream_index(1);
let write_res = out_ctx.write_interleaved(&mut pkt);
pkt.unref();
if let Err(e) = write_res {
loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break 'copy;
}
} else {
pkt.unref();
}
}
}
}
if vid_eof && aud_eof {
break 'copy;
}
}
let _ = out_ctx.write_trailer();
log::info!("audio replaced output={}", output.display());
match loop_err {
Some(e) => Err(e),
None => Ok(()),
}
}
pub(crate) fn run_audio_extraction(
input: &Path,
output: &Path,
requested_idx: Option<usize>,
) -> Result<(), RemuxError> {
let mut in_ctx = ff_sys::InputFormatContext::open(input)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
in_ctx
.find_stream_info()
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
let nb_streams = in_ctx.nb_streams() as usize;
let audio_stream_idx = if let Some(idx) = requested_idx {
if idx >= nb_streams {
return Err(RemuxError::OperationFailed {
reason: format!("stream index {idx} out of range (input has {nb_streams} streams)"),
});
}
let is_audio = in_ctx
.stream(idx)
.is_some_and(|s| s.codecpar().codec_type() == ff_sys::AVMediaType_AVMEDIA_TYPE_AUDIO);
if !is_audio {
return Err(RemuxError::OperationFailed {
reason: format!("stream index {idx} is not an audio stream"),
});
}
idx
} else {
let Some(idx) = in_ctx
.streams()
.find(|s| s.codecpar().codec_type() == ff_sys::AVMediaType_AVMEDIA_TYPE_AUDIO)
.map(|s| s.index() as usize)
else {
return Err(RemuxError::OperationFailed {
reason: format!("no audio stream found in input path={}", input.display()),
});
};
idx
};
let mut out_ctx = ff_sys::OutputFormatContext::new(None, output)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
let out_idx = out_ctx.new_stream(None).map_err(|_| RemuxError::Ffmpeg {
code: 0,
message: "avformat_new_stream failed".to_string(),
})?;
let in_tb = {
let in_stream =
in_ctx
.stream(audio_stream_idx)
.ok_or_else(|| RemuxError::OperationFailed {
reason: "audio input stream missing".to_string(),
})?;
out_ctx
.copy_stream_params(out_idx, in_stream.codecpar())
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
in_stream.time_base()
};
out_ctx
.open_io(output)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
out_ctx
.write_header()
.map_err(|e| RemuxError::OperationFailed {
reason: format!(
"codec incompatible with output container: {}",
ff_sys::av_error_string(e.code())
),
})?;
let out_tb = out_ctx.stream_time_base(out_idx);
log::debug!(
"audio extraction header written audio_stream_idx={audio_stream_idx} \
output={}",
output.display()
);
let Ok(mut pkt) = ff_sys::Packet::new() else {
let _ = out_ctx.write_trailer();
return Err(RemuxError::Ffmpeg {
code: 0,
message: "av_packet_alloc failed".to_string(),
});
};
let mut loop_err: Option<RemuxError> = None;
'read: loop {
match in_ctx.read_frame(&mut pkt) {
Err(e) if e.is_eof() => break 'read,
Err(e) => {
loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break 'read;
}
Ok(()) => {}
}
if pkt.stream_index() as usize != audio_stream_idx {
pkt.unref();
continue 'read;
}
pkt.rescale_ts(in_tb, out_tb);
pkt.set_stream_index(0);
let write_res = out_ctx.write_interleaved(&mut pkt);
pkt.unref();
if let Err(e) = write_res {
loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break 'read;
}
}
let _ = out_ctx.write_trailer();
log::info!(
"audio extracted output={} stream_index={audio_stream_idx}",
output.display()
);
match loop_err {
Some(e) => Err(e),
None => Ok(()),
}
}
pub(crate) fn run_audio_addition(
video_input: &Path,
audio_input: &Path,
output: &Path,
loop_audio: bool,
) -> Result<(), RemuxError> {
let mut vid_ctx = ff_sys::InputFormatContext::open(video_input)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
vid_ctx
.find_stream_info()
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
let Some(video_stream_idx) = vid_ctx
.streams()
.find(|s| s.codecpar().codec_type() == ff_sys::AVMediaType_AVMEDIA_TYPE_VIDEO)
.map(|s| s.index() as usize)
else {
return Err(RemuxError::OperationFailed {
reason: format!(
"no video stream found in video input path={}",
video_input.display()
),
});
};
let mut aud_ctx = ff_sys::InputFormatContext::open(audio_input)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
aud_ctx
.find_stream_info()
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
let Some(audio_stream_idx) = aud_ctx
.streams()
.find(|s| s.codecpar().codec_type() == ff_sys::AVMediaType_AVMEDIA_TYPE_AUDIO)
.map(|s| s.index() as usize)
else {
return Err(RemuxError::OperationFailed {
reason: format!(
"no audio stream found in audio input path={}",
audio_input.display()
),
});
};
let vid_duration_us = vid_ctx.duration();
let aud_duration_us = aud_ctx.duration();
let should_loop = loop_audio
&& vid_duration_us > 0
&& aud_duration_us > 0
&& aud_duration_us < vid_duration_us;
let mut out_ctx = ff_sys::OutputFormatContext::new(None, output)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
let vid_out_idx = out_ctx.new_stream(None).map_err(|_| RemuxError::Ffmpeg {
code: 0,
message: "avformat_new_stream failed for video".to_string(),
})?;
let vid_in_tb = {
let vid_in =
vid_ctx
.stream(video_stream_idx)
.ok_or_else(|| RemuxError::OperationFailed {
reason: "video input stream missing".to_string(),
})?;
out_ctx
.copy_stream_params(vid_out_idx, vid_in.codecpar())
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
vid_in.time_base()
};
let aud_out_idx = out_ctx.new_stream(None).map_err(|_| RemuxError::Ffmpeg {
code: 0,
message: "avformat_new_stream failed for audio".to_string(),
})?;
let aud_in_tb = {
let aud_in =
aud_ctx
.stream(audio_stream_idx)
.ok_or_else(|| RemuxError::OperationFailed {
reason: "audio input stream missing".to_string(),
})?;
out_ctx
.copy_stream_params(aud_out_idx, aud_in.codecpar())
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
aud_in.time_base()
};
out_ctx
.open_io(output)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
out_ctx
.write_header()
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
let vid_out_tb = out_ctx.stream_time_base(vid_out_idx);
let aud_out_tb = out_ctx.stream_time_base(aud_out_idx);
let aud_loop_duration_in_tb: i64 = aud_ctx
.stream(audio_stream_idx)
.map_or(0, |s| s.duration().max(0));
log::debug!(
"audio addition header written should_loop={should_loop} \
video_stream_idx={video_stream_idx} audio_stream_idx={audio_stream_idx}"
);
let Ok(mut pkt) = ff_sys::Packet::new() else {
let _ = out_ctx.write_trailer();
return Err(RemuxError::Ffmpeg {
code: 0,
message: "av_packet_alloc failed".to_string(),
});
};
let mut add_loop_err: Option<RemuxError> = None;
let mut vid_eof = false;
let mut aud_eof = false;
let mut aud_pts_offset_in_tb: i64 = 0;
'copy: loop {
if !vid_eof {
match vid_ctx.read_frame(&mut pkt) {
Err(e) if e.is_eof() => {
vid_eof = true;
}
Err(e) => {
add_loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break 'copy;
}
Ok(()) => {
if pkt.stream_index() as usize == video_stream_idx {
pkt.rescale_ts(vid_in_tb, vid_out_tb);
pkt.set_stream_index(0);
let write_res = out_ctx.write_interleaved(&mut pkt);
pkt.unref();
if let Err(e) = write_res {
add_loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break 'copy;
}
} else {
pkt.unref();
}
}
}
}
if vid_eof {
break 'copy;
}
if !aud_eof {
match aud_ctx.read_frame(&mut pkt) {
Err(e) if e.is_eof() => {
if should_loop {
let _ = aud_ctx.seek_frame(
audio_stream_idx as i32,
0,
ff_sys::avformat::seek_flags::BACKWARD,
);
aud_pts_offset_in_tb += aud_loop_duration_in_tb;
} else {
aud_eof = true;
}
}
Err(e) => {
add_loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break 'copy;
}
Ok(()) => {
if pkt.stream_index() as usize == audio_stream_idx {
if pkt.pts() != ff_sys::AV_NOPTS_VALUE {
pkt.set_pts(pkt.pts() + aud_pts_offset_in_tb);
}
if pkt.dts() != ff_sys::AV_NOPTS_VALUE {
pkt.set_dts(pkt.dts() + aud_pts_offset_in_tb);
}
pkt.rescale_ts(aud_in_tb, aud_out_tb);
pkt.set_stream_index(1);
let write_res = out_ctx.write_interleaved(&mut pkt);
pkt.unref();
if let Err(e) = write_res {
add_loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break 'copy;
}
} else {
pkt.unref();
}
}
}
}
}
let _ = out_ctx.write_trailer();
log::info!(
"audio added output={} loop_audio={loop_audio}",
output.display()
);
match add_loop_err {
Some(e) => Err(e),
None => Ok(()),
}
}