use crate::{Result, TranscodeError};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum MultiPassMode {
#[default]
SinglePass,
TwoPass,
ThreePass,
}
#[derive(Debug, Clone)]
pub struct MultiPassConfig {
pub mode: MultiPassMode,
pub stats_file: PathBuf,
pub current_pass: u32,
pub keep_stats: bool,
pub target_bitrate: Option<u64>,
pub max_bitrate: Option<u64>,
}
impl MultiPassMode {
#[must_use]
pub fn pass_count(self) -> u32 {
match self {
Self::SinglePass => 1,
Self::TwoPass => 2,
Self::ThreePass => 3,
}
}
#[must_use]
pub fn requires_stats(self) -> bool {
!matches!(self, Self::SinglePass)
}
#[must_use]
pub fn description(self) -> &'static str {
match self {
Self::SinglePass => "Single-pass encoding (fast, good quality)",
Self::TwoPass => "Two-pass encoding (balanced speed and quality)",
Self::ThreePass => "Three-pass encoding (slow, best quality)",
}
}
}
impl MultiPassConfig {
pub fn new(mode: MultiPassMode, stats_file: impl Into<PathBuf>) -> Self {
Self {
mode,
stats_file: stats_file.into(),
current_pass: 1,
keep_stats: false,
target_bitrate: None,
max_bitrate: None,
}
}
#[must_use]
pub fn with_target_bitrate(mut self, bitrate: u64) -> Self {
self.target_bitrate = Some(bitrate);
self
}
#[must_use]
pub fn with_max_bitrate(mut self, bitrate: u64) -> Self {
self.max_bitrate = Some(bitrate);
self
}
#[must_use]
pub fn keep_stats(mut self, keep: bool) -> Self {
self.keep_stats = keep;
self
}
#[must_use]
pub fn stats_file_for_pass(&self, pass: u32) -> PathBuf {
if self.mode.pass_count() == 1 {
return self.stats_file.clone();
}
let mut path = self.stats_file.clone();
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("stats");
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("log");
path.set_file_name(format!("{stem}_pass{pass}.{ext}"));
path
}
#[must_use]
pub fn is_analysis_pass(&self, pass: u32) -> bool {
if self.mode == MultiPassMode::SinglePass {
return false;
}
pass < self.mode.pass_count()
}
#[must_use]
pub fn encoder_flags_for_pass(&self, pass: u32) -> Vec<String> {
let mut flags = Vec::new();
match self.mode {
MultiPassMode::SinglePass => {
}
MultiPassMode::TwoPass => {
if pass == 1 {
flags.push("pass=1".to_string());
flags.push(format!(
"stats={}",
self.stats_file_for_pass(pass).display()
));
} else {
flags.push("pass=2".to_string());
flags.push(format!("stats={}", self.stats_file_for_pass(1).display()));
}
}
MultiPassMode::ThreePass => match pass {
1 => {
flags.push("pass=1".to_string());
flags.push(format!(
"stats={}",
self.stats_file_for_pass(pass).display()
));
}
2 => {
flags.push("pass=2".to_string());
flags.push(format!(
"stats={}",
self.stats_file_for_pass(pass).display()
));
}
3 => {
flags.push("pass=3".to_string());
flags.push(format!("stats={}", self.stats_file_for_pass(1).display()));
flags.push(format!("stats2={}", self.stats_file_for_pass(2).display()));
}
_ => {}
},
}
flags
}
pub fn validate(&self) -> Result<()> {
if self.mode.requires_stats() {
let parent = self.stats_file.parent().ok_or_else(|| {
TranscodeError::MultiPassError("Invalid stats file path".to_string())
})?;
if !parent.exists() {
return Err(TranscodeError::MultiPassError(format!(
"Stats directory does not exist: {}",
parent.display()
)));
}
}
if let (Some(target), Some(max)) = (self.target_bitrate, self.max_bitrate) {
if target > max {
return Err(TranscodeError::MultiPassError(
"Target bitrate cannot exceed max bitrate".to_string(),
));
}
}
Ok(())
}
pub fn cleanup(&self) -> Result<()> {
if !self.keep_stats && self.mode.requires_stats() {
for pass in 1..=self.mode.pass_count() {
let stats_file = self.stats_file_for_pass(pass);
if stats_file.exists() {
std::fs::remove_file(&stats_file)?;
}
}
}
Ok(())
}
}
pub struct MultiPassEncoder {
config: MultiPassConfig,
}
impl MultiPassEncoder {
#[must_use]
pub fn new(config: MultiPassConfig) -> Self {
Self { config }
}
#[must_use]
pub fn pass_count(&self) -> u32 {
self.config.mode.pass_count()
}
#[must_use]
pub fn has_more_passes(&self) -> bool {
self.config.current_pass < self.pass_count()
}
pub fn next_pass(&mut self) {
if self.has_more_passes() {
self.config.current_pass += 1;
}
}
#[must_use]
pub fn current_pass(&self) -> u32 {
self.config.current_pass
}
#[must_use]
pub fn current_encoder_flags(&self) -> Vec<String> {
self.config.encoder_flags_for_pass(self.config.current_pass)
}
#[must_use]
pub fn is_current_analysis_pass(&self) -> bool {
self.config.is_analysis_pass(self.config.current_pass)
}
pub fn reset(&mut self) {
self.config.current_pass = 1;
}
pub fn cleanup(&self) -> Result<()> {
self.config.cleanup()
}
}
#[allow(dead_code)]
pub struct MultiPassConfigBuilder {
mode: MultiPassMode,
stats_file: Option<PathBuf>,
keep_stats: bool,
target_bitrate: Option<u64>,
max_bitrate: Option<u64>,
}
#[allow(dead_code)]
impl MultiPassConfigBuilder {
#[must_use]
pub fn new(mode: MultiPassMode) -> Self {
Self {
mode,
stats_file: None,
keep_stats: false,
target_bitrate: None,
max_bitrate: None,
}
}
#[must_use]
pub fn stats_file(mut self, path: impl Into<PathBuf>) -> Self {
self.stats_file = Some(path.into());
self
}
#[must_use]
pub fn keep_stats(mut self, keep: bool) -> Self {
self.keep_stats = keep;
self
}
#[must_use]
pub fn target_bitrate(mut self, bitrate: u64) -> Self {
self.target_bitrate = Some(bitrate);
self
}
#[must_use]
pub fn max_bitrate(mut self, bitrate: u64) -> Self {
self.max_bitrate = Some(bitrate);
self
}
pub fn build(self) -> Result<MultiPassConfig> {
let stats_file = if self.mode.requires_stats() {
self.stats_file.ok_or_else(|| {
TranscodeError::MultiPassError(
"Stats file required for multi-pass encoding".to_string(),
)
})?
} else {
self.stats_file
.unwrap_or_else(|| PathBuf::from("/tmp/stats.log"))
};
let mut config = MultiPassConfig::new(self.mode, stats_file);
config.keep_stats = self.keep_stats;
config.target_bitrate = self.target_bitrate;
config.max_bitrate = self.max_bitrate;
config.validate()?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multipass_mode_pass_count() {
assert_eq!(MultiPassMode::SinglePass.pass_count(), 1);
assert_eq!(MultiPassMode::TwoPass.pass_count(), 2);
assert_eq!(MultiPassMode::ThreePass.pass_count(), 3);
}
#[test]
fn test_multipass_mode_requires_stats() {
assert!(!MultiPassMode::SinglePass.requires_stats());
assert!(MultiPassMode::TwoPass.requires_stats());
assert!(MultiPassMode::ThreePass.requires_stats());
}
#[test]
fn test_multipass_config_stats_file() {
let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
assert_eq!(
config.stats_file_for_pass(1),
PathBuf::from("/tmp/stats_pass1.log")
);
assert_eq!(
config.stats_file_for_pass(2),
PathBuf::from("/tmp/stats_pass2.log")
);
}
#[test]
fn test_multipass_config_is_analysis_pass() {
let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
assert!(config.is_analysis_pass(1));
assert!(!config.is_analysis_pass(2));
}
#[test]
fn test_multipass_encoder_flow() {
let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
let mut encoder = MultiPassEncoder::new(config);
assert_eq!(encoder.current_pass(), 1);
assert!(encoder.has_more_passes());
assert!(encoder.is_current_analysis_pass());
encoder.next_pass();
assert_eq!(encoder.current_pass(), 2);
assert!(!encoder.has_more_passes());
assert!(!encoder.is_current_analysis_pass());
}
#[test]
fn test_multipass_encoder_reset() {
let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
let mut encoder = MultiPassEncoder::new(config);
encoder.next_pass();
assert_eq!(encoder.current_pass(), 2);
encoder.reset();
assert_eq!(encoder.current_pass(), 1);
}
#[test]
fn test_multipass_config_builder() {
let config = MultiPassConfigBuilder::new(MultiPassMode::TwoPass)
.stats_file("/tmp/test_stats.log")
.target_bitrate(5_000_000)
.max_bitrate(8_000_000)
.keep_stats(true)
.build()
.expect("should succeed in test");
assert_eq!(config.mode, MultiPassMode::TwoPass);
assert_eq!(config.target_bitrate, Some(5_000_000));
assert_eq!(config.max_bitrate, Some(8_000_000));
assert!(config.keep_stats);
}
#[test]
fn test_encoder_flags_two_pass() {
let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
let flags1 = config.encoder_flags_for_pass(1);
assert!(flags1.contains(&"pass=1".to_string()));
let flags2 = config.encoder_flags_for_pass(2);
assert!(flags2.contains(&"pass=2".to_string()));
}
#[test]
fn test_single_pass_no_stats() {
let config = MultiPassConfig::new(MultiPassMode::SinglePass, "/tmp/stats.log");
assert!(!config.is_analysis_pass(1));
assert_eq!(config.encoder_flags_for_pass(1).len(), 0);
}
}