use std::path::{Path, PathBuf};
use ffmpeg_next::{codec::Id, media::Type};
use crate::configuration::ExtractOptions;
use crate::error::UnbundleError;
use crate::progress::{OperationType, ProgressTracker};
pub struct Remuxer {
input_path: PathBuf,
output_path: PathBuf,
copy_video: bool,
copy_audio: bool,
copy_subtitles: bool,
}
impl Remuxer {
pub fn new<P1: AsRef<Path>, P2: AsRef<Path>>(
input: P1,
output: P2,
) -> Result<Self, UnbundleError> {
let input_path = input.as_ref().to_path_buf();
let output_path = output.as_ref().to_path_buf();
ffmpeg_next::init().map_err(|e| UnbundleError::FileOpen {
path: input_path.clone(),
reason: format!("FFmpeg initialisation failed: {e}"),
})?;
if !input_path.exists() {
return Err(UnbundleError::FileOpen {
path: input_path,
reason: "File does not exist".to_string(),
});
}
Ok(Self {
input_path,
output_path,
copy_video: true,
copy_audio: true,
copy_subtitles: true,
})
}
#[must_use]
pub fn exclude_video(mut self) -> Self {
self.copy_video = false;
self
}
#[must_use]
pub fn with_exclude_video(self) -> Self {
self.exclude_video()
}
#[must_use]
pub fn exclude_audio(mut self) -> Self {
self.copy_audio = false;
self
}
#[must_use]
pub fn with_exclude_audio(self) -> Self {
self.exclude_audio()
}
#[must_use]
pub fn exclude_subtitles(mut self) -> Self {
self.copy_subtitles = false;
self
}
#[must_use]
pub fn with_exclude_subtitles(self) -> Self {
self.exclude_subtitles()
}
pub fn run(&self) -> Result<(), UnbundleError> {
self.run_with_options(&ExtractOptions::default())
}
pub fn run_with_options(&self, config: &ExtractOptions) -> Result<(), UnbundleError> {
log::info!(
"Remuxing {} → {} (video={}, audio={}, subtitles={})",
self.input_path.display(),
self.output_path.display(),
self.copy_video,
self.copy_audio,
self.copy_subtitles,
);
let mut input_context =
ffmpeg_next::format::input(&self.input_path).map_err(|e| UnbundleError::FileOpen {
path: self.input_path.clone(),
reason: e.to_string(),
})?;
let mut output_context = ffmpeg_next::format::output(&self.output_path).map_err(|e| {
UnbundleError::FileOpen {
path: self.output_path.clone(),
reason: format!("Failed to create output: {e}"),
}
})?;
let mut stream_map: Vec<Option<usize>> = Vec::new();
let mut output_stream_count: usize = 0;
for stream in input_context.streams() {
let medium = stream.parameters().medium();
let include = match medium {
Type::Video => self.copy_video,
Type::Audio => self.copy_audio,
Type::Subtitle => self.copy_subtitles,
_ => false,
};
if include {
let mut out_stream =
output_context.add_stream(ffmpeg_next::encoder::find(Id::None))?;
out_stream.set_parameters(stream.parameters());
unsafe {
(*out_stream.parameters().as_mut_ptr()).codec_tag = 0;
}
stream_map.push(Some(output_stream_count));
output_stream_count += 1;
} else {
stream_map.push(None);
}
}
output_context.write_header()?;
let total_packets: Option<u64> = None;
let mut tracker = ProgressTracker::new(
config.progress.clone(),
OperationType::Remuxing,
total_packets,
config.batch_size,
);
for (stream, mut packet) in input_context.packets() {
if config.is_cancelled() {
return Err(UnbundleError::Cancelled);
}
let input_idx = stream.index();
let Some(output_idx) = stream_map.get(input_idx).copied().flatten() else {
continue;
};
let input_time_base = stream.time_base();
let output_time_base = output_context.stream(output_idx).unwrap().time_base();
packet.set_stream(output_idx);
packet.rescale_ts(input_time_base, output_time_base);
packet.set_position(-1);
packet.write_interleaved(&mut output_context)?;
tracker.advance(None, None);
}
tracker.finish();
output_context.write_trailer()?;
Ok(())
}
}