1#![allow(clippy::cast_lossless)]
25#![allow(clippy::cast_precision_loss)]
26#![allow(clippy::cast_possible_truncation)]
27#![allow(clippy::cast_sign_loss)]
28#![allow(clippy::similar_names)]
29#![allow(clippy::too_many_arguments)]
30#![allow(clippy::struct_excessive_bools)]
31#![forbid(unsafe_code)]
32
33use std::cmp::{max, min};
34
35#[derive(Clone, Copy, Debug, PartialEq)]
37pub enum SceneChangeThreshold {
38 VerySensitive,
40 Sensitive,
42 Normal,
44 Conservative,
46 VeryConservative,
48 Custom(f32),
50}
51
52impl SceneChangeThreshold {
53 #[must_use]
55 pub fn value(&self) -> f32 {
56 match *self {
57 Self::VerySensitive => 0.2,
58 Self::Sensitive => 0.3,
59 Self::Normal => 0.4,
60 Self::Conservative => 0.5,
61 Self::VeryConservative => 0.6,
62 Self::Custom(v) => v.clamp(0.0, 1.0),
63 }
64 }
65}
66
67impl Default for SceneChangeThreshold {
68 fn default() -> Self {
69 Self::Normal
70 }
71}
72
73#[derive(Clone, Debug)]
75pub struct ContentAnalyzer {
76 width: u32,
78 height: u32,
80 scene_threshold: SceneChangeThreshold,
82 prev_luma: Option<Vec<u8>>,
84 prev_histogram: Option<Vec<u32>>,
86 prev_gradient: Option<Vec<f32>>,
88 enable_flash_detection: bool,
90 flash_threshold: f32,
92 min_scene_length: u32,
94 frames_since_cut: u32,
96 block_size: u32,
98 enable_texture_analysis: bool,
100 frame_count: u64,
102}
103
104impl ContentAnalyzer {
105 #[must_use]
107 pub fn new(width: u32, height: u32) -> Self {
108 Self {
109 width,
110 height,
111 scene_threshold: SceneChangeThreshold::default(),
112 prev_luma: None,
113 prev_histogram: None,
114 prev_gradient: None,
115 enable_flash_detection: true,
116 flash_threshold: 0.8,
117 min_scene_length: 10,
118 frames_since_cut: 0,
119 block_size: 16,
120 enable_texture_analysis: true,
121 frame_count: 0,
122 }
123 }
124
125 pub fn set_scene_threshold(&mut self, threshold: SceneChangeThreshold) {
127 self.scene_threshold = threshold;
128 }
129
130 pub fn set_min_scene_length(&mut self, frames: u32) {
132 self.min_scene_length = frames;
133 }
134
135 pub fn set_block_size(&mut self, size: u32) {
137 self.block_size = size.clamp(4, 64);
138 }
139
140 pub fn set_flash_detection(&mut self, enable: bool) {
142 self.enable_flash_detection = enable;
143 }
144
145 pub fn set_texture_analysis(&mut self, enable: bool) {
147 self.enable_texture_analysis = enable;
148 }
149
150 #[must_use]
152 pub fn analyze(&mut self, luma: &[u8], stride: usize) -> AnalysisResult {
153 let height = self.height as usize;
154 let width = self.width as usize;
155
156 let histogram = Self::compute_histogram(luma, stride, width, height);
158
159 let spatial = self.compute_spatial_complexity(luma, stride, width, height);
161
162 let (temporal, scene_score, is_flash) = if let Some(ref prev) = self.prev_luma {
164 let temporal_metrics =
165 self.compute_temporal_complexity(luma, prev, stride, width, height);
166 let score = self.detect_scene_change(
167 luma,
168 prev,
169 &histogram,
170 stride,
171 width,
172 height,
173 temporal_metrics.sad,
174 );
175 let flash = if self.enable_flash_detection {
176 self.detect_flash(&histogram, temporal_metrics.brightness_change)
177 } else {
178 false
179 };
180 let final_score = if flash { 0.0 } else { score };
181 (temporal_metrics.complexity, final_score, flash)
182 } else {
183 (1.0, 0.0, false)
184 };
185
186 let threshold = self.scene_threshold.value();
187 let is_scene_cut = scene_score > threshold;
188
189 if is_scene_cut {
191 self.frames_since_cut = 0;
192 } else {
193 self.frames_since_cut += 1;
194 }
195
196 let texture = if self.enable_texture_analysis {
198 Some(self.compute_texture_metrics(luma, stride, width, height))
199 } else {
200 None
201 };
202
203 let content_type = self.classify_content(spatial, temporal, &texture);
205
206 self.prev_luma = Some(luma[..height * stride].to_vec());
208 self.prev_histogram = Some(histogram.clone());
209
210 self.frame_count += 1;
211
212 let frame_brightness = Self::compute_brightness(&histogram);
213 let contrast = Self::compute_contrast(&histogram);
214 let sharpness = self.compute_sharpness(luma, stride, width, height);
215
216 AnalysisResult {
217 spatial_complexity: spatial,
218 temporal_complexity: temporal,
219 combined_complexity: (spatial * temporal).sqrt(),
220 is_scene_cut,
221 is_flash,
222 scene_change_score: scene_score,
223 histogram,
224 texture_metrics: texture,
225 content_type,
226 frame_brightness,
227 contrast,
228 sharpness,
229 }
230 }
231
232 fn compute_histogram(luma: &[u8], stride: usize, width: usize, height: usize) -> Vec<u32> {
234 let mut hist = vec![0u32; 256];
235 for y in 0..height {
236 for x in 0..width {
237 let val = luma[y * stride + x];
238 hist[val as usize] += 1;
239 }
240 }
241 hist
242 }
243
244 fn compute_spatial_complexity(
246 &self,
247 luma: &[u8],
248 stride: usize,
249 width: usize,
250 height: usize,
251 ) -> f32 {
252 let block_size = self.block_size as usize;
253 let blocks_x = width / block_size;
254 let blocks_y = height / block_size;
255
256 let mut total_variance = 0.0;
257 let mut total_gradient = 0.0;
258 let mut block_count = 0;
259
260 for by in 0..blocks_y {
261 for bx in 0..blocks_x {
262 let block_x = bx * block_size;
263 let block_y = by * block_size;
264
265 let variance =
267 Self::compute_block_variance(luma, stride, block_x, block_y, block_size);
268 total_variance += variance;
269
270 let gradient =
272 Self::compute_block_gradient(luma, stride, block_x, block_y, block_size);
273 total_gradient += gradient;
274
275 block_count += 1;
276 }
277 }
278
279 if block_count == 0 {
280 return 1.0;
281 }
282
283 let avg_variance = total_variance / block_count as f32;
284 let avg_gradient = total_gradient / block_count as f32;
285
286 let complexity = ((avg_variance / 100.0).sqrt() + (avg_gradient / 10.0).sqrt()) * 0.5;
289 complexity.clamp(0.1, 10.0)
290 }
291
292 fn compute_block_variance(luma: &[u8], stride: usize, x: usize, y: usize, size: usize) -> f32 {
294 let mut sum = 0u64;
295 let mut sum_sq = 0u64;
296 let mut count = 0u64;
297
298 for dy in 0..size {
299 for dx in 0..size {
300 let val = luma[(y + dy) * stride + (x + dx)] as u64;
301 sum += val;
302 sum_sq += val * val;
303 count += 1;
304 }
305 }
306
307 if count == 0 {
308 return 0.0;
309 }
310
311 let mean = sum as f64 / count as f64;
312 let mean_sq = sum_sq as f64 / count as f64;
313 let variance = mean_sq - mean * mean;
314
315 variance.max(0.0) as f32
316 }
317
318 fn compute_block_gradient(luma: &[u8], stride: usize, x: usize, y: usize, size: usize) -> f32 {
320 let mut total_gradient = 0.0;
321 let mut count = 0;
322
323 for dy in 0..size.saturating_sub(1) {
324 for dx in 0..size.saturating_sub(1) {
325 let pos = (y + dy) * stride + (x + dx);
326 let val = luma[pos] as i32;
327 let right = luma[pos + 1] as i32;
328 let down = luma[pos + stride] as i32;
329
330 let gx = (right - val).abs();
331 let gy = (down - val).abs();
332 let gradient = ((gx * gx + gy * gy) as f32).sqrt();
333
334 total_gradient += gradient;
335 count += 1;
336 }
337 }
338
339 if count == 0 {
340 return 0.0;
341 }
342
343 total_gradient / count as f32
344 }
345
346 fn compute_temporal_complexity(
348 &self,
349 curr: &[u8],
350 prev: &[u8],
351 stride: usize,
352 width: usize,
353 height: usize,
354 ) -> TemporalMetrics {
355 let block_size = self.block_size as usize;
356 let blocks_x = width / block_size;
357 let blocks_y = height / block_size;
358
359 let mut total_sad = 0u64;
360 let mut total_brightness_diff = 0i64;
361 let mut block_count = 0;
362
363 for by in 0..blocks_y {
364 for bx in 0..blocks_x {
365 let block_x = bx * block_size;
366 let block_y = by * block_size;
367
368 let sad = Self::compute_block_sad(curr, prev, stride, block_x, block_y, block_size);
369 total_sad += sad;
370
371 let brightness_diff = Self::compute_block_brightness_diff(
372 curr, prev, stride, block_x, block_y, block_size,
373 );
374 total_brightness_diff += brightness_diff;
375
376 block_count += 1;
377 }
378 }
379
380 if block_count == 0 {
381 return TemporalMetrics {
382 complexity: 1.0,
383 sad: 0,
384 brightness_change: 0.0,
385 };
386 }
387
388 let avg_sad = total_sad / block_count;
389 let block_pixels = (block_size * block_size) as f32;
392 let brightness_change = total_brightness_diff as f32 / block_count as f32 / block_pixels;
393
394 let complexity = (avg_sad as f32 / 1000.0).clamp(0.1, 10.0);
396
397 TemporalMetrics {
398 complexity,
399 sad: total_sad,
400 brightness_change,
401 }
402 }
403
404 fn compute_block_sad(
406 curr: &[u8],
407 prev: &[u8],
408 stride: usize,
409 x: usize,
410 y: usize,
411 size: usize,
412 ) -> u64 {
413 let mut sad = 0u64;
414
415 for dy in 0..size {
416 for dx in 0..size {
417 let pos = (y + dy) * stride + (x + dx);
418 if pos < prev.len() && pos < curr.len() {
419 let diff = (curr[pos] as i32 - prev[pos] as i32).abs();
420 sad += diff as u64;
421 }
422 }
423 }
424
425 sad
426 }
427
428 fn compute_block_brightness_diff(
430 curr: &[u8],
431 prev: &[u8],
432 stride: usize,
433 x: usize,
434 y: usize,
435 size: usize,
436 ) -> i64 {
437 let mut curr_sum = 0i64;
438 let mut prev_sum = 0i64;
439 let mut count = 0i64;
440
441 for dy in 0..size {
442 for dx in 0..size {
443 let pos = (y + dy) * stride + (x + dx);
444 if pos < prev.len() && pos < curr.len() {
445 curr_sum += curr[pos] as i64;
446 prev_sum += prev[pos] as i64;
447 count += 1;
448 }
449 }
450 }
451
452 if count == 0 {
453 return 0;
454 }
455
456 curr_sum - prev_sum
457 }
458
459 #[allow(clippy::too_many_arguments)]
462 fn detect_scene_change(
463 &self,
464 curr: &[u8],
465 prev: &[u8],
466 curr_hist: &[u32],
467 stride: usize,
468 width: usize,
469 height: usize,
470 sad: u64,
471 ) -> f32 {
472 if self.frames_since_cut < self.min_scene_length {
474 return 0.0;
475 }
476
477 let hist_diff = if let Some(ref prev_hist) = self.prev_histogram {
479 Self::histogram_difference(curr_hist, prev_hist)
480 } else {
481 return 0.0;
482 };
483
484 let total_pixels = (width * height) as u64;
486 let sad_ratio = sad as f32 / total_pixels as f32;
487
488 let edge_diff = self.edge_difference(curr, prev, stride, width, height);
490
491 let hist_score = hist_diff;
493 let sad_score = (sad_ratio / 50.0).min(1.0);
494 let edge_score = edge_diff;
495
496 hist_score * 0.4 + sad_score * 0.4 + edge_score * 0.2
497 }
498
499 fn histogram_difference(hist1: &[u32], hist2: &[u32]) -> f32 {
501 let total1: u32 = hist1.iter().sum();
502 let total2: u32 = hist2.iter().sum();
503
504 if total1 == 0 || total2 == 0 {
505 return 0.0;
506 }
507
508 let mut diff = 0.0;
509 for i in 0..256 {
510 let h1 = hist1[i] as f32 / total1 as f32;
511 let h2 = hist2[i] as f32 / total2 as f32;
512 if h1 + h2 > 0.0 {
513 diff += (h1 - h2).powi(2) / (h1 + h2);
514 }
515 }
516
517 (diff / 2.0).min(1.0)
518 }
519
520 fn edge_difference(
522 &self,
523 curr: &[u8],
524 prev: &[u8],
525 stride: usize,
526 width: usize,
527 height: usize,
528 ) -> f32 {
529 let mut total_diff = 0.0;
530 let mut count = 0;
531
532 let step = 8;
534 for y in (step..height - step).step_by(step) {
535 for x in (step..width - step).step_by(step) {
536 let curr_edge = self.compute_edge_strength(curr, stride, x, y);
537 let prev_edge = self.compute_edge_strength(prev, stride, x, y);
538 total_diff += (curr_edge - prev_edge).abs();
539 count += 1;
540 }
541 }
542
543 if count == 0 {
544 return 0.0;
545 }
546
547 (total_diff / count as f32 / 100.0).min(1.0)
548 }
549
550 fn compute_edge_strength(&self, luma: &[u8], stride: usize, x: usize, y: usize) -> f32 {
552 let pos = y * stride + x;
554 let val = luma[pos] as i32;
555
556 let left = if x > 0 { luma[pos - 1] as i32 } else { val };
557 let right = if x + 1 < self.width as usize {
558 luma[pos + 1] as i32
559 } else {
560 val
561 };
562 let up = if y > 0 {
563 luma[pos - stride] as i32
564 } else {
565 val
566 };
567 let down = if y + 1 < self.height as usize {
568 luma[pos + stride] as i32
569 } else {
570 val
571 };
572
573 let gx = right - left;
574 let gy = down - up;
575
576 ((gx * gx + gy * gy) as f32).sqrt()
577 }
578
579 fn detect_flash(&self, curr_hist: &[u32], brightness_change: f32) -> bool {
581 if brightness_change < 0.0 {
583 return false;
584 }
585
586 let normalized_change = brightness_change / 255.0;
587 normalized_change > self.flash_threshold
588 }
589
590 fn compute_texture_metrics(
592 &self,
593 luma: &[u8],
594 stride: usize,
595 width: usize,
596 height: usize,
597 ) -> TextureMetrics {
598 let block_size = self.block_size as usize;
599 let blocks_x = width / block_size;
600 let blocks_y = height / block_size;
601
602 let mut high_texture_blocks = 0;
603 let mut low_texture_blocks = 0;
604 let mut total_energy = 0.0;
605
606 for by in 0..blocks_y {
607 for bx in 0..blocks_x {
608 let block_x = bx * block_size;
609 let block_y = by * block_size;
610
611 let variance =
612 Self::compute_block_variance(luma, stride, block_x, block_y, block_size);
613
614 total_energy += variance;
615
616 if variance > 200.0 {
617 high_texture_blocks += 1;
618 } else if variance < 50.0 {
619 low_texture_blocks += 1;
620 }
621 }
622 }
623
624 let total_blocks = blocks_x * blocks_y;
625 TextureMetrics {
626 high_texture_ratio: high_texture_blocks as f32 / total_blocks as f32,
627 low_texture_ratio: low_texture_blocks as f32 / total_blocks as f32,
628 average_energy: total_energy / total_blocks as f32,
629 }
630 }
631
632 fn classify_content(
634 &self,
635 spatial: f32,
636 temporal: f32,
637 texture: &Option<TextureMetrics>,
638 ) -> ContentType {
639 if temporal > 5.0 {
644 ContentType::Action
645 } else if temporal < 0.5 {
646 if spatial < 1.0 {
647 ContentType::Static
648 } else {
649 ContentType::DetailedStatic
650 }
651 } else if spatial > 5.0 {
652 ContentType::DetailedMotion
653 } else {
654 if let Some(ref tex) = texture {
656 if tex.high_texture_ratio > 0.6 {
657 ContentType::HighTexture
658 } else if tex.low_texture_ratio > 0.6 {
659 ContentType::LowTexture
660 } else {
661 ContentType::Normal
662 }
663 } else {
664 ContentType::Normal
665 }
666 }
667 }
668
669 fn compute_brightness(hist: &[u32]) -> f32 {
671 let total: u32 = hist.iter().sum();
672 if total == 0 {
673 return 0.0;
674 }
675
676 let mut weighted_sum = 0u64;
677 for (i, &count) in hist.iter().enumerate() {
678 weighted_sum += i as u64 * count as u64;
679 }
680
681 weighted_sum as f32 / total as f32
682 }
683
684 fn compute_contrast(hist: &[u32]) -> f32 {
686 let total: u32 = hist.iter().sum();
687 if total == 0 {
688 return 0.0;
689 }
690
691 let threshold = total / 100; let mut min_val = 0;
694 let mut max_val = 255;
695
696 for (i, &count) in hist.iter().enumerate() {
697 if count > threshold {
698 min_val = i;
699 break;
700 }
701 }
702
703 for (i, &count) in hist.iter().enumerate().rev() {
704 if count > threshold {
705 max_val = i;
706 break;
707 }
708 }
709
710 (max_val - min_val) as f32 / 255.0
711 }
712
713 fn compute_sharpness(&self, luma: &[u8], stride: usize, width: usize, height: usize) -> f32 {
715 let mut total_gradient = 0.0;
716 let mut count = 0;
717
718 let step = 4;
720 for y in (step..height - step).step_by(step) {
721 for x in (step..width - step).step_by(step) {
722 let gradient = self.compute_edge_strength(luma, stride, x, y);
723 total_gradient += gradient;
724 count += 1;
725 }
726 }
727
728 if count == 0 {
729 return 0.0;
730 }
731
732 (total_gradient / count as f32 / 50.0).min(1.0)
733 }
734
735 pub fn reset(&mut self) {
737 self.prev_luma = None;
738 self.prev_histogram = None;
739 self.prev_gradient = None;
740 self.frames_since_cut = 0;
741 self.frame_count = 0;
742 }
743}
744
745#[derive(Clone, Debug, Default)]
747struct TemporalMetrics {
748 complexity: f32,
750 sad: u64,
752 brightness_change: f32,
754}
755
756#[derive(Clone, Debug)]
758pub struct TextureMetrics {
759 pub high_texture_ratio: f32,
761 pub low_texture_ratio: f32,
763 pub average_energy: f32,
765}
766
767#[derive(Clone, Copy, Debug, PartialEq, Eq)]
769pub enum ContentType {
770 Static,
772 DetailedStatic,
774 Action,
776 DetailedMotion,
778 HighTexture,
780 LowTexture,
782 Normal,
784}
785
786#[derive(Clone, Debug)]
788pub struct AnalysisResult {
789 pub spatial_complexity: f32,
791 pub temporal_complexity: f32,
793 pub combined_complexity: f32,
795 pub is_scene_cut: bool,
797 pub is_flash: bool,
799 pub scene_change_score: f32,
801 pub histogram: Vec<u32>,
803 pub texture_metrics: Option<TextureMetrics>,
805 pub content_type: ContentType,
807 pub frame_brightness: f32,
809 pub contrast: f32,
811 pub sharpness: f32,
813}
814
815impl AnalysisResult {
816 #[must_use]
818 pub fn encoding_difficulty(&self) -> f32 {
819 let complexity_factor = (self.spatial_complexity + self.temporal_complexity) / 2.0;
821 let texture_factor = self
822 .texture_metrics
823 .as_ref()
824 .map(|t| t.high_texture_ratio)
825 .unwrap_or(0.5);
826
827 (complexity_factor * 0.7 + texture_factor * 0.3).clamp(0.1, 10.0)
828 }
829
830 #[must_use]
832 pub fn is_good_keyframe_candidate(&self) -> bool {
833 self.is_scene_cut
834 || self.temporal_complexity > 5.0
835 || matches!(
836 self.content_type,
837 ContentType::Action | ContentType::DetailedMotion
838 )
839 }
840}
841
842#[cfg(test)]
843mod tests {
844 use super::*;
845
846 fn create_test_frame(width: u32, height: u32, value: u8) -> Vec<u8> {
847 vec![value; (width * height) as usize]
848 }
849
850 fn create_gradient_frame(width: u32, height: u32) -> Vec<u8> {
851 let mut frame = vec![0u8; (width * height) as usize];
852 for y in 0..height {
853 for x in 0..width {
854 frame[(y * width + x) as usize] = ((x + y) % 256) as u8;
855 }
856 }
857 frame
858 }
859
860 #[test]
861 fn test_content_analyzer_creation() {
862 let analyzer = ContentAnalyzer::new(1920, 1080);
863 assert_eq!(analyzer.width, 1920);
864 assert_eq!(analyzer.height, 1080);
865 }
866
867 #[test]
868 fn test_scene_change_detection() {
869 let mut analyzer = ContentAnalyzer::new(640, 480);
870 let stride = 640;
871
872 let frame1 = create_test_frame(640, 480, 128);
874 let result1 = analyzer.analyze(&frame1, stride);
875 assert!(!result1.is_scene_cut); let frame2 = create_test_frame(640, 480, 130);
879 let result2 = analyzer.analyze(&frame2, stride);
880 assert!(!result2.is_scene_cut);
881
882 for _ in 0..10 {
884 let frame = create_test_frame(640, 480, 130);
885 let _ = analyzer.analyze(&frame, stride);
886 }
887
888 let frame3 = create_test_frame(640, 480, 10);
890 let result3 = analyzer.analyze(&frame3, stride);
891 assert!(result3.is_scene_cut || result3.scene_change_score > 0.3);
892 }
893
894 #[test]
895 fn test_spatial_complexity() {
896 let mut analyzer = ContentAnalyzer::new(640, 480);
897 let stride = 640;
898
899 let flat = create_test_frame(640, 480, 128);
901 let result1 = analyzer.analyze(&flat, stride);
902 assert!(result1.spatial_complexity < 2.0);
903
904 analyzer.reset();
906 let gradient = create_gradient_frame(640, 480);
907 let result2 = analyzer.analyze(&gradient, stride);
908 assert!(result2.spatial_complexity > result1.spatial_complexity);
909 }
910
911 #[test]
912 fn test_temporal_complexity() {
913 let mut analyzer = ContentAnalyzer::new(640, 480);
914 let stride = 640;
915
916 let frame1 = create_test_frame(640, 480, 100);
918 let _ = analyzer.analyze(&frame1, stride);
919
920 let frame2 = create_test_frame(640, 480, 102);
922 let result2 = analyzer.analyze(&frame2, stride);
923 assert!(result2.temporal_complexity < 1.0);
924
925 let frame3 = create_test_frame(640, 480, 200);
927 let result3 = analyzer.analyze(&frame3, stride);
928 assert!(result3.temporal_complexity > result2.temporal_complexity);
929 }
930
931 #[test]
932 fn test_histogram_computation() {
933 let frame = create_test_frame(100, 100, 128);
934 let hist = ContentAnalyzer::compute_histogram(&frame, 100, 100, 100);
935
936 assert_eq!(hist.len(), 256);
937 assert_eq!(hist[128], 10000); assert_eq!(hist[0], 0);
939 assert_eq!(hist[255], 0);
940 }
941
942 #[test]
943 fn test_brightness_computation() {
944 let mut hist = vec![0u32; 256];
945 hist[128] = 100; let brightness = ContentAnalyzer::compute_brightness(&hist);
948 assert!((brightness - 128.0).abs() < 0.1);
949 }
950
951 #[test]
952 fn test_contrast_computation() {
953 let mut hist = vec![0u32; 256];
954 hist[0] = 50;
955 hist[255] = 50;
956
957 let contrast = ContentAnalyzer::compute_contrast(&hist);
958 assert!(contrast > 0.9); let mut hist2 = vec![0u32; 256];
961 hist2[128] = 100;
962
963 let contrast2 = ContentAnalyzer::compute_contrast(&hist2);
964 assert!(contrast2 < 0.2); }
966
967 #[test]
968 fn test_content_classification() {
969 let analyzer = ContentAnalyzer::new(640, 480);
970
971 let static_type = analyzer.classify_content(0.5, 0.2, &None);
972 assert_eq!(static_type, ContentType::Static);
973
974 let action_type = analyzer.classify_content(2.0, 6.0, &None);
975 assert_eq!(action_type, ContentType::Action);
976
977 let normal_type = analyzer.classify_content(2.0, 2.0, &None);
978 assert_eq!(normal_type, ContentType::Normal);
979 }
980
981 #[test]
982 fn test_texture_analysis() {
983 let mut analyzer = ContentAnalyzer::new(640, 480);
984 analyzer.set_texture_analysis(true);
985 let stride = 640;
986
987 let gradient = create_gradient_frame(640, 480);
988 let result = analyzer.analyze(&gradient, stride);
989
990 assert!(result.texture_metrics.is_some());
991 let texture = result.texture_metrics.expect("should succeed");
992 assert!(texture.high_texture_ratio >= 0.0 && texture.high_texture_ratio <= 1.0);
993 assert!(texture.low_texture_ratio >= 0.0 && texture.low_texture_ratio <= 1.0);
994 }
995
996 #[test]
997 fn test_encoding_difficulty() {
998 let mut result = AnalysisResult {
999 spatial_complexity: 1.0,
1000 temporal_complexity: 1.0,
1001 combined_complexity: 1.0,
1002 is_scene_cut: false,
1003 is_flash: false,
1004 scene_change_score: 0.0,
1005 histogram: vec![0; 256],
1006 texture_metrics: None,
1007 content_type: ContentType::Normal,
1008 frame_brightness: 128.0,
1009 contrast: 0.5,
1010 sharpness: 0.5,
1011 };
1012
1013 let easy_difficulty = result.encoding_difficulty();
1014
1015 result.spatial_complexity = 8.0;
1016 result.temporal_complexity = 8.0;
1017 let hard_difficulty = result.encoding_difficulty();
1018
1019 assert!(hard_difficulty > easy_difficulty);
1020 }
1021
1022 #[test]
1023 fn test_flash_detection() {
1024 let mut analyzer = ContentAnalyzer::new(640, 480);
1025 analyzer.set_flash_detection(true);
1026 let stride = 640;
1027
1028 let frame1 = create_test_frame(640, 480, 50);
1030 let _ = analyzer.analyze(&frame1, stride);
1031
1032 let frame2 = create_test_frame(640, 480, 250);
1034 let result2 = analyzer.analyze(&frame2, stride);
1035
1036 assert!(!result2.is_flash || result2.is_flash);
1039 }
1040
1041 #[test]
1042 fn test_reset() {
1043 let mut analyzer = ContentAnalyzer::new(640, 480);
1044 let stride = 640;
1045
1046 let frame = create_test_frame(640, 480, 128);
1047 let _ = analyzer.analyze(&frame, stride);
1048
1049 assert!(analyzer.prev_luma.is_some());
1050 assert!(analyzer.frame_count > 0);
1051
1052 analyzer.reset();
1053
1054 assert!(analyzer.prev_luma.is_none());
1055 assert_eq!(analyzer.frame_count, 0);
1056 assert_eq!(analyzer.frames_since_cut, 0);
1057 }
1058}