1#![allow(dead_code)]
2use std::collections::VecDeque;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum SceneType {
19 Static,
21 Dialogue,
23 Moderate,
25 Action,
27 HighDetail,
29 DarkScene,
31 Transition,
33}
34
35#[derive(Debug, Clone)]
37pub struct SceneMetrics {
38 pub scene_index: u32,
40 pub start_frame: u64,
42 pub end_frame: u64,
44 pub spatial_complexity: f64,
46 pub temporal_complexity: f64,
48 pub avg_luminance: f64,
50 pub luminance_variance: f64,
52 pub texture_density: f64,
54 pub scene_type: SceneType,
56}
57
58impl SceneMetrics {
59 #[must_use]
61 pub fn new(scene_index: u32, start_frame: u64, end_frame: u64) -> Self {
62 Self {
63 scene_index,
64 start_frame,
65 end_frame,
66 spatial_complexity: 0.5,
67 temporal_complexity: 0.5,
68 avg_luminance: 0.5,
69 luminance_variance: 0.1,
70 texture_density: 0.5,
71 scene_type: SceneType::Moderate,
72 }
73 }
74
75 #[must_use]
77 pub fn frame_count(&self) -> u64 {
78 self.end_frame.saturating_sub(self.start_frame)
79 }
80
81 #[must_use]
83 pub fn combined_complexity(&self) -> f64 {
84 (self.spatial_complexity * 0.4
85 + self.temporal_complexity * 0.4
86 + self.texture_density * 0.2)
87 .clamp(0.0, 1.0)
88 }
89
90 #[must_use]
92 pub fn is_dark(&self) -> bool {
93 self.avg_luminance < 0.2
94 }
95}
96
97#[derive(Debug, Clone)]
99pub struct SceneEncodeParams {
100 pub qp_offset: f64,
102 pub bitrate_weight: f64,
104 pub gop_size: u32,
106 pub force_keyframe: bool,
108 pub min_qp: f64,
110 pub max_qp: f64,
112 pub b_frames: u32,
114 pub enable_aq: bool,
116 pub aq_strength: f64,
118}
119
120impl Default for SceneEncodeParams {
121 fn default() -> Self {
122 Self {
123 qp_offset: 0.0,
124 bitrate_weight: 1.0,
125 gop_size: 250,
126 force_keyframe: true,
127 min_qp: 0.0,
128 max_qp: 51.0,
129 b_frames: 3,
130 enable_aq: true,
131 aq_strength: 1.0,
132 }
133 }
134}
135
136impl SceneEncodeParams {
137 #[must_use]
139 pub fn new() -> Self {
140 Self::default()
141 }
142
143 #[must_use]
145 pub fn effective_qp(&self, base_qp: f64) -> f64 {
146 (base_qp + self.qp_offset).clamp(self.min_qp, self.max_qp)
147 }
148}
149
150#[derive(Debug)]
152pub struct SceneEncoder {
153 base_qp: f64,
155 target_bitrate_bps: u64,
157 frame_rate: f64,
159 scene_params: Vec<SceneEncodeParams>,
161 scene_history: VecDeque<SceneMetrics>,
163 max_history: usize,
165}
166
167impl SceneEncoder {
168 #[must_use]
170 pub fn new(base_qp: f64, target_bitrate_bps: u64, frame_rate: f64) -> Self {
171 Self {
172 base_qp,
173 target_bitrate_bps,
174 frame_rate,
175 scene_params: Vec::new(),
176 scene_history: VecDeque::new(),
177 max_history: 100,
178 }
179 }
180
181 #[must_use]
183 pub fn base_qp(&self) -> f64 {
184 self.base_qp
185 }
186
187 #[must_use]
189 pub fn target_bitrate_bps(&self) -> u64 {
190 self.target_bitrate_bps
191 }
192
193 #[must_use]
195 pub fn generate_params(&self, metrics: &SceneMetrics) -> SceneEncodeParams {
196 let mut params = SceneEncodeParams::default();
197
198 params.qp_offset = self.compute_qp_offset(metrics);
200 params.bitrate_weight = self.compute_bitrate_weight(metrics);
201 params.gop_size = self.compute_gop_size(metrics);
202 params.b_frames = self.compute_b_frames(metrics);
203 params.aq_strength = self.compute_aq_strength(metrics);
204
205 params.force_keyframe = true;
207
208 params
209 }
210
211 pub fn process_scene(&mut self, metrics: SceneMetrics) -> SceneEncodeParams {
213 let params = self.generate_params(&metrics);
214 self.scene_params.push(params.clone());
215 self.scene_history.push_back(metrics);
216 if self.scene_history.len() > self.max_history {
217 self.scene_history.pop_front();
218 }
219 params
220 }
221
222 #[must_use]
224 pub fn scene_params(&self) -> &[SceneEncodeParams] {
225 &self.scene_params
226 }
227
228 #[must_use]
230 pub fn scenes_processed(&self) -> usize {
231 self.scene_params.len()
232 }
233
234 #[must_use]
236 pub fn scene_history(&self) -> &VecDeque<SceneMetrics> {
237 &self.scene_history
238 }
239
240 fn compute_qp_offset(&self, metrics: &SceneMetrics) -> f64 {
242 match metrics.scene_type {
243 SceneType::Static => -4.0, SceneType::Dialogue => -2.0, SceneType::Moderate => 0.0, SceneType::Action => 2.0, SceneType::HighDetail => 1.0, SceneType::DarkScene => -3.0, SceneType::Transition => 3.0, }
251 }
252
253 fn compute_bitrate_weight(&self, metrics: &SceneMetrics) -> f64 {
255 let complexity = metrics.combined_complexity();
256 0.6 + complexity * 1.2
258 }
259
260 #[allow(clippy::cast_possible_truncation)]
262 #[allow(clippy::cast_sign_loss)]
263 fn compute_gop_size(&self, metrics: &SceneMetrics) -> u32 {
264 let frame_count = metrics.frame_count();
265 let max_gop = frame_count.min(300) as u32;
267
268 match metrics.scene_type {
269 SceneType::Static => max_gop.min(300),
270 SceneType::Dialogue => max_gop.min(250),
271 SceneType::Moderate => max_gop.min(200),
272 SceneType::Action => max_gop.min(120),
273 SceneType::HighDetail => max_gop.min(150),
274 SceneType::DarkScene => max_gop.min(250),
275 SceneType::Transition => max_gop.min(60),
276 }
277 }
278
279 fn compute_b_frames(&self, metrics: &SceneMetrics) -> u32 {
281 match metrics.scene_type {
282 SceneType::Static => 5,
283 SceneType::Dialogue => 4,
284 SceneType::Moderate => 3,
285 SceneType::Action => 2,
286 SceneType::HighDetail => 3,
287 SceneType::DarkScene => 4,
288 SceneType::Transition => 1,
289 }
290 }
291
292 fn compute_aq_strength(&self, metrics: &SceneMetrics) -> f64 {
294 if metrics.is_dark() {
295 1.5
297 } else if metrics.spatial_complexity > 0.7 {
298 1.2
299 } else {
300 1.0
301 }
302 }
303
304 #[must_use]
306 #[allow(clippy::cast_precision_loss)]
307 pub fn avg_scene_complexity(&self) -> f64 {
308 if self.scene_history.is_empty() {
309 return 0.0;
310 }
311 let total: f64 = self
312 .scene_history
313 .iter()
314 .map(|m| m.combined_complexity())
315 .sum();
316 total / self.scene_history.len() as f64
317 }
318}
319
320#[derive(Debug)]
327pub struct LookaheadSceneQp {
328 lookahead_depth: usize,
330 base_qp: f64,
332 complexity_buffer: VecDeque<FrameLookaheadInfo>,
334 qp_history: VecDeque<f64>,
336 max_qp_delta: f64,
338 smoothing: f64,
340 scene_cut_boost: f64,
342 frames_processed: u64,
344}
345
346#[derive(Debug, Clone)]
348pub struct FrameLookaheadInfo {
349 pub frame_index: u64,
351 pub spatial_complexity: f64,
353 pub temporal_complexity: f64,
355 pub is_scene_cut: bool,
357 pub avg_luminance: f64,
359}
360
361impl FrameLookaheadInfo {
362 pub fn new(frame_index: u64) -> Self {
364 Self {
365 frame_index,
366 spatial_complexity: 0.5,
367 temporal_complexity: 0.5,
368 is_scene_cut: false,
369 avg_luminance: 0.5,
370 }
371 }
372
373 pub fn combined_complexity(&self) -> f64 {
375 (self.spatial_complexity * 0.5 + self.temporal_complexity * 0.5).clamp(0.0, 1.0)
376 }
377}
378
379#[derive(Debug, Clone)]
381pub struct LookaheadQpResult {
382 pub recommended_qp: f64,
384 pub qp_delta: f64,
386 pub near_scene_cut: bool,
388 pub distance_to_next_cut: Option<usize>,
390 pub upcoming_complexity: f64,
392}
393
394impl LookaheadSceneQp {
395 pub fn new(lookahead_depth: usize, base_qp: f64) -> Self {
397 Self {
398 lookahead_depth: lookahead_depth.max(1),
399 base_qp,
400 complexity_buffer: VecDeque::new(),
401 qp_history: VecDeque::new(),
402 max_qp_delta: 6.0,
403 smoothing: 0.3,
404 scene_cut_boost: 3.0,
405 frames_processed: 0,
406 }
407 }
408
409 pub fn set_max_qp_delta(&mut self, delta: f64) {
411 self.max_qp_delta = delta.max(0.0);
412 }
413
414 pub fn set_scene_cut_boost(&mut self, boost: f64) {
416 self.scene_cut_boost = boost.max(0.0);
417 }
418
419 pub fn set_smoothing(&mut self, smoothing: f64) {
421 self.smoothing = smoothing.clamp(0.0, 1.0);
422 }
423
424 pub fn feed_frame(&mut self, info: FrameLookaheadInfo) {
426 self.complexity_buffer.push_back(info);
427 while self.complexity_buffer.len() > self.lookahead_depth * 2 {
429 self.complexity_buffer.pop_front();
430 }
431 }
432
433 #[allow(clippy::cast_precision_loss)]
442 pub fn analyze_frame(&mut self, current: &FrameLookaheadInfo) -> LookaheadQpResult {
443 let distance_to_cut = self.find_next_scene_cut();
445 let near_scene_cut =
446 current.is_scene_cut || distance_to_cut.map(|d| d < 3).unwrap_or(false);
447
448 let upcoming_complexity = self.compute_upcoming_complexity();
450
451 let complexity_delta = self.complexity_to_qp_delta(upcoming_complexity);
453
454 let scene_cut_delta = if current.is_scene_cut {
456 -self.scene_cut_boost
458 } else if let Some(dist) = distance_to_cut {
459 if dist <= 2 {
460 let ramp_factor = dist as f64 / 3.0;
462 self.scene_cut_boost * 0.3 * ramp_factor
463 } else {
464 0.0
465 }
466 } else {
467 0.0
468 };
469
470 let dark_delta = if current.avg_luminance < 0.15 {
472 -1.5 } else {
474 0.0
475 };
476
477 let raw_delta = complexity_delta + scene_cut_delta + dark_delta;
479 let clamped_delta = raw_delta.clamp(-self.max_qp_delta, self.max_qp_delta);
480
481 let smoothed_delta = if let Some(&last_qp) = self.qp_history.back() {
483 let last_delta = last_qp - self.base_qp;
484 if current.is_scene_cut {
485 clamped_delta
487 } else {
488 last_delta * self.smoothing + clamped_delta * (1.0 - self.smoothing)
489 }
490 } else {
491 clamped_delta
492 };
493
494 let recommended_qp = (self.base_qp + smoothed_delta).clamp(1.0, 51.0);
495
496 self.qp_history.push_back(recommended_qp);
498 if self.qp_history.len() > self.lookahead_depth {
499 self.qp_history.pop_front();
500 }
501 self.frames_processed += 1;
502
503 LookaheadQpResult {
504 recommended_qp,
505 qp_delta: smoothed_delta,
506 near_scene_cut,
507 distance_to_next_cut: distance_to_cut,
508 upcoming_complexity,
509 }
510 }
511
512 pub fn frames_processed(&self) -> u64 {
514 self.frames_processed
515 }
516
517 pub fn reset(&mut self) {
519 self.complexity_buffer.clear();
520 self.qp_history.clear();
521 self.frames_processed = 0;
522 }
523
524 fn find_next_scene_cut(&self) -> Option<usize> {
526 for (i, info) in self.complexity_buffer.iter().enumerate() {
527 if info.is_scene_cut && i > 0 {
528 return Some(i);
529 }
530 }
531 None
532 }
533
534 #[allow(clippy::cast_precision_loss)]
536 fn compute_upcoming_complexity(&self) -> f64 {
537 if self.complexity_buffer.is_empty() {
538 return 0.5;
539 }
540 let count = self.complexity_buffer.len().min(self.lookahead_depth);
541 let sum: f64 = self
542 .complexity_buffer
543 .iter()
544 .take(count)
545 .map(|f| f.combined_complexity())
546 .sum();
547 sum / count as f64
548 }
549
550 fn complexity_to_qp_delta(&self, complexity: f64) -> f64 {
552 let centered = complexity - 0.5;
555 -centered * self.max_qp_delta * 2.0
556 }
557}
558
559#[derive(Debug, Clone)]
561pub struct SceneBitratePlan {
562 allocations: Vec<SceneBitrateAlloc>,
564}
565
566#[derive(Debug, Clone)]
568pub struct SceneBitrateAlloc {
569 pub scene_index: u32,
571 pub allocated_bps: u64,
573 pub frame_count: u64,
575 pub total_bits: u64,
577}
578
579impl SceneBitratePlan {
580 #[must_use]
582 pub fn new() -> Self {
583 Self {
584 allocations: Vec::new(),
585 }
586 }
587
588 #[must_use]
590 #[allow(clippy::cast_precision_loss)]
591 pub fn distribute(
592 scenes: &[SceneMetrics],
593 params: &[SceneEncodeParams],
594 total_bitrate_bps: u64,
595 fps: f64,
596 ) -> Self {
597 if scenes.is_empty() || params.is_empty() || fps <= 0.0 {
598 return Self::new();
599 }
600
601 let len = scenes.len().min(params.len());
602
603 let weighted_total: f64 = (0..len)
605 .map(|i| scenes[i].frame_count() as f64 * params[i].bitrate_weight)
606 .sum();
607
608 if weighted_total <= 0.0 {
609 return Self::new();
610 }
611
612 let total_frames: u64 = scenes[..len].iter().map(|s| s.frame_count()).sum();
613 let total_bits = (total_bitrate_bps as f64 * total_frames as f64 / fps) as u64;
614
615 let mut allocations = Vec::with_capacity(len);
616 for i in 0..len {
617 let weight = scenes[i].frame_count() as f64 * params[i].bitrate_weight / weighted_total;
618 let scene_bits = (total_bits as f64 * weight) as u64;
619 let scene_bps = if scenes[i].frame_count() > 0 {
620 (scene_bits as f64 * fps / scenes[i].frame_count() as f64) as u64
621 } else {
622 0
623 };
624
625 allocations.push(SceneBitrateAlloc {
626 scene_index: scenes[i].scene_index,
627 allocated_bps: scene_bps,
628 frame_count: scenes[i].frame_count(),
629 total_bits: scene_bits,
630 });
631 }
632
633 Self { allocations }
634 }
635
636 #[must_use]
638 pub fn allocations(&self) -> &[SceneBitrateAlloc] {
639 &self.allocations
640 }
641
642 #[must_use]
644 pub fn len(&self) -> usize {
645 self.allocations.len()
646 }
647
648 #[must_use]
650 pub fn is_empty(&self) -> bool {
651 self.allocations.is_empty()
652 }
653
654 #[must_use]
656 pub fn total_bits(&self) -> u64 {
657 self.allocations.iter().map(|a| a.total_bits).sum()
658 }
659}
660
661impl Default for SceneBitratePlan {
662 fn default() -> Self {
663 Self::new()
664 }
665}
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670
671 fn make_scene(index: u32, start: u64, end: u64, st: SceneType) -> SceneMetrics {
672 let mut m = SceneMetrics::new(index, start, end);
673 m.scene_type = st;
674 m.spatial_complexity = match st {
675 SceneType::Static => 0.1,
676 SceneType::Dialogue => 0.3,
677 SceneType::Moderate => 0.5,
678 SceneType::Action => 0.8,
679 SceneType::HighDetail => 0.9,
680 SceneType::DarkScene => 0.4,
681 SceneType::Transition => 0.6,
682 };
683 m.temporal_complexity = match st {
684 SceneType::Static => 0.05,
685 SceneType::Dialogue => 0.2,
686 SceneType::Moderate => 0.5,
687 SceneType::Action => 0.9,
688 SceneType::HighDetail => 0.4,
689 SceneType::DarkScene => 0.3,
690 SceneType::Transition => 0.7,
691 };
692 if st == SceneType::DarkScene {
693 m.avg_luminance = 0.1;
694 }
695 m
696 }
697
698 #[test]
699 fn test_scene_metrics_frame_count() {
700 let m = SceneMetrics::new(0, 10, 50);
701 assert_eq!(m.frame_count(), 40);
702 }
703
704 #[test]
705 fn test_scene_metrics_combined_complexity() {
706 let mut m = SceneMetrics::new(0, 0, 100);
707 m.spatial_complexity = 0.8;
708 m.temporal_complexity = 0.6;
709 m.texture_density = 0.5;
710 let cc = m.combined_complexity();
711 assert!((cc - 0.66).abs() < 1e-6);
713 }
714
715 #[test]
716 fn test_scene_metrics_is_dark() {
717 let mut m = SceneMetrics::new(0, 0, 100);
718 m.avg_luminance = 0.1;
719 assert!(m.is_dark());
720 m.avg_luminance = 0.5;
721 assert!(!m.is_dark());
722 }
723
724 #[test]
725 fn test_scene_encode_params_default() {
726 let p = SceneEncodeParams::default();
727 assert!((p.qp_offset - 0.0).abs() < 1e-9);
728 assert!((p.bitrate_weight - 1.0).abs() < 1e-9);
729 assert_eq!(p.gop_size, 250);
730 }
731
732 #[test]
733 fn test_effective_qp() {
734 let p = SceneEncodeParams {
735 qp_offset: -3.0,
736 min_qp: 10.0,
737 max_qp: 45.0,
738 ..SceneEncodeParams::default()
739 };
740 assert!((p.effective_qp(28.0) - 25.0).abs() < 1e-9);
741 assert!((p.effective_qp(11.0) - 10.0).abs() < 1e-9);
743 }
744
745 #[test]
746 fn test_scene_encoder_new() {
747 let enc = SceneEncoder::new(26.0, 5_000_000, 30.0);
748 assert!((enc.base_qp() - 26.0).abs() < 1e-9);
749 assert_eq!(enc.target_bitrate_bps(), 5_000_000);
750 }
751
752 #[test]
753 fn test_generate_params_static() {
754 let enc = SceneEncoder::new(26.0, 5_000_000, 30.0);
755 let scene = make_scene(0, 0, 300, SceneType::Static);
756 let params = enc.generate_params(&scene);
757 assert!(params.qp_offset < 0.0); assert!(params.gop_size <= 300);
759 }
760
761 #[test]
762 fn test_generate_params_action() {
763 let enc = SceneEncoder::new(26.0, 5_000_000, 30.0);
764 let scene = make_scene(0, 0, 200, SceneType::Action);
765 let params = enc.generate_params(&scene);
766 assert!(params.qp_offset > 0.0); assert!(params.b_frames <= 3);
768 }
769
770 #[test]
771 fn test_generate_params_dark() {
772 let enc = SceneEncoder::new(26.0, 5_000_000, 30.0);
773 let scene = make_scene(0, 0, 200, SceneType::DarkScene);
774 let params = enc.generate_params(&scene);
775 assert!(params.aq_strength > 1.0); }
777
778 #[test]
779 fn test_process_scene() {
780 let mut enc = SceneEncoder::new(26.0, 5_000_000, 30.0);
781 let scene = make_scene(0, 0, 100, SceneType::Moderate);
782 let _params = enc.process_scene(scene);
783 assert_eq!(enc.scenes_processed(), 1);
784 }
785
786 #[test]
787 fn test_avg_scene_complexity() {
788 let mut enc = SceneEncoder::new(26.0, 5_000_000, 30.0);
789 enc.process_scene(make_scene(0, 0, 100, SceneType::Static));
790 enc.process_scene(make_scene(1, 100, 200, SceneType::Action));
791 let avg = enc.avg_scene_complexity();
792 assert!(avg > 0.0 && avg < 1.0);
793 }
794
795 #[test]
796 fn test_bitrate_plan_distribute() {
797 let scenes = vec![
798 make_scene(0, 0, 100, SceneType::Static),
799 make_scene(1, 100, 300, SceneType::Action),
800 ];
801 let enc = SceneEncoder::new(26.0, 5_000_000, 30.0);
802 let params: Vec<_> = scenes.iter().map(|s| enc.generate_params(s)).collect();
803
804 let plan = SceneBitratePlan::distribute(&scenes, ¶ms, 5_000_000, 30.0);
805 assert_eq!(plan.len(), 2);
806 assert!(!plan.is_empty());
807 assert!(plan.total_bits() > 0);
808 }
809
810 #[test]
811 fn test_bitrate_plan_empty() {
812 let plan = SceneBitratePlan::distribute(&[], &[], 5_000_000, 30.0);
813 assert!(plan.is_empty());
814 assert_eq!(plan.total_bits(), 0);
815 }
816
817 #[test]
818 fn test_bitrate_plan_action_gets_more_bits() {
819 let scenes = vec![
820 make_scene(0, 0, 100, SceneType::Static),
821 make_scene(1, 100, 200, SceneType::Action),
822 ];
823 let enc = SceneEncoder::new(26.0, 5_000_000, 30.0);
824 let params: Vec<_> = scenes.iter().map(|s| enc.generate_params(s)).collect();
825
826 let plan = SceneBitratePlan::distribute(&scenes, ¶ms, 5_000_000, 30.0);
827 let allocs = plan.allocations();
828 assert!(allocs[1].allocated_bps > allocs[0].allocated_bps);
830 }
831
832 #[test]
835 fn test_lookahead_scene_qp_creation() {
836 let la = LookaheadSceneQp::new(20, 28.0);
837 assert_eq!(la.frames_processed(), 0);
838 assert!((la.base_qp - 28.0).abs() < f64::EPSILON);
839 }
840
841 #[test]
842 fn test_lookahead_scene_qp_no_lookahead() {
843 let mut la = LookaheadSceneQp::new(10, 28.0);
844 let frame = FrameLookaheadInfo::new(0);
845 let result = la.analyze_frame(&frame);
846 assert!(
848 (result.recommended_qp - 28.0).abs() < 7.0,
849 "QP should be near base: {}",
850 result.recommended_qp
851 );
852 }
853
854 #[test]
855 fn test_lookahead_scene_cut_boost() {
856 let mut la = LookaheadSceneQp::new(10, 28.0);
857 la.set_scene_cut_boost(4.0);
858
859 for i in 0..5 {
861 let mut info = FrameLookaheadInfo::new(i);
862 info.spatial_complexity = 0.5;
863 info.temporal_complexity = 0.5;
864 la.feed_frame(info.clone());
865 la.analyze_frame(&info);
866 }
867
868 let mut cut_frame = FrameLookaheadInfo::new(5);
870 cut_frame.is_scene_cut = true;
871 cut_frame.spatial_complexity = 0.6;
872 la.feed_frame(cut_frame.clone());
873 let result = la.analyze_frame(&cut_frame);
874
875 assert!(result.near_scene_cut, "Should detect scene cut");
876 assert!(
878 result.qp_delta < 0.0,
879 "Scene cut should produce negative QP delta: {}",
880 result.qp_delta
881 );
882 }
883
884 #[test]
885 fn test_lookahead_upcoming_complexity_high() {
886 let mut la = LookaheadSceneQp::new(10, 28.0);
887
888 for i in 0..10 {
890 let mut info = FrameLookaheadInfo::new(i);
891 info.spatial_complexity = 0.9;
892 info.temporal_complexity = 0.8;
893 la.feed_frame(info);
894 }
895
896 let frame = FrameLookaheadInfo::new(0);
897 let result = la.analyze_frame(&frame);
898 assert!(
900 result.upcoming_complexity > 0.7,
901 "Upcoming complexity should be high: {}",
902 result.upcoming_complexity
903 );
904 assert!(
905 result.qp_delta < 0.0,
906 "High complexity should lower QP: {}",
907 result.qp_delta
908 );
909 }
910
911 #[test]
912 fn test_lookahead_upcoming_complexity_low() {
913 let mut la = LookaheadSceneQp::new(10, 28.0);
914
915 for i in 0..10 {
917 let mut info = FrameLookaheadInfo::new(i);
918 info.spatial_complexity = 0.1;
919 info.temporal_complexity = 0.1;
920 la.feed_frame(info);
921 }
922
923 let frame = FrameLookaheadInfo::new(0);
924 let result = la.analyze_frame(&frame);
925 assert!(
927 result.upcoming_complexity < 0.3,
928 "Upcoming complexity should be low: {}",
929 result.upcoming_complexity
930 );
931 assert!(
932 result.qp_delta > 0.0,
933 "Low complexity should raise QP: {}",
934 result.qp_delta
935 );
936 }
937
938 #[test]
939 fn test_lookahead_distance_to_cut() {
940 let mut la = LookaheadSceneQp::new(10, 28.0);
941
942 for i in 0..5 {
944 la.feed_frame(FrameLookaheadInfo::new(i));
945 }
946 let mut cut = FrameLookaheadInfo::new(5);
947 cut.is_scene_cut = true;
948 la.feed_frame(cut);
949
950 let frame = FrameLookaheadInfo::new(0);
951 let result = la.analyze_frame(&frame);
952 assert!(
953 result.distance_to_next_cut.is_some(),
954 "Should find upcoming scene cut"
955 );
956 }
957
958 #[test]
959 fn test_lookahead_dark_scene_boost() {
960 let mut la = LookaheadSceneQp::new(10, 28.0);
961
962 let mut dark_frame = FrameLookaheadInfo::new(0);
963 dark_frame.avg_luminance = 0.1;
964 dark_frame.spatial_complexity = 0.5;
965 dark_frame.temporal_complexity = 0.5;
966 la.feed_frame(dark_frame.clone());
967
968 let result = la.analyze_frame(&dark_frame);
969 assert!(
971 result.recommended_qp < 28.0 + 1.0,
972 "Dark scene should get lower QP: {}",
973 result.recommended_qp
974 );
975 }
976
977 #[test]
978 fn test_lookahead_qp_clamped() {
979 let mut la = LookaheadSceneQp::new(10, 28.0);
980 la.set_max_qp_delta(3.0);
981
982 for i in 0..10 {
984 let mut info = FrameLookaheadInfo::new(i);
985 info.spatial_complexity = 1.0;
986 info.temporal_complexity = 1.0;
987 la.feed_frame(info);
988 }
989
990 let frame = FrameLookaheadInfo::new(0);
991 let result = la.analyze_frame(&frame);
992 assert!(
993 result.qp_delta >= -3.0 && result.qp_delta <= 3.0,
994 "QP delta should be clamped to max: {}",
995 result.qp_delta
996 );
997 assert!(result.recommended_qp >= 1.0 && result.recommended_qp <= 51.0);
998 }
999
1000 #[test]
1001 fn test_lookahead_smoothing() {
1002 let mut la = LookaheadSceneQp::new(10, 28.0);
1003 la.set_smoothing(0.5);
1004
1005 let mut info1 = FrameLookaheadInfo::new(0);
1007 info1.spatial_complexity = 0.1;
1008 info1.temporal_complexity = 0.1;
1009 la.feed_frame(info1.clone());
1010 let r1 = la.analyze_frame(&info1);
1011
1012 let mut info2 = FrameLookaheadInfo::new(1);
1014 info2.spatial_complexity = 0.9;
1015 info2.temporal_complexity = 0.9;
1016 la.feed_frame(info2.clone());
1017 let r2 = la.analyze_frame(&info2);
1018
1019 let delta_change = (r2.qp_delta - r1.qp_delta).abs();
1021 assert!(
1022 delta_change < 12.0,
1023 "Smoothing should dampen QP changes: delta_change={}",
1024 delta_change
1025 );
1026 }
1027
1028 #[test]
1029 fn test_lookahead_reset() {
1030 let mut la = LookaheadSceneQp::new(10, 28.0);
1031 la.feed_frame(FrameLookaheadInfo::new(0));
1032 la.analyze_frame(&FrameLookaheadInfo::new(0));
1033 la.reset();
1034 assert_eq!(la.frames_processed(), 0);
1035 }
1036
1037 #[test]
1038 fn test_frame_lookahead_info_combined_complexity() {
1039 let mut info = FrameLookaheadInfo::new(0);
1040 info.spatial_complexity = 0.8;
1041 info.temporal_complexity = 0.4;
1042 let cc = info.combined_complexity();
1043 assert!((cc - 0.6).abs() < 0.01);
1044 }
1045
1046 #[test]
1047 fn test_lookahead_multiple_scene_cuts() {
1048 let mut la = LookaheadSceneQp::new(20, 28.0);
1049
1050 for i in 0..5 {
1052 la.feed_frame(FrameLookaheadInfo::new(i));
1053 }
1054 let mut cut1 = FrameLookaheadInfo::new(5);
1055 cut1.is_scene_cut = true;
1056 la.feed_frame(cut1);
1057 for i in 6..10 {
1058 la.feed_frame(FrameLookaheadInfo::new(i));
1059 }
1060 let mut cut2 = FrameLookaheadInfo::new(10);
1061 cut2.is_scene_cut = true;
1062 la.feed_frame(cut2);
1063
1064 let frame = FrameLookaheadInfo::new(0);
1065 let result = la.analyze_frame(&frame);
1066 assert!(result.distance_to_next_cut.is_some());
1068 }
1069}