use crate::traits::FrameHasher;
use crate::types::{AnalyzedFrame, DecodedFrame, Segment};
#[derive(Debug, Clone)]
pub struct AnalysisConfig {
pub similarity_threshold: u32,
pub min_segment_frames: usize,
pub detect_static: bool,
pub identical_threshold: u32,
}
impl Default for AnalysisConfig {
fn default() -> Self {
Self {
similarity_threshold: 5,
min_segment_frames: 2,
detect_static: true,
identical_threshold: 0,
}
}
}
impl AnalysisConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_similarity_threshold(mut self, threshold: u32) -> Self {
self.similarity_threshold = threshold;
self
}
pub fn with_min_segment_frames(mut self, min: usize) -> Self {
self.min_segment_frames = min.max(1);
self
}
pub fn with_static_detection(mut self, enabled: bool) -> Self {
self.detect_static = enabled;
self
}
pub fn with_identical_threshold(mut self, threshold: u32) -> Self {
self.identical_threshold = threshold;
self
}
}
pub fn analyze_frames<H: FrameHasher>(
frames: Vec<DecodedFrame>,
hasher: &H,
config: &AnalysisConfig,
progress: Option<&(dyn Fn(usize, usize) + Send + Sync)>,
) -> (Vec<AnalyzedFrame<H::Hash>>, Vec<Segment>) {
if frames.is_empty() {
return (Vec::new(), Vec::new());
}
let total_frames = frames.len();
let mut analyzed: Vec<AnalyzedFrame<H::Hash>> = Vec::with_capacity(total_frames);
for (i, frame) in frames.into_iter().enumerate() {
let hash = hasher.hash_frame(&frame.image);
analyzed.push(AnalyzedFrame::new(frame, hash));
if let Some(callback) = progress {
callback(i + 1, total_frames);
}
}
let mut distances: Vec<u32> = Vec::with_capacity(analyzed.len().saturating_sub(1));
for i in 0..analyzed.len().saturating_sub(1) {
let dist = hasher.distance(&analyzed[i].hash, &analyzed[i + 1].hash);
distances.push(dist);
analyzed[i + 1].distance_to_prev = Some(dist);
}
let segments = detect_segments(&mut analyzed, &distances, config);
(analyzed, segments)
}
#[cfg(feature = "parallel")]
pub fn analyze_frames_parallel<H: FrameHasher>(
frames: Vec<DecodedFrame>,
hasher: &H,
config: &AnalysisConfig,
progress: Option<&(dyn Fn(usize, usize) + Send + Sync)>,
) -> (Vec<AnalyzedFrame<H::Hash>>, Vec<Segment>)
where
H::Hash: Send,
{
use rayon::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
if frames.is_empty() {
return (Vec::new(), Vec::new());
}
let total_frames = frames.len();
let current_frame = AtomicUsize::new(0);
let mut analyzed: Vec<AnalyzedFrame<H::Hash>> = frames
.into_par_iter()
.map(|frame| {
let hash = hasher.hash_frame(&frame.image);
if let Some(callback) = progress {
let current = current_frame.fetch_add(1, Ordering::Relaxed) + 1;
callback(current, total_frames);
}
AnalyzedFrame::new(frame, hash)
})
.collect();
let mut distances: Vec<u32> = Vec::with_capacity(analyzed.len().saturating_sub(1));
for i in 0..analyzed.len().saturating_sub(1) {
let dist = hasher.distance(&analyzed[i].hash, &analyzed[i + 1].hash);
distances.push(dist);
analyzed[i + 1].distance_to_prev = Some(dist);
}
let segments = detect_segments(&mut analyzed, &distances, config);
(analyzed, segments)
}
fn detect_segments<H>(
analyzed: &mut [AnalyzedFrame<H>],
distances: &[u32],
config: &AnalysisConfig,
) -> Vec<Segment> {
let mut segments = Vec::new();
let mut segment_id = 0;
let mut i = 0;
while i < analyzed.len() {
let segment_start = i;
let mut segment_end = i + 1;
let mut total_distance: u64 = 0;
let mut distance_count: usize = 0;
let mut all_identical = true;
while segment_end < analyzed.len() {
let dist_idx = segment_end - 1;
if dist_idx < distances.len() {
let dist = distances[dist_idx];
if dist <= config.similarity_threshold {
total_distance += dist as u64;
distance_count += 1;
if dist > config.identical_threshold {
all_identical = false;
} else if config.identical_threshold == 0 {
if analyzed[segment_end - 1].frame.image
!= analyzed[segment_end].frame.image
{
all_identical = false;
}
}
segment_end += 1;
} else {
break;
}
} else {
break;
}
}
let segment_frames = segment_end - segment_start;
if segment_frames >= config.min_segment_frames {
let total_duration_cs: u16 = analyzed[segment_start..segment_end]
.iter()
.map(|f| f.delay_cs())
.sum();
let avg_distance = if distance_count > 0 {
total_distance as f64 / distance_count as f64
} else {
0.0
};
for frame in &mut analyzed[segment_start..segment_end] {
frame.segment_id = Some(segment_id);
}
segments.push(Segment {
id: segment_id,
frame_range: segment_start..segment_end,
total_duration_cs,
avg_distance,
is_static: config.detect_static && all_identical,
});
segment_id += 1;
} else {
let total_duration_cs = analyzed[segment_start].delay_cs();
analyzed[segment_start].segment_id = Some(segment_id);
segments.push(Segment {
id: segment_id,
frame_range: segment_start..segment_start + 1,
total_duration_cs,
avg_distance: 0.0,
is_static: false,
});
segment_id += 1;
segment_end = segment_start + 1;
}
i = segment_end;
}
segments
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analysis_config_defaults() {
let config = AnalysisConfig::default();
assert_eq!(config.similarity_threshold, 5);
assert_eq!(config.min_segment_frames, 2);
assert!(config.detect_static);
}
#[test]
fn test_analysis_config_builder() {
let config = AnalysisConfig::new()
.with_similarity_threshold(10)
.with_min_segment_frames(3)
.with_static_detection(false);
assert_eq!(config.similarity_threshold, 10);
assert_eq!(config.min_segment_frames, 3);
assert!(!config.detect_static);
}
}