use crate::{
command::{
PROGRESS_CHARS, SmallDuration,
args::{self, Encoder},
},
console_ext::style,
ffmpeg,
ffprobe::{self, Ffprobe},
log::ProgressLogger,
process::FfmpegOut,
temporary::{self, TempKind},
};
use clap::Parser;
use console::style;
use indicatif::{HumanBytes, ProgressBar, ProgressStyle};
use log::info;
use same_file::is_same_file;
use std::{
path::{Path, PathBuf},
sync::Arc,
time::{Duration, Instant},
};
use tokio::fs;
use tokio_stream::StreamExt;
#[derive(Parser)]
#[group(skip)]
pub struct Args {
#[clap(flatten)]
pub args: args::Encode,
#[arg(long)]
pub crf: f32,
#[clap(flatten)]
pub encode: args::EncodeToOutput,
}
pub async fn encode(args: Args) -> anyhow::Result<()> {
let bar = ProgressBar::new(1).with_style(
ProgressStyle::default_bar()
.template("{spinner:.cyan.bold} {elapsed_precise:.bold} {wide_bar:.cyan/blue} ({msg}eta {eta})")?
.progress_chars(PROGRESS_CHARS)
);
bar.enable_steady_tick(Duration::from_millis(100));
let probe = ffprobe::probe(&args.args.input);
run(args, probe.into(), &bar).await
}
pub async fn run(
Args {
args,
crf,
encode:
args::EncodeToOutput {
output,
audio_codec,
downmix_to_stereo,
video_only,
overwrite_input,
},
}: Args,
probe: Arc<Ffprobe>,
bar: &ProgressBar,
) -> anyhow::Result<()> {
let defaulting_output = output.is_none();
let output =
output.unwrap_or_else(|| default_output_name(&args.input, &args.encoder, probe.is_image));
anyhow::ensure!(
overwrite_input || !is_same_file(&output, &args.input).unwrap_or(false),
"Input and Output are specified as the same file. Not proceeding. \
Pass in `--overwrite-input` to allow this."
);
temporary::add(&output, TempKind::NotKeepable);
if defaulting_output {
let out = shell_escape::escape(output.display().to_string().into());
bar.println(style!("Encoding {out}").dim().to_string());
}
bar.set_message("encoding, ");
let mut enc_args = args.to_ffmpeg_args(crf, &probe)?;
enc_args.video_only = video_only;
let has_audio = probe.has_audio;
if let Ok(d) = &probe.duration {
bar.set_length(d.as_micros_u64().max(1));
}
let stereo_downmix = downmix_to_stereo && probe.max_audio_channels.is_some_and(|c| c > 3);
let audio_codec = audio_codec.as_deref();
if stereo_downmix && audio_codec == Some("copy") {
anyhow::bail!("--stereo-downmix cannot be used with --acodec copy");
}
info!(
"encoding {}",
output.file_name().and_then(|n| n.to_str()).unwrap_or("")
);
let mut enc = ffmpeg::encode(enc_args, &output, has_audio, audio_codec, stereo_downmix)?;
let mut logger = ProgressLogger::new(module_path!(), Instant::now());
let mut stream_sizes = None;
while let Some(progress) = enc.next().await {
match progress? {
FfmpegOut::Progress { fps, time, .. } => {
if fps > 0.0 {
bar.set_message(format!("{fps} fps, "));
}
if let Ok(d) = &probe.duration {
bar.set_position(time.as_micros_u64());
logger.update(*d, time, fps);
}
}
FfmpegOut::StreamSizes {
video,
audio,
subtitle,
other,
} => stream_sizes = Some((video, audio, subtitle, other)),
}
}
enc.wait().await?; bar.finish();
temporary::unadd(&output);
let output_size = fs::metadata(&output).await?.len();
let output_percent = 100.0 * output_size as f64 / fs::metadata(&args.input).await?.len() as f64;
let output_size = style(HumanBytes(output_size)).dim().bold();
let output_percent = style!("{}%", output_percent.round()).dim().bold();
eprint!(
"{} {output_size} {}{output_percent}",
style("Encoded").dim(),
style("(").dim(),
);
if let Some((video, audio, subtitle, other)) = stream_sizes
&& (audio > 0 || subtitle > 0 || other > 0)
{
for (label, size) in [
("video:", video),
("audio:", audio),
("subs:", subtitle),
("other:", other),
] {
if size > 0 {
let size = style(HumanBytes(size)).dim();
eprint!("{} {}{size}", style(",").dim(), style(label).dim(),);
}
}
}
eprintln!("{}", style(")").dim());
Ok(())
}
pub fn default_output_ext(input: &Path, encoder: &Encoder, is_image: bool) -> &'static str {
if is_image {
return encoder.default_image_ext();
}
match input.extension().and_then(|e| e.to_str()) {
Some("mp4") => "mp4",
_ => "mkv",
}
}
pub fn default_output_name(input: &Path, encoder: &Encoder, is_image: bool) -> PathBuf {
let pre = ffmpeg::pre_extension_name(encoder.as_str());
let ext = default_output_ext(input, encoder, is_image);
input.with_extension(format!("{pre}.{ext}"))
}