1use crate::{Result, TranscodeError};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
9pub enum MultiPassMode {
10 #[default]
12 SinglePass,
13 TwoPass,
15 ThreePass,
17}
18
19#[derive(Debug, Clone)]
21pub struct MultiPassConfig {
22 pub mode: MultiPassMode,
24 pub stats_file: PathBuf,
26 pub current_pass: u32,
28 pub keep_stats: bool,
30 pub target_bitrate: Option<u64>,
32 pub max_bitrate: Option<u64>,
34}
35
36impl MultiPassMode {
37 #[must_use]
39 pub fn pass_count(self) -> u32 {
40 match self {
41 Self::SinglePass => 1,
42 Self::TwoPass => 2,
43 Self::ThreePass => 3,
44 }
45 }
46
47 #[must_use]
49 pub fn requires_stats(self) -> bool {
50 !matches!(self, Self::SinglePass)
51 }
52
53 #[must_use]
55 pub fn description(self) -> &'static str {
56 match self {
57 Self::SinglePass => "Single-pass encoding (fast, good quality)",
58 Self::TwoPass => "Two-pass encoding (balanced speed and quality)",
59 Self::ThreePass => "Three-pass encoding (slow, best quality)",
60 }
61 }
62}
63
64impl MultiPassConfig {
65 pub fn new(mode: MultiPassMode, stats_file: impl Into<PathBuf>) -> Self {
72 Self {
73 mode,
74 stats_file: stats_file.into(),
75 current_pass: 1,
76 keep_stats: false,
77 target_bitrate: None,
78 max_bitrate: None,
79 }
80 }
81
82 #[must_use]
84 pub fn with_target_bitrate(mut self, bitrate: u64) -> Self {
85 self.target_bitrate = Some(bitrate);
86 self
87 }
88
89 #[must_use]
91 pub fn with_max_bitrate(mut self, bitrate: u64) -> Self {
92 self.max_bitrate = Some(bitrate);
93 self
94 }
95
96 #[must_use]
98 pub fn keep_stats(mut self, keep: bool) -> Self {
99 self.keep_stats = keep;
100 self
101 }
102
103 #[must_use]
105 pub fn stats_file_for_pass(&self, pass: u32) -> PathBuf {
106 if self.mode.pass_count() == 1 {
107 return self.stats_file.clone();
108 }
109
110 let mut path = self.stats_file.clone();
111 let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("stats");
112 let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("log");
113
114 path.set_file_name(format!("{stem}_pass{pass}.{ext}"));
115 path
116 }
117
118 #[must_use]
120 pub fn is_analysis_pass(&self, pass: u32) -> bool {
121 if self.mode == MultiPassMode::SinglePass {
122 return false;
123 }
124
125 pass < self.mode.pass_count()
127 }
128
129 #[must_use]
131 pub fn encoder_flags_for_pass(&self, pass: u32) -> Vec<String> {
132 let mut flags = Vec::new();
133
134 match self.mode {
135 MultiPassMode::SinglePass => {
136 }
138 MultiPassMode::TwoPass => {
139 if pass == 1 {
140 flags.push("pass=1".to_string());
141 flags.push(format!(
142 "stats={}",
143 self.stats_file_for_pass(pass).display()
144 ));
145 } else {
146 flags.push("pass=2".to_string());
147 flags.push(format!("stats={}", self.stats_file_for_pass(1).display()));
148 }
149 }
150 MultiPassMode::ThreePass => match pass {
151 1 => {
152 flags.push("pass=1".to_string());
153 flags.push(format!(
154 "stats={}",
155 self.stats_file_for_pass(pass).display()
156 ));
157 }
158 2 => {
159 flags.push("pass=2".to_string());
160 flags.push(format!(
161 "stats={}",
162 self.stats_file_for_pass(pass).display()
163 ));
164 }
165 3 => {
166 flags.push("pass=3".to_string());
167 flags.push(format!("stats={}", self.stats_file_for_pass(1).display()));
168 flags.push(format!("stats2={}", self.stats_file_for_pass(2).display()));
169 }
170 _ => {}
171 },
172 }
173
174 flags
175 }
176
177 pub fn validate(&self) -> Result<()> {
183 if self.mode.requires_stats() {
184 let parent = self.stats_file.parent().ok_or_else(|| {
185 TranscodeError::MultiPassError("Invalid stats file path".to_string())
186 })?;
187
188 if !parent.exists() {
189 return Err(TranscodeError::MultiPassError(format!(
190 "Stats directory does not exist: {}",
191 parent.display()
192 )));
193 }
194 }
195
196 if let (Some(target), Some(max)) = (self.target_bitrate, self.max_bitrate) {
197 if target > max {
198 return Err(TranscodeError::MultiPassError(
199 "Target bitrate cannot exceed max bitrate".to_string(),
200 ));
201 }
202 }
203
204 Ok(())
205 }
206
207 pub fn cleanup(&self) -> Result<()> {
209 if !self.keep_stats && self.mode.requires_stats() {
210 for pass in 1..=self.mode.pass_count() {
211 let stats_file = self.stats_file_for_pass(pass);
212 if stats_file.exists() {
213 std::fs::remove_file(&stats_file)?;
214 }
215 }
216 }
217 Ok(())
218 }
219}
220
221pub struct MultiPassEncoder {
223 config: MultiPassConfig,
224}
225
226impl MultiPassEncoder {
227 #[must_use]
229 pub fn new(config: MultiPassConfig) -> Self {
230 Self { config }
231 }
232
233 #[must_use]
235 pub fn pass_count(&self) -> u32 {
236 self.config.mode.pass_count()
237 }
238
239 #[must_use]
241 pub fn has_more_passes(&self) -> bool {
242 self.config.current_pass < self.pass_count()
243 }
244
245 pub fn next_pass(&mut self) {
247 if self.has_more_passes() {
248 self.config.current_pass += 1;
249 }
250 }
251
252 #[must_use]
254 pub fn current_pass(&self) -> u32 {
255 self.config.current_pass
256 }
257
258 #[must_use]
260 pub fn current_encoder_flags(&self) -> Vec<String> {
261 self.config.encoder_flags_for_pass(self.config.current_pass)
262 }
263
264 #[must_use]
266 pub fn is_current_analysis_pass(&self) -> bool {
267 self.config.is_analysis_pass(self.config.current_pass)
268 }
269
270 pub fn reset(&mut self) {
272 self.config.current_pass = 1;
273 }
274
275 pub fn cleanup(&self) -> Result<()> {
277 self.config.cleanup()
278 }
279}
280
281#[allow(dead_code)]
283pub struct MultiPassConfigBuilder {
284 mode: MultiPassMode,
285 stats_file: Option<PathBuf>,
286 keep_stats: bool,
287 target_bitrate: Option<u64>,
288 max_bitrate: Option<u64>,
289}
290
291#[allow(dead_code)]
292impl MultiPassConfigBuilder {
293 #[must_use]
295 pub fn new(mode: MultiPassMode) -> Self {
296 Self {
297 mode,
298 stats_file: None,
299 keep_stats: false,
300 target_bitrate: None,
301 max_bitrate: None,
302 }
303 }
304
305 #[must_use]
307 pub fn stats_file(mut self, path: impl Into<PathBuf>) -> Self {
308 self.stats_file = Some(path.into());
309 self
310 }
311
312 #[must_use]
314 pub fn keep_stats(mut self, keep: bool) -> Self {
315 self.keep_stats = keep;
316 self
317 }
318
319 #[must_use]
321 pub fn target_bitrate(mut self, bitrate: u64) -> Self {
322 self.target_bitrate = Some(bitrate);
323 self
324 }
325
326 #[must_use]
328 pub fn max_bitrate(mut self, bitrate: u64) -> Self {
329 self.max_bitrate = Some(bitrate);
330 self
331 }
332
333 pub fn build(self) -> Result<MultiPassConfig> {
339 let stats_file = if self.mode.requires_stats() {
340 self.stats_file.ok_or_else(|| {
341 TranscodeError::MultiPassError(
342 "Stats file required for multi-pass encoding".to_string(),
343 )
344 })?
345 } else {
346 self.stats_file
347 .unwrap_or_else(|| PathBuf::from("/tmp/stats.log"))
348 };
349
350 let mut config = MultiPassConfig::new(self.mode, stats_file);
351 config.keep_stats = self.keep_stats;
352 config.target_bitrate = self.target_bitrate;
353 config.max_bitrate = self.max_bitrate;
354
355 config.validate()?;
356 Ok(config)
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn test_multipass_mode_pass_count() {
366 assert_eq!(MultiPassMode::SinglePass.pass_count(), 1);
367 assert_eq!(MultiPassMode::TwoPass.pass_count(), 2);
368 assert_eq!(MultiPassMode::ThreePass.pass_count(), 3);
369 }
370
371 #[test]
372 fn test_multipass_mode_requires_stats() {
373 assert!(!MultiPassMode::SinglePass.requires_stats());
374 assert!(MultiPassMode::TwoPass.requires_stats());
375 assert!(MultiPassMode::ThreePass.requires_stats());
376 }
377
378 #[test]
379 fn test_multipass_config_stats_file() {
380 let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
381 assert_eq!(
382 config.stats_file_for_pass(1),
383 PathBuf::from("/tmp/stats_pass1.log")
384 );
385 assert_eq!(
386 config.stats_file_for_pass(2),
387 PathBuf::from("/tmp/stats_pass2.log")
388 );
389 }
390
391 #[test]
392 fn test_multipass_config_is_analysis_pass() {
393 let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
394 assert!(config.is_analysis_pass(1));
395 assert!(!config.is_analysis_pass(2));
396 }
397
398 #[test]
399 fn test_multipass_encoder_flow() {
400 let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
401 let mut encoder = MultiPassEncoder::new(config);
402
403 assert_eq!(encoder.current_pass(), 1);
404 assert!(encoder.has_more_passes());
405 assert!(encoder.is_current_analysis_pass());
406
407 encoder.next_pass();
408
409 assert_eq!(encoder.current_pass(), 2);
410 assert!(!encoder.has_more_passes());
411 assert!(!encoder.is_current_analysis_pass());
412 }
413
414 #[test]
415 fn test_multipass_encoder_reset() {
416 let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
417 let mut encoder = MultiPassEncoder::new(config);
418
419 encoder.next_pass();
420 assert_eq!(encoder.current_pass(), 2);
421
422 encoder.reset();
423 assert_eq!(encoder.current_pass(), 1);
424 }
425
426 #[test]
427 fn test_multipass_config_builder() {
428 let config = MultiPassConfigBuilder::new(MultiPassMode::TwoPass)
429 .stats_file("/tmp/test_stats.log")
430 .target_bitrate(5_000_000)
431 .max_bitrate(8_000_000)
432 .keep_stats(true)
433 .build()
434 .expect("should succeed in test");
435
436 assert_eq!(config.mode, MultiPassMode::TwoPass);
437 assert_eq!(config.target_bitrate, Some(5_000_000));
438 assert_eq!(config.max_bitrate, Some(8_000_000));
439 assert!(config.keep_stats);
440 }
441
442 #[test]
443 fn test_encoder_flags_two_pass() {
444 let config = MultiPassConfig::new(MultiPassMode::TwoPass, "/tmp/stats.log");
445
446 let flags1 = config.encoder_flags_for_pass(1);
447 assert!(flags1.contains(&"pass=1".to_string()));
448
449 let flags2 = config.encoder_flags_for_pass(2);
450 assert!(flags2.contains(&"pass=2".to_string()));
451 }
452
453 #[test]
454 fn test_single_pass_no_stats() {
455 let config = MultiPassConfig::new(MultiPassMode::SinglePass, "/tmp/stats.log");
456 assert!(!config.is_analysis_pass(1));
457 assert_eq!(config.encoder_flags_for_pass(1).len(), 0);
458 }
459}