1use crate::traits::FrameHasher;
4use crate::types::{AnalyzedFrame, DecodedFrame, Segment};
5
6#[derive(Debug, Clone)]
8pub struct AnalysisConfig {
9 pub similarity_threshold: u32,
11 pub min_segment_frames: usize,
13 pub detect_static: bool,
15 pub identical_threshold: u32,
17}
18
19impl Default for AnalysisConfig {
20 fn default() -> Self {
21 Self {
22 similarity_threshold: 5,
23 min_segment_frames: 2,
24 detect_static: true,
25 identical_threshold: 0,
26 }
27 }
28}
29
30impl AnalysisConfig {
31 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn with_similarity_threshold(mut self, threshold: u32) -> Self {
38 self.similarity_threshold = threshold;
39 self
40 }
41
42 pub fn with_min_segment_frames(mut self, min: usize) -> Self {
44 self.min_segment_frames = min.max(1);
45 self
46 }
47
48 pub fn with_static_detection(mut self, enabled: bool) -> Self {
50 self.detect_static = enabled;
51 self
52 }
53
54 pub fn with_identical_threshold(mut self, threshold: u32) -> Self {
56 self.identical_threshold = threshold;
57 self
58 }
59}
60
61pub fn analyze_frames<H: FrameHasher>(
78 frames: Vec<DecodedFrame>,
79 hasher: &H,
80 config: &AnalysisConfig,
81 progress: Option<&(dyn Fn(usize, usize) + Send + Sync)>,
82) -> (Vec<AnalyzedFrame<H::Hash>>, Vec<Segment>) {
83 if frames.is_empty() {
84 return (Vec::new(), Vec::new());
85 }
86
87 let total_frames = frames.len();
88
89 let mut analyzed: Vec<AnalyzedFrame<H::Hash>> = Vec::with_capacity(total_frames);
91 for (i, frame) in frames.into_iter().enumerate() {
92 let hash = hasher.hash_frame(&frame.image);
93 analyzed.push(AnalyzedFrame::new(frame, hash));
94
95 if let Some(callback) = progress {
96 callback(i + 1, total_frames);
97 }
98 }
99
100 let mut distances: Vec<u32> = Vec::with_capacity(analyzed.len().saturating_sub(1));
102 for i in 0..analyzed.len().saturating_sub(1) {
103 let dist = hasher.distance(&analyzed[i].hash, &analyzed[i + 1].hash);
104 distances.push(dist);
105 analyzed[i + 1].distance_to_prev = Some(dist);
107 }
108
109 let segments = detect_segments(&mut analyzed, &distances, config);
111
112 (analyzed, segments)
113}
114
115#[cfg(feature = "parallel")]
117pub fn analyze_frames_parallel<H: FrameHasher>(
118 frames: Vec<DecodedFrame>,
119 hasher: &H,
120 config: &AnalysisConfig,
121 progress: Option<&(dyn Fn(usize, usize) + Send + Sync)>,
122) -> (Vec<AnalyzedFrame<H::Hash>>, Vec<Segment>)
123where
124 H::Hash: Send,
125{
126 use rayon::prelude::*;
127 use std::sync::atomic::{AtomicUsize, Ordering};
128
129 if frames.is_empty() {
130 return (Vec::new(), Vec::new());
131 }
132
133 let total_frames = frames.len();
134 let current_frame = AtomicUsize::new(0);
135
136 let mut analyzed: Vec<AnalyzedFrame<H::Hash>> = frames
138 .into_par_iter()
139 .map(|frame| {
140 let hash = hasher.hash_frame(&frame.image);
141
142 if let Some(callback) = progress {
143 let current = current_frame.fetch_add(1, Ordering::Relaxed) + 1;
144 callback(current, total_frames);
145 }
146
147 AnalyzedFrame::new(frame, hash)
148 })
149 .collect();
150
151 let mut distances: Vec<u32> = Vec::with_capacity(analyzed.len().saturating_sub(1));
154 for i in 0..analyzed.len().saturating_sub(1) {
155 let dist = hasher.distance(&analyzed[i].hash, &analyzed[i + 1].hash);
156 distances.push(dist);
157 analyzed[i + 1].distance_to_prev = Some(dist);
159 }
160
161 let segments = detect_segments(&mut analyzed, &distances, config);
163
164 (analyzed, segments)
165}
166
167fn detect_segments<H>(
169 analyzed: &mut [AnalyzedFrame<H>],
170 distances: &[u32],
171 config: &AnalysisConfig,
172) -> Vec<Segment> {
173 let mut segments = Vec::new();
174 let mut segment_id = 0;
175
176 let mut i = 0;
177 while i < analyzed.len() {
178 let segment_start = i;
180 let mut segment_end = i + 1;
181 let mut total_distance: u64 = 0;
182 let mut distance_count: usize = 0;
183 let mut all_identical = true;
184
185 while segment_end < analyzed.len() {
186 let dist_idx = segment_end - 1;
187 if dist_idx < distances.len() {
188 let dist = distances[dist_idx];
189 if dist <= config.similarity_threshold {
190 total_distance += dist as u64;
191 distance_count += 1;
192
193 if dist > config.identical_threshold {
195 all_identical = false;
196 } else if config.identical_threshold == 0 {
197 if analyzed[segment_end - 1].frame.image
200 != analyzed[segment_end].frame.image
201 {
202 all_identical = false;
203 }
204 }
205
206 segment_end += 1;
207 } else {
208 break;
209 }
210 } else {
211 break;
212 }
213 }
214
215 let segment_frames = segment_end - segment_start;
216
217 if segment_frames >= config.min_segment_frames {
219 let total_duration_cs: u16 = analyzed[segment_start..segment_end]
221 .iter()
222 .map(|f| f.delay_cs())
223 .sum();
224
225 let avg_distance = if distance_count > 0 {
227 total_distance as f64 / distance_count as f64
228 } else {
229 0.0
230 };
231
232 for frame in &mut analyzed[segment_start..segment_end] {
234 frame.segment_id = Some(segment_id);
235 }
236
237 segments.push(Segment {
238 id: segment_id,
239 frame_range: segment_start..segment_end,
240 total_duration_cs,
241 avg_distance,
242 is_static: config.detect_static && all_identical,
243 });
244
245 segment_id += 1;
246 } else {
247 let total_duration_cs = analyzed[segment_start].delay_cs();
249
250 analyzed[segment_start].segment_id = Some(segment_id);
251
252 segments.push(Segment {
253 id: segment_id,
254 frame_range: segment_start..segment_start + 1,
255 total_duration_cs,
256 avg_distance: 0.0,
257 is_static: false,
258 });
259
260 segment_id += 1;
261 segment_end = segment_start + 1;
262 }
263
264 i = segment_end;
265 }
266
267 segments
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_analysis_config_defaults() {
276 let config = AnalysisConfig::default();
277 assert_eq!(config.similarity_threshold, 5);
278 assert_eq!(config.min_segment_frames, 2);
279 assert!(config.detect_static);
280 }
281
282 #[test]
283 fn test_analysis_config_builder() {
284 let config = AnalysisConfig::new()
285 .with_similarity_threshold(10)
286 .with_min_segment_frames(3)
287 .with_static_detection(false);
288
289 assert_eq!(config.similarity_threshold, 10);
290 assert_eq!(config.min_segment_frames, 3);
291 assert!(!config.detect_static);
292 }
293}