1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum SceneType {
14 Static,
16 SlowMotion,
18 ActionFast,
20 Talking,
22 Credits,
24 Animation,
26 HighComplexity,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct SceneSegment {
35 pub start_frame: u64,
37 pub end_frame: u64,
39 pub duration_frames: u32,
41 pub motion_score: f32,
43 pub complexity: f32,
45 pub is_dark: bool,
47 pub scene_type: SceneType,
49}
50
51impl SceneSegment {
52 #[must_use]
54 pub fn new(
55 start_frame: u64,
56 end_frame: u64,
57 duration_frames: u32,
58 motion_score: f32,
59 complexity: f32,
60 is_dark: bool,
61 scene_type: SceneType,
62 ) -> Self {
63 Self {
64 start_frame,
65 end_frame,
66 duration_frames,
67 motion_score,
68 complexity,
69 is_dark,
70 scene_type,
71 }
72 }
73
74 #[must_use]
76 pub fn frame_count(&self) -> u32 {
77 self.duration_frames
78 }
79
80 #[must_use]
82 pub fn duration_secs(&self, fps: f32) -> f32 {
83 if fps <= 0.0 {
84 return 0.0;
85 }
86 self.duration_frames as f32 / fps
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct SceneEncodeParams {
95 pub crf: u8,
97 pub bitrate_kbps: u32,
99 pub max_bitrate_kbps: u32,
101 pub b_frames: u8,
103 pub ref_frames: u8,
105 pub preset: String,
107 pub gop_size: u32,
109 pub tile_cols: u8,
111 pub tile_rows: u8,
113}
114
115impl SceneEncodeParams {
116 #[must_use]
118 pub fn is_valid(&self) -> bool {
119 self.bitrate_kbps > 0
120 && self.max_bitrate_kbps >= self.bitrate_kbps
121 && !self.preset.is_empty()
122 && self.gop_size > 0
123 }
124}
125
126#[derive(Debug, Clone, Default)]
130pub struct PerSceneEncoder;
131
132impl PerSceneEncoder {
133 #[must_use]
135 pub fn new() -> Self {
136 Self
137 }
138
139 #[must_use]
154 pub fn compute_params(
155 &self,
156 scene: &SceneSegment,
157 target_bitrate_kbps: u32,
158 codec: &str,
159 ) -> SceneEncodeParams {
160 let (base_crf, base_b_frames, base_ref_frames, base_preset, base_gop) =
162 Self::codec_base_values(codec);
163
164 let crf_adj = self.crf_adjustment(scene);
166
167 let raw_crf = base_crf as i32 + crf_adj;
169 let crf = raw_crf.clamp(0, 51) as u8;
170
171 let (b_frames, gop_mult) = self.motion_params(scene, base_b_frames);
173
174 let gop_size = ((base_gop as f32) * gop_mult).round() as u32;
175 let gop_size = gop_size.max(1);
176
177 let bitrate_scale = self.bitrate_scale(scene);
179 let bitrate_kbps = ((target_bitrate_kbps as f32) * bitrate_scale).round() as u32;
180 let bitrate_kbps = bitrate_kbps.max(50);
181 let max_bitrate_kbps = (bitrate_kbps as f32 * 2.0).round() as u32;
182
183 let (tile_cols, tile_rows) = Self::tile_layout(codec);
185
186 SceneEncodeParams {
187 crf,
188 bitrate_kbps,
189 max_bitrate_kbps,
190 b_frames,
191 ref_frames: base_ref_frames,
192 preset: base_preset.to_string(),
193 gop_size,
194 tile_cols,
195 tile_rows,
196 }
197 }
198
199 fn codec_base_values(codec: &str) -> (u8, u8, u8, &'static str, u32) {
201 match codec.to_lowercase().as_str() {
203 "av1" | "libaom-av1" | "svt-av1" => (35, 0, 3, "5", 240),
204 "vp9" | "libvpx-vp9" => (33, 0, 3, "good", 240),
205 "h265" | "hevc" | "libx265" => (28, 4, 4, "medium", 250),
206 "h264" | "avc" | "libx264" => (23, 3, 3, "medium", 250),
207 _ => (28, 2, 3, "medium", 250),
208 }
209 }
210
211 fn crf_adjustment(&self, scene: &SceneSegment) -> i32 {
213 let mut adj: i32 = 0;
214
215 match scene.scene_type {
216 SceneType::Static => adj -= 2,
217 SceneType::ActionFast => adj += 2,
218 SceneType::HighComplexity => adj += 3,
219 SceneType::Animation => adj -= 1,
220 SceneType::Credits => adj -= 2,
221 SceneType::SlowMotion => adj -= 1,
222 SceneType::Talking => {}
223 }
224
225 if scene.is_dark {
226 adj -= 3;
227 }
228
229 if scene.complexity > 0.8 {
231 adj += 1;
232 } else if scene.complexity < 0.2 {
233 adj -= 1;
234 }
235
236 adj
237 }
238
239 fn motion_params(&self, scene: &SceneSegment, base_b_frames: u8) -> (u8, f32) {
241 match scene.scene_type {
242 SceneType::Static => {
243 let b = base_b_frames.saturating_sub(1);
245 (b, 2.0)
246 }
247 SceneType::ActionFast => {
248 (0, 0.5)
250 }
251 SceneType::HighComplexity => {
252 let b = (base_b_frames + 2).min(8);
254 (b, 1.0)
255 }
256 SceneType::Credits => {
257 let b = base_b_frames.saturating_sub(1);
259 (b, 1.5)
260 }
261 SceneType::Animation => {
262 (base_b_frames, 1.5)
264 }
265 SceneType::SlowMotion | SceneType::Talking => (base_b_frames, 1.0),
266 }
267 }
268
269 fn bitrate_scale(&self, scene: &SceneSegment) -> f32 {
271 let mut scale = 0.5 + scene.complexity * 1.5; if scene.is_dark {
273 scale *= 0.85;
275 }
276 match scene.scene_type {
277 SceneType::Static => scale *= 0.6,
278 SceneType::ActionFast => scale *= 1.4,
279 SceneType::HighComplexity => scale *= 1.5,
280 _ => {}
281 }
282 scale.clamp(0.3, 3.0)
283 }
284
285 fn tile_layout(codec: &str) -> (u8, u8) {
287 match codec.to_lowercase().as_str() {
288 "av1" | "libaom-av1" | "svt-av1" => (4, 2),
289 "vp9" | "libvpx-vp9" => (2, 1),
290 "h265" | "hevc" | "libx265" => (2, 1),
291 _ => (1, 1),
292 }
293 }
294}
295
296#[must_use]
303pub fn estimate_output_size(params: &SceneEncodeParams, duration_frames: u32, fps: f32) -> u64 {
304 if fps <= 0.0 || duration_frames == 0 {
305 return 0;
306 }
307 let duration_secs = duration_frames as f64 / fps as f64;
308 let bytes_per_sec = params.bitrate_kbps as f64 * 1000.0 / 8.0;
309 let raw_bytes = bytes_per_sec * duration_secs;
310 (raw_bytes * 0.9) as u64
311}
312
313#[derive(Debug, Clone)]
318pub struct TargetSizeSolver {
319 pub target_bytes: u64,
321 pub duration_frames: u32,
323 pub fps: f32,
325}
326
327impl TargetSizeSolver {
328 #[must_use]
330 pub fn new(target_bytes: u64, duration_frames: u32, fps: f32) -> Self {
331 Self {
332 target_bytes,
333 duration_frames,
334 fps,
335 }
336 }
337
338 #[must_use]
346 pub fn solve_bitrate(&self, complexity_factor: f32) -> u32 {
347 if self.fps <= 0.0 || self.duration_frames == 0 || self.target_bytes == 0 {
348 return 0;
349 }
350
351 let duration_secs = self.duration_frames as f64 / self.fps as f64;
352 let fill = (0.9 / complexity_factor.max(0.1) as f64).clamp(0.3, 1.5);
354 let bitrate_bps = (self.target_bytes as f64 * 8.0) / (duration_secs * fill);
357 let bitrate_kbps = (bitrate_bps / 1000.0).round() as u32;
358 bitrate_kbps.max(1)
359 }
360}
361
362#[derive(Debug, Clone)]
367pub struct BudgetAllocator {
368 pub total_budget_bytes: u64,
370 pub scenes: Vec<SceneSegment>,
372}
373
374impl BudgetAllocator {
375 #[must_use]
377 pub fn new(total_budget_bytes: u64, scenes: Vec<SceneSegment>) -> Self {
378 Self {
379 total_budget_bytes,
380 scenes,
381 }
382 }
383
384 #[must_use]
391 pub fn allocate(&self) -> Vec<u64> {
392 if self.scenes.is_empty() {
393 return Vec::new();
394 }
395
396 let weights: Vec<f64> = self
397 .scenes
398 .iter()
399 .map(|s| {
400 let duration_weight = s.duration_frames as f64;
401 let complexity_weight = 0.5 + s.complexity as f64;
402 let motion_weight = 1.0 + s.motion_score as f64 * 0.5;
403 duration_weight * complexity_weight * motion_weight
404 })
405 .collect();
406
407 let total_weight: f64 = weights.iter().sum();
408
409 if total_weight <= 0.0 {
410 let per_scene = self.total_budget_bytes / self.scenes.len() as u64;
412 return vec![per_scene; self.scenes.len()];
413 }
414
415 let mut allocations: Vec<u64> = weights
416 .iter()
417 .map(|&w| {
418 let frac = w / total_weight;
419 (self.total_budget_bytes as f64 * frac).round() as u64
420 })
421 .collect();
422
423 let allocated_sum: u64 = allocations.iter().sum();
425 if allocated_sum > self.total_budget_bytes {
426 if let Some(max_idx) = allocations
428 .iter()
429 .enumerate()
430 .max_by_key(|(_, &v)| v)
431 .map(|(i, _)| i)
432 {
433 let excess = allocated_sum - self.total_budget_bytes;
434 allocations[max_idx] = allocations[max_idx].saturating_sub(excess);
435 }
436 }
437
438 allocations
439 }
440
441 #[must_use]
443 pub fn allocated_total(&self) -> u64 {
444 self.allocate().iter().sum()
445 }
446}
447
448#[cfg(test)]
451mod tests {
452 use super::*;
453
454 fn make_scene(
455 scene_type: SceneType,
456 complexity: f32,
457 motion: f32,
458 is_dark: bool,
459 ) -> SceneSegment {
460 SceneSegment::new(0, 239, 240, motion, complexity, is_dark, scene_type)
461 }
462
463 #[test]
466 fn test_scene_segment_new() {
467 let seg = SceneSegment::new(0, 299, 300, 0.3, 0.5, false, SceneType::Talking);
468 assert_eq!(seg.start_frame, 0);
469 assert_eq!(seg.end_frame, 299);
470 assert_eq!(seg.duration_frames, 300);
471 assert!((seg.motion_score - 0.3).abs() < 1e-6);
472 assert!(!seg.is_dark);
473 }
474
475 #[test]
476 fn test_scene_segment_duration_secs() {
477 let seg = make_scene(SceneType::Talking, 0.5, 0.3, false);
478 let dur = seg.duration_secs(30.0);
479 assert!((dur - 8.0).abs() < 0.01); }
481
482 #[test]
483 fn test_scene_segment_duration_secs_zero_fps() {
484 let seg = make_scene(SceneType::Talking, 0.5, 0.3, false);
485 assert_eq!(seg.duration_secs(0.0), 0.0);
486 }
487
488 #[test]
489 fn test_frame_count_alias() {
490 let seg = make_scene(SceneType::Static, 0.1, 0.0, false);
491 assert_eq!(seg.frame_count(), seg.duration_frames);
492 }
493
494 #[test]
497 fn test_static_scene_lower_crf_larger_gop() {
498 let enc = PerSceneEncoder::new();
499 let static_scene = make_scene(SceneType::Static, 0.3, 0.1, false);
500 let action_scene = make_scene(SceneType::ActionFast, 0.3, 0.9, false);
501 let p_static = enc.compute_params(&static_scene, 4000, "h264");
502 let p_action = enc.compute_params(&action_scene, 4000, "h264");
503 assert!(
504 p_static.crf < p_action.crf,
505 "Static CRF should be lower than ActionFast CRF"
506 );
507 assert!(
508 p_static.gop_size > p_action.gop_size,
509 "Static GOP should be larger"
510 );
511 }
512
513 #[test]
514 fn test_action_fast_no_b_frames() {
515 let enc = PerSceneEncoder::new();
516 let scene = make_scene(SceneType::ActionFast, 0.8, 0.95, false);
517 let params = enc.compute_params(&scene, 8000, "h264");
518 assert_eq!(params.b_frames, 0, "ActionFast should use no B-frames");
519 }
520
521 #[test]
522 fn test_dark_scene_lower_crf() {
523 let enc = PerSceneEncoder::new();
524 let dark = make_scene(SceneType::Talking, 0.5, 0.3, true);
525 let bright = make_scene(SceneType::Talking, 0.5, 0.3, false);
526 let p_dark = enc.compute_params(&dark, 4000, "h264");
527 let p_bright = enc.compute_params(&bright, 4000, "h264");
528 assert!(
529 p_dark.crf < p_bright.crf,
530 "Dark scene should have lower CRF"
531 );
532 }
533
534 #[test]
535 fn test_av1_tile_layout() {
536 let enc = PerSceneEncoder::new();
537 let scene = make_scene(SceneType::Talking, 0.5, 0.3, false);
538 let params = enc.compute_params(&scene, 4000, "av1");
539 assert_eq!(params.tile_cols, 4);
540 assert_eq!(params.tile_rows, 2);
541 }
542
543 #[test]
544 fn test_vp9_tile_layout() {
545 let enc = PerSceneEncoder::new();
546 let scene = make_scene(SceneType::Talking, 0.5, 0.3, false);
547 let params = enc.compute_params(&scene, 4000, "vp9");
548 assert_eq!(params.tile_cols, 2);
549 assert_eq!(params.tile_rows, 1);
550 }
551
552 #[test]
553 fn test_h265_tile_layout() {
554 let enc = PerSceneEncoder::new();
555 let scene = make_scene(SceneType::Talking, 0.5, 0.3, false);
556 let params = enc.compute_params(&scene, 4000, "h265");
557 assert_eq!(params.tile_cols, 2);
558 assert_eq!(params.tile_rows, 1);
559 }
560
561 #[test]
562 fn test_params_are_valid() {
563 let enc = PerSceneEncoder::new();
564 let scene = make_scene(SceneType::Talking, 0.5, 0.3, false);
565 let params = enc.compute_params(&scene, 4000, "vp9");
566 assert!(params.is_valid());
567 }
568
569 #[test]
570 fn test_max_bitrate_gte_avg_bitrate() {
571 let enc = PerSceneEncoder::new();
572 let scene = make_scene(SceneType::HighComplexity, 0.9, 0.8, false);
573 let params = enc.compute_params(&scene, 6000, "av1");
574 assert!(params.max_bitrate_kbps >= params.bitrate_kbps);
575 }
576
577 #[test]
578 fn test_crf_clamped_to_valid_range() {
579 let enc = PerSceneEncoder::new();
580 let scene = make_scene(SceneType::HighComplexity, 0.9, 0.9, true);
582 let params = enc.compute_params(&scene, 2000, "h264");
583 assert!(params.crf <= 51);
584 }
585
586 #[test]
589 fn test_estimate_output_size_basic() {
590 let params = SceneEncodeParams {
591 crf: 28,
592 bitrate_kbps: 1000,
593 max_bitrate_kbps: 2000,
594 b_frames: 3,
595 ref_frames: 3,
596 preset: "medium".to_string(),
597 gop_size: 250,
598 tile_cols: 1,
599 tile_rows: 1,
600 };
601 let size = estimate_output_size(¶ms, 300, 30.0);
603 assert!((size as i64 - 1_125_000).abs() < 1000);
604 }
605
606 #[test]
607 fn test_estimate_output_size_zero_fps() {
608 let params = SceneEncodeParams {
609 crf: 28,
610 bitrate_kbps: 1000,
611 max_bitrate_kbps: 2000,
612 b_frames: 3,
613 ref_frames: 3,
614 preset: "medium".to_string(),
615 gop_size: 250,
616 tile_cols: 1,
617 tile_rows: 1,
618 };
619 assert_eq!(estimate_output_size(¶ms, 300, 0.0), 0);
620 }
621
622 #[test]
623 fn test_estimate_output_size_zero_frames() {
624 let params = SceneEncodeParams {
625 crf: 28,
626 bitrate_kbps: 1000,
627 max_bitrate_kbps: 2000,
628 b_frames: 3,
629 ref_frames: 3,
630 preset: "medium".to_string(),
631 gop_size: 250,
632 tile_cols: 1,
633 tile_rows: 1,
634 };
635 assert_eq!(estimate_output_size(¶ms, 0, 30.0), 0);
636 }
637
638 #[test]
641 fn test_target_size_solver_basic() {
642 let solver = TargetSizeSolver::new(10_000_000, 300, 30.0);
644 let kbps = solver.solve_bitrate(1.0);
645 assert!(kbps > 7000 && kbps < 10000, "kbps={kbps} out of range");
647 }
648
649 #[test]
650 fn test_target_size_solver_high_complexity() {
651 let solver = TargetSizeSolver::new(10_000_000, 300, 30.0);
652 let kbps_normal = solver.solve_bitrate(1.0);
653 let kbps_complex = solver.solve_bitrate(2.0);
654 assert!(kbps_complex > kbps_normal);
656 }
657
658 #[test]
659 fn test_target_size_solver_zero_target() {
660 let solver = TargetSizeSolver::new(0, 300, 30.0);
661 assert_eq!(solver.solve_bitrate(1.0), 0);
662 }
663
664 #[test]
665 fn test_target_size_solver_zero_frames() {
666 let solver = TargetSizeSolver::new(10_000_000, 0, 30.0);
667 assert_eq!(solver.solve_bitrate(1.0), 0);
668 }
669
670 #[test]
673 fn test_budget_allocator_sums_to_budget() {
674 let scenes = vec![
675 make_scene(SceneType::Static, 0.2, 0.1, false),
676 make_scene(SceneType::Talking, 0.5, 0.4, false),
677 make_scene(SceneType::ActionFast, 0.9, 0.95, false),
678 ];
679 let allocator = BudgetAllocator::new(100_000_000, scenes);
680 let allocs = allocator.allocate();
681 let total: u64 = allocs.iter().sum();
682 assert!(total <= 100_000_000, "total={total} exceeded budget");
683 }
684
685 #[test]
686 fn test_budget_allocator_complex_scene_gets_more() {
687 let scenes = vec![
688 make_scene(SceneType::Static, 0.1, 0.1, false),
689 make_scene(SceneType::HighComplexity, 0.95, 0.95, false),
690 ];
691 let allocator = BudgetAllocator::new(100_000_000, scenes);
692 let allocs = allocator.allocate();
693 assert!(
694 allocs[1] > allocs[0],
695 "Complex scene should receive more budget"
696 );
697 }
698
699 #[test]
700 fn test_budget_allocator_empty_scenes() {
701 let allocator = BudgetAllocator::new(100_000_000, vec![]);
702 assert!(allocator.allocate().is_empty());
703 }
704
705 #[test]
706 fn test_budget_allocator_allocated_total() {
707 let scenes = vec![
708 make_scene(SceneType::Talking, 0.5, 0.4, false),
709 make_scene(SceneType::Animation, 0.6, 0.2, false),
710 ];
711 let allocator = BudgetAllocator::new(50_000_000, scenes);
712 let total = allocator.allocated_total();
713 assert!(total <= 50_000_000);
714 }
715
716 #[test]
717 fn test_budget_allocator_single_scene() {
718 let scenes = vec![make_scene(SceneType::Talking, 0.5, 0.3, false)];
719 let allocator = BudgetAllocator::new(20_000_000, scenes);
720 let allocs = allocator.allocate();
721 assert_eq!(allocs.len(), 1);
722 assert!(allocs[0] <= 20_000_000);
723 }
724
725 #[test]
726 fn test_scene_type_equality() {
727 assert_eq!(SceneType::Static, SceneType::Static);
728 assert_ne!(SceneType::Static, SceneType::ActionFast);
729 }
730
731 #[test]
732 fn test_encode_params_preset_not_empty() {
733 let enc = PerSceneEncoder::new();
734 let scene = make_scene(SceneType::Talking, 0.5, 0.3, false);
735 for codec in &["av1", "vp9", "h265", "h264"] {
736 let p = enc.compute_params(&scene, 4000, codec);
737 assert!(!p.preset.is_empty(), "preset empty for codec {codec}");
738 }
739 }
740}