1#![allow(dead_code)]
24#![allow(clippy::cast_precision_loss)]
25
26#[derive(Debug, Clone)]
32pub struct FirstPassFrameStats {
33 pub frame_index: u64,
35 pub intra_cost: f32,
37 pub inter_cost: f32,
39 pub intra_inter_ratio: f32,
41 pub mean_sad: f32,
43 pub key_frame_bits: u64,
45 pub is_scene_change: bool,
47}
48
49impl FirstPassFrameStats {
50 #[must_use]
52 pub fn new(frame_index: u64, intra_cost: f32, inter_cost: f32) -> Self {
53 let ratio = if inter_cost > 0.0 {
54 intra_cost / inter_cost
55 } else {
56 1.0
57 };
58 Self {
59 frame_index,
60 intra_cost,
61 inter_cost,
62 intra_inter_ratio: ratio,
63 mean_sad: 0.0,
64 key_frame_bits: 0,
65 is_scene_change: ratio > 2.5,
66 }
67 }
68
69 #[must_use]
74 pub fn complexity_weight(&self) -> f32 {
75 let base = self.inter_cost.max(0.001);
76 if self.is_scene_change {
77 base * 1.8
78 } else {
79 base
80 }
81 }
82}
83
84#[derive(Debug, Default, Clone)]
90pub struct TwoPassFirstPassStats {
91 frames: Vec<FirstPassFrameStats>,
92}
93
94impl TwoPassFirstPassStats {
95 #[must_use]
97 pub fn new() -> Self {
98 Self::default()
99 }
100
101 pub fn push(&mut self, stats: FirstPassFrameStats) {
103 self.frames.push(stats);
104 }
105
106 #[must_use]
108 pub fn frame_count(&self) -> usize {
109 self.frames.len()
110 }
111
112 #[must_use]
114 pub fn get(&self, index: usize) -> Option<&FirstPassFrameStats> {
115 self.frames.get(index)
116 }
117
118 pub fn iter(&self) -> impl Iterator<Item = &FirstPassFrameStats> {
120 self.frames.iter()
121 }
122
123 #[must_use]
125 pub fn total_complexity(&self) -> f32 {
126 self.frames.iter().map(|f| f.complexity_weight()).sum()
127 }
128
129 #[must_use]
131 pub fn mean_intra_inter_ratio(&self) -> f32 {
132 if self.frames.is_empty() {
133 return 1.0;
134 }
135 self.frames.iter().map(|f| f.intra_inter_ratio).sum::<f32>() / self.frames.len() as f32
136 }
137
138 #[must_use]
140 pub fn scene_change_count(&self) -> usize {
141 self.frames.iter().filter(|f| f.is_scene_change).count()
142 }
143}
144
145#[derive(Debug, Clone)]
151pub struct TwoPassAllocatorConfig {
152 pub target_bitrate_bps: u64,
154 pub frame_rate: f64,
156 pub gop_size: u32,
158 pub max_bitrate_ratio: f32,
162 pub min_qp: u32,
164 pub max_qp: u32,
166}
167
168impl Default for TwoPassAllocatorConfig {
169 fn default() -> Self {
170 Self {
171 target_bitrate_bps: 4_000_000,
172 frame_rate: 30.0,
173 gop_size: 120,
174 max_bitrate_ratio: 2.5,
175 min_qp: 16,
176 max_qp: 51,
177 }
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct FrameAllocationResult {
184 pub frame_index: u64,
186 pub target_bits: u64,
188 pub suggested_qp: u32,
190 pub force_keyframe: bool,
192}
193
194#[derive(Debug, Clone)]
199pub struct TwoPassBitrateAllocator {
200 cfg: TwoPassAllocatorConfig,
201}
202
203impl TwoPassBitrateAllocator {
204 #[must_use]
206 pub fn new(cfg: TwoPassAllocatorConfig) -> Self {
207 Self { cfg }
208 }
209
210 #[must_use]
212 pub fn default_allocator() -> Self {
213 Self::new(TwoPassAllocatorConfig::default())
214 }
215
216 #[must_use]
221 pub fn allocate(&self, stats: &TwoPassFirstPassStats) -> Vec<FrameAllocationResult> {
222 let n = stats.frame_count();
223 if n == 0 {
224 return Vec::new();
225 }
226
227 let total_complexity = stats.total_complexity();
228 let seconds = n as f64 / self.cfg.frame_rate.max(1.0);
229 let total_bits = (self.cfg.target_bitrate_bps as f64 * seconds) as u64;
230
231 let max_frame_bits =
232 ((total_bits as f64 / n as f64) * f64::from(self.cfg.max_bitrate_ratio)) as u64;
233
234 let mut results = Vec::with_capacity(n);
235
236 for frame_stats in stats.iter() {
237 let weight = frame_stats.complexity_weight();
238 let raw_bits = if total_complexity > 0.0 {
239 ((weight as f64 / total_complexity as f64) * total_bits as f64) as u64
240 } else {
241 total_bits / n as u64
242 };
243
244 let target_bits = raw_bits.min(max_frame_bits).max(512);
245
246 let bpp = target_bits as f64 / (frame_stats.inter_cost.max(0.001) as f64 * 100.0);
248 let suggested_qp = self.bits_to_qp(bpp);
249
250 results.push(FrameAllocationResult {
251 frame_index: frame_stats.frame_index,
252 target_bits,
253 suggested_qp,
254 force_keyframe: frame_stats.is_scene_change,
255 });
256 }
257
258 results
259 }
260
261 fn bits_to_qp(&self, bpp: f64) -> u32 {
263 let qp_f = 36.0 - 12.0 * (bpp / 0.1 + 1.0).log2();
265 (qp_f.round() as u32).clamp(self.cfg.min_qp, self.cfg.max_qp)
266 }
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum TwoPassState {
276 FirstPass,
278 AllocationReady,
280 SecondPass,
282 Complete,
284}
285
286#[derive(Debug)]
291pub struct TwoPassSession {
292 cfg: TwoPassAllocatorConfig,
293 state: TwoPassState,
294 first_pass_stats: TwoPassFirstPassStats,
295 allocations: Vec<FrameAllocationResult>,
296 second_pass_index: usize,
297}
298
299impl TwoPassSession {
300 #[must_use]
302 pub fn new(cfg: TwoPassAllocatorConfig) -> Self {
303 Self {
304 cfg,
305 state: TwoPassState::FirstPass,
306 first_pass_stats: TwoPassFirstPassStats::new(),
307 allocations: Vec::new(),
308 second_pass_index: 0,
309 }
310 }
311
312 #[must_use]
314 pub fn state(&self) -> TwoPassState {
315 self.state
316 }
317
318 pub fn record_first_pass_frame(
322 &mut self,
323 stats: FirstPassFrameStats,
324 ) -> Result<(), &'static str> {
325 if self.state != TwoPassState::FirstPass {
326 return Err("session is not in first-pass state");
327 }
328 self.first_pass_stats.push(stats);
329 Ok(())
330 }
331
332 pub fn finish_first_pass(&mut self) {
336 if self.state != TwoPassState::FirstPass {
337 return;
338 }
339 let allocator = TwoPassBitrateAllocator::new(self.cfg.clone());
340 self.allocations = allocator.allocate(&self.first_pass_stats);
341 self.state = TwoPassState::AllocationReady;
342 }
343
344 pub fn begin_second_pass(&mut self) -> Result<(), &'static str> {
348 if self.state != TwoPassState::AllocationReady {
349 return Err("first pass not yet finalised");
350 }
351 self.second_pass_index = 0;
352 self.state = TwoPassState::SecondPass;
353 Ok(())
354 }
355
356 pub fn next_frame_allocation(&mut self) -> Option<&FrameAllocationResult> {
360 if self.state != TwoPassState::SecondPass {
361 return None;
362 }
363 if self.second_pass_index >= self.allocations.len() {
364 self.state = TwoPassState::Complete;
365 return None;
366 }
367 let result = &self.allocations[self.second_pass_index];
368 self.second_pass_index += 1;
369 if self.second_pass_index >= self.allocations.len() {
370 self.state = TwoPassState::Complete;
371 }
372 Some(result)
373 }
374
375 #[must_use]
377 pub fn first_pass_stats(&self) -> &TwoPassFirstPassStats {
378 &self.first_pass_stats
379 }
380
381 #[must_use]
383 pub fn allocations(&self) -> &[FrameAllocationResult] {
384 &self.allocations
385 }
386}
387
388#[derive(Debug, Clone, Default)]
412pub struct TwoPassStateTracker {
413 first_pass_complexities: Vec<f32>,
415}
416
417impl TwoPassStateTracker {
418 #[must_use]
420 pub fn new() -> Self {
421 Self::default()
422 }
423
424 pub fn record_first_pass(&mut self, complexity: f32) {
429 self.first_pass_complexities.push(complexity.max(0.0));
430 }
431
432 #[must_use]
451 pub fn compute_bitrate(&self, complexity: f32, budget: u32) -> u32 {
452 let total: f32 = self.first_pass_complexities.iter().sum();
453 if total <= 0.0 || budget == 0 {
454 return budget;
455 }
456 let share = complexity.max(0.0) / total;
457 let bits = (budget as f64 * share as f64).round() as u64;
458 bits.clamp(1, u64::from(budget)) as u32
459 }
460
461 #[must_use]
463 pub fn frame_count(&self) -> usize {
464 self.first_pass_complexities.len()
465 }
466
467 #[must_use]
469 pub fn total_complexity(&self) -> f32 {
470 self.first_pass_complexities.iter().sum()
471 }
472
473 #[must_use]
475 pub fn mean_complexity(&self) -> f32 {
476 let n = self.first_pass_complexities.len();
477 if n == 0 {
478 return 0.0;
479 }
480 self.total_complexity() / n as f32
481 }
482}
483
484#[cfg(test)]
489mod tests {
490 use super::*;
491
492 fn make_stats(n: usize) -> TwoPassFirstPassStats {
493 let mut stats = TwoPassFirstPassStats::new();
494 for i in 0..n {
495 let intra = 1.0 + (i as f32) * 0.1;
496 let inter = 0.5 + (i as f32) * 0.05;
497 stats.push(FirstPassFrameStats::new(i as u64, intra, inter));
498 }
499 stats
500 }
501
502 #[test]
503 fn test_first_pass_stats_frame_count() {
504 let stats = make_stats(10);
505 assert_eq!(stats.frame_count(), 10);
506 }
507
508 #[test]
509 fn test_total_complexity_positive() {
510 let stats = make_stats(5);
511 assert!(stats.total_complexity() > 0.0);
512 }
513
514 #[test]
515 fn test_allocator_correct_count() {
516 let stats = make_stats(30);
517 let alloc = TwoPassBitrateAllocator::default_allocator();
518 let results = alloc.allocate(&stats);
519 assert_eq!(results.len(), 30);
520 }
521
522 #[test]
523 fn test_allocator_empty_stats() {
524 let stats = TwoPassFirstPassStats::new();
525 let alloc = TwoPassBitrateAllocator::default_allocator();
526 let results = alloc.allocate(&stats);
527 assert!(results.is_empty());
528 }
529
530 #[test]
531 fn test_allocator_bits_in_range() {
532 let stats = make_stats(100);
533 let alloc = TwoPassBitrateAllocator::default_allocator();
534 let results = alloc.allocate(&stats);
535 for r in &results {
536 assert!(r.target_bits >= 512, "target_bits should be at least 512");
537 }
538 }
539
540 #[test]
541 fn test_allocator_qp_in_range() {
542 let stats = make_stats(20);
543 let alloc = TwoPassBitrateAllocator::default_allocator();
544 let results = alloc.allocate(&stats);
545 for r in &results {
546 assert!(r.suggested_qp >= 16 && r.suggested_qp <= 51);
547 }
548 }
549
550 #[test]
551 fn test_session_state_transitions() {
552 let mut session = TwoPassSession::new(TwoPassAllocatorConfig::default());
553 assert_eq!(session.state(), TwoPassState::FirstPass);
554
555 let frame = FirstPassFrameStats::new(0, 1.5, 0.8);
556 session
557 .record_first_pass_frame(frame)
558 .expect("should succeed");
559 session.finish_first_pass();
560 assert_eq!(session.state(), TwoPassState::AllocationReady);
561
562 session.begin_second_pass().expect("should succeed");
563 assert_eq!(session.state(), TwoPassState::SecondPass);
564 }
565
566 #[test]
567 fn test_session_second_pass_iterates() {
568 let mut session = TwoPassSession::new(TwoPassAllocatorConfig::default());
569 for i in 0..5u64 {
570 let f = FirstPassFrameStats::new(i, 1.0, 0.5);
571 session.record_first_pass_frame(f).expect("ok");
572 }
573 session.finish_first_pass();
574 session.begin_second_pass().expect("ok");
575
576 let mut count = 0;
577 while let Some(_alloc) = session.next_frame_allocation() {
578 count += 1;
579 }
580 assert_eq!(count, 5);
581 assert_eq!(session.state(), TwoPassState::Complete);
582 }
583
584 #[test]
585 fn test_first_pass_record_after_finish_errors() {
586 let mut session = TwoPassSession::new(TwoPassAllocatorConfig::default());
587 session.finish_first_pass();
588 let f = FirstPassFrameStats::new(0, 1.0, 0.5);
589 assert!(session.record_first_pass_frame(f).is_err());
590 }
591
592 #[test]
593 fn test_scene_change_flagged_on_high_ratio() {
594 let stats = FirstPassFrameStats::new(0, 5.0, 0.5);
595 assert!(stats.is_scene_change);
597 }
598
599 #[test]
600 fn test_scene_change_not_flagged_on_low_ratio() {
601 let stats = FirstPassFrameStats::new(0, 1.0, 0.8);
602 assert!(!stats.is_scene_change);
604 }
605
606 #[test]
607 fn test_mean_intra_inter_ratio() {
608 let mut s = TwoPassFirstPassStats::new();
609 s.push(FirstPassFrameStats::new(0, 2.0, 1.0)); s.push(FirstPassFrameStats::new(1, 4.0, 1.0)); let mean = s.mean_intra_inter_ratio();
612 assert!((mean - 3.0).abs() < 0.01);
613 }
614
615 #[test]
617 fn two_pass_tracker_new_is_empty() {
618 let t = TwoPassStateTracker::new();
619 assert_eq!(t.frame_count(), 0);
620 }
621
622 #[test]
623 fn two_pass_tracker_record_adds_frames() {
624 let mut t = TwoPassStateTracker::new();
625 t.record_first_pass(1.0);
626 t.record_first_pass(2.0);
627 assert_eq!(t.frame_count(), 2);
628 }
629
630 #[test]
631 fn two_pass_tracker_compute_bitrate_proportional() {
632 let mut t = TwoPassStateTracker::new();
633 t.record_first_pass(1.0);
634 t.record_first_pass(1.0);
635 let bits = t.compute_bitrate(1.0, 1000);
637 assert_eq!(bits, 500);
638 }
639
640 #[test]
641 fn two_pass_tracker_compute_bitrate_empty_returns_budget() {
642 let t = TwoPassStateTracker::new();
643 let bits = t.compute_bitrate(1.0, 999);
644 assert_eq!(bits, 999);
645 }
646
647 #[test]
648 fn two_pass_tracker_compute_bitrate_at_least_one() {
649 let mut t = TwoPassStateTracker::new();
650 t.record_first_pass(1000.0);
651 let bits = t.compute_bitrate(0.001, 1000);
653 assert!(bits >= 1);
654 }
655
656 #[test]
657 fn two_pass_tracker_total_complexity() {
658 let mut t = TwoPassStateTracker::new();
659 t.record_first_pass(2.0);
660 t.record_first_pass(3.0);
661 assert!((t.total_complexity() - 5.0).abs() < 1e-5);
662 }
663
664 #[test]
665 fn two_pass_tracker_mean_complexity() {
666 let mut t = TwoPassStateTracker::new();
667 t.record_first_pass(4.0);
668 t.record_first_pass(6.0);
669 assert!((t.mean_complexity() - 5.0).abs() < 1e-5);
670 }
671}