pub mod command_builder;
pub mod ffmpeg;
pub use command_builder::EncodingParams;
pub use ffmpeg::{EncodeResult, ProgressCallback, encode_video};
use crate::analyzer::{DvMode, HdrType, VideoMetadata};
use crate::config::AppConfig;
use crate::tracks::TrackSelection;
use crate::verifier;
use std::sync::atomic::AtomicBool;
use tracing::{info, warn};
#[derive(Debug)]
pub enum FullEncodeResult {
Success,
SuccessWithVmaf {
vmaf: verifier::VmafResult,
source_deleted: bool,
},
VmafFailed { message: String },
Cancelled,
Error(String),
QualityWarning {
vmaf: verifier::VmafResult,
threshold: f64,
},
}
impl FullEncodeResult {
pub fn is_success(&self) -> bool {
matches!(self, Self::Success | Self::SuccessWithVmaf { .. })
}
}
#[allow(clippy::too_many_arguments)]
pub fn run_encoding_pipeline(
input: &str,
output: &str,
metadata: &VideoMetadata,
tracks: TrackSelection,
dv_mode: DvMode,
remux_only: bool,
config: &AppConfig,
progress_callback: Option<ProgressCallback>,
cancel_flag: &AtomicBool,
on_before_vmaf: Option<Box<dyn FnOnce() + Send>>,
) -> FullEncodeResult {
let params = EncodingParams::from_metadata(
input, output, metadata, config, tracks, dv_mode, remux_only,
);
let duration = metadata.duration_secs;
let total_frames = if metadata.frame_rate_den > 0 {
duration * f64::from(metadata.frame_rate_num) / f64::from(metadata.frame_rate_den)
} else {
0.0
};
let encode_result = encode_video(
¶ms,
progress_callback,
cancel_flag,
duration,
total_frames,
);
match encode_result {
EncodeResult::Success => {
let tone_mapped = metadata.hdr_type == HdrType::DolbyVision
&& metadata.dv_profile == Some(5)
&& dv_mode == DvMode::ToHdr10;
if tone_mapped && config.quality.vmaf_enabled {
info!("Skipping VMAF: DV profile 5 tone-mapped output is not comparable");
}
let vmaf_threshold = if config.quality.vmaf_enabled && !remux_only && !tone_mapped {
if let Some(cb) = on_before_vmaf {
cb();
}
Some(config.quality.vmaf_threshold)
} else {
None
};
let mut result = run_vmaf_check(
input,
output,
vmaf_threshold,
metadata.hdr_type,
metadata.width,
);
if config.quality.delete_source_on_success && result.is_success() {
match std::fs::remove_file(input) {
Ok(()) => {
info!("Deleted source file: {input}");
if let FullEncodeResult::SuccessWithVmaf {
ref mut source_deleted,
..
} = result
{
*source_deleted = true;
}
}
Err(e) => warn!("Failed to delete source file {input}: {e}"),
}
}
result
}
EncodeResult::Cancelled => FullEncodeResult::Cancelled,
EncodeResult::Error(e) => FullEncodeResult::Error(e),
}
}
fn run_vmaf_check(
input: &str,
output: &str,
threshold: Option<f64>,
hdr_type: HdrType,
width: u32,
) -> FullEncodeResult {
let Some(threshold) = threshold else {
return FullEncodeResult::Success;
};
info!("Running VMAF quality check...");
let input_path = std::path::Path::new(input);
let output_path = std::path::Path::new(output);
match verifier::calculate_vmaf(input_path, output_path, hdr_type, width) {
Ok(vmaf) => {
info!("VMAF score: {:.2} ({})", vmaf.score, vmaf.quality_grade());
if !vmaf.meets_threshold(threshold) {
warn!(
"VMAF score {:.2} is below threshold {:.2}",
vmaf.score, threshold
);
return FullEncodeResult::QualityWarning { vmaf, threshold };
}
FullEncodeResult::SuccessWithVmaf {
vmaf,
source_deleted: false,
}
}
Err(e) => {
warn!("VMAF calculation failed: {:?}", e);
FullEncodeResult::VmafFailed {
message: e.to_string(),
}
}
}
}