#![allow(clippy::cast_precision_loss)]
#![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;
const AV_TIME_BASE: i64 = 1_000_000;
#[derive(Debug, Default, Clone)]
pub(crate) struct BsfSpec {
pub(crate) video: Option<String>,
pub(crate) audio: Option<String>,
}
impl BsfSpec {
fn for_media_type(&self, codec_type: ff_sys::AVMediaType) -> Option<&str> {
if codec_type == ff_sys::AVMediaType_AVMEDIA_TYPE_VIDEO {
self.video.as_deref()
} else if codec_type == ff_sys::AVMediaType_AVMEDIA_TYPE_AUDIO {
self.audio.as_deref()
} else {
None
}
}
}
pub(crate) fn run_trim(
input: &Path,
output: &Path,
start_sec: f64,
end_sec: f64,
bsf: &BsfSpec,
) -> 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 mut out_ctx = ff_sys::OutputFormatContext::new(None, output)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
let nb_streams = in_ctx.nb_streams() as usize;
let mut filters: Vec<Option<ff_sys::BsfContext>> = Vec::with_capacity(nb_streams);
for i in 0..nb_streams {
let Some(in_stream) = in_ctx.stream(i) else {
return Err(RemuxError::OperationFailed {
reason: format!("input stream {i} is missing"),
});
};
let out_idx = out_ctx.new_stream(None).map_err(|_| RemuxError::Ffmpeg {
code: 0,
message: "avformat_new_stream failed".to_string(),
})?;
let par = in_stream.codecpar();
if let Some(spec) = bsf.for_media_type(par.codec_type()) {
let filter =
ff_sys::BsfContext::open(spec, Some(par), in_stream.time_base()).map_err(|e| {
RemuxError::InvalidConfig {
reason: format!(
"bitstream filter {spec:?} for stream {i}: {}",
ff_sys::av_error_string(e.code())
),
}
})?;
out_ctx
.copy_stream_params(out_idx, filter.output_params())
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
filters.push(Some(filter));
} else {
out_ctx
.copy_stream_params(out_idx, par)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
filters.push(None);
}
}
let start_ts = (start_sec * AV_TIME_BASE as f64) as i64;
in_ctx
.seek_file(-1, i64::MIN, start_ts, start_ts, 0)
.map_err(|e| RemuxError::from_ffmpeg_error(e.code()))?;
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()))?;
log::debug!("stream copy trim header written nb_streams={nb_streams}");
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(()) => {}
}
let stream_idx = pkt.stream_index() as usize;
if stream_idx >= nb_streams {
pkt.unref();
continue;
}
let Some(in_stream) = in_ctx.stream(stream_idx) else {
pkt.unref();
continue;
};
let in_tb = in_stream.time_base();
let ts = if pkt.pts() != ff_sys::AV_NOPTS_VALUE {
pkt.pts()
} else {
pkt.dts()
};
if ts != ff_sys::AV_NOPTS_VALUE && in_tb.den != 0 {
let ts_sec = ts as f64 * f64::from(in_tb.num) / f64::from(in_tb.den);
if ts_sec >= end_sec {
pkt.unref();
break 'read;
}
}
let out_tb = out_ctx.stream_time_base(stream_idx);
let Some(filter) = filters[stream_idx].as_mut() else {
pkt.rescale_ts(in_tb, out_tb);
pkt.set_stream_index(stream_idx as i32);
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;
}
continue;
};
if let Err(e) = filter.send_packet(&mut pkt) {
loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break 'read;
}
let bsf_tb = filter.output_time_base();
loop {
match filter.receive_packet(&mut pkt) {
Ok(ff_sys::ReceiveOutcome::Frame) => {}
Ok(_) => break,
Err(e) => {
loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break 'read;
}
}
pkt.rescale_ts(bsf_tb, out_tb);
pkt.set_stream_index(stream_idx as i32);
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;
}
}
}
if loop_err.is_none() {
for (idx, slot) in filters.iter_mut().enumerate() {
let Some(filter) = slot.as_mut() else {
continue;
};
if let Err(e) = filter.send_eof() {
loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break;
}
let bsf_tb = filter.output_time_base();
let out_tb = out_ctx.stream_time_base(idx);
loop {
match filter.receive_packet(&mut pkt) {
Ok(ff_sys::ReceiveOutcome::Frame) => {}
Ok(_) => break,
Err(e) => {
loop_err = Some(RemuxError::from_ffmpeg_error(e.code()));
break;
}
}
pkt.rescale_ts(bsf_tb, out_tb);
pkt.set_stream_index(idx as i32);
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;
}
}
if loop_err.is_some() {
break;
}
}
}
let _ = out_ctx.write_trailer();
log::debug!("stream copy trim complete");
match loop_err {
Some(e) => Err(e),
None => Ok(()),
}
}