1#![allow(dead_code)]
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct AlignmentOffset {
11 pub stream_index: usize,
13 pub offset_ms: f64,
15 pub confidence: f64,
17}
18
19impl AlignmentOffset {
20 #[must_use]
22 pub fn new(stream_index: usize, offset_ms: f64, confidence: f64) -> Self {
23 Self {
24 stream_index,
25 offset_ms,
26 confidence,
27 }
28 }
29
30 #[must_use]
32 pub fn apply_to_pts(&self, raw_pts_ms: f64) -> f64 {
33 raw_pts_ms + self.offset_ms
34 }
35
36 #[must_use]
38 pub fn is_within_tolerance(&self, tolerance_ms: f64) -> bool {
39 self.offset_ms.abs() <= tolerance_ms
40 }
41}
42
43#[derive(Debug, Clone)]
45pub struct TemporalAlignment {
46 pub stream_index: usize,
48 pub applied_offset_ms: f64,
50 pub drift_rate_ms_per_s: f64,
52 pub aligned: bool,
54}
55
56impl TemporalAlignment {
57 #[must_use]
59 pub fn new(
60 stream_index: usize,
61 applied_offset_ms: f64,
62 drift_rate_ms_per_s: f64,
63 aligned: bool,
64 ) -> Self {
65 Self {
66 stream_index,
67 applied_offset_ms,
68 drift_rate_ms_per_s,
69 aligned,
70 }
71 }
72
73 #[must_use]
77 pub fn is_synchronized(&self) -> bool {
78 self.aligned && self.drift_rate_ms_per_s.abs() < 0.1
79 }
80
81 #[must_use]
83 pub fn predicted_drift_ms(&self, duration_s: f64) -> f64 {
84 self.drift_rate_ms_per_s * duration_s
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct StreamAlignerConfig {
91 pub tolerance_ms: f64,
93 pub max_drift_ms_per_s: f64,
95 pub min_confidence: f64,
97}
98
99impl Default for StreamAlignerConfig {
100 fn default() -> Self {
101 Self {
102 tolerance_ms: 10.0,
103 max_drift_ms_per_s: 0.5,
104 min_confidence: 0.60,
105 }
106 }
107}
108
109#[derive(Debug)]
111pub struct StreamAligner {
112 config: StreamAlignerConfig,
113 alignments: Vec<TemporalAlignment>,
114}
115
116impl StreamAligner {
117 #[must_use]
119 pub fn new(config: StreamAlignerConfig) -> Self {
120 Self {
121 config,
122 alignments: Vec::new(),
123 }
124 }
125
126 #[must_use]
128 pub fn default_aligner() -> Self {
129 Self::new(StreamAlignerConfig::default())
130 }
131
132 pub fn align_streams(&mut self, offsets: &[AlignmentOffset]) -> &[TemporalAlignment] {
136 self.alignments.clear();
137 for off in offsets {
138 let aligned = off.confidence >= self.config.min_confidence;
139 let applied = if aligned { off.offset_ms } else { 0.0 };
140 let drift = 0.0_f64;
142 self.alignments.push(TemporalAlignment::new(
143 off.stream_index,
144 applied,
145 drift,
146 aligned,
147 ));
148 }
149 &self.alignments
150 }
151
152 #[must_use]
154 pub fn max_offset_ms(&self) -> f64 {
155 self.alignments
156 .iter()
157 .map(|a| a.applied_offset_ms.abs())
158 .fold(0.0_f64, f64::max)
159 }
160
161 #[must_use]
163 pub fn synchronized_count(&self) -> usize {
164 self.alignments
165 .iter()
166 .filter(|a| a.is_synchronized())
167 .count()
168 }
169
170 #[must_use]
172 pub fn all_synchronized(&self) -> bool {
173 !self.alignments.is_empty()
174 && self
175 .alignments
176 .iter()
177 .all(TemporalAlignment::is_synchronized)
178 }
179
180 #[must_use]
182 pub fn get_alignment(&self, stream_index: usize) -> Option<&TemporalAlignment> {
183 self.alignments
184 .iter()
185 .find(|a| a.stream_index == stream_index)
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
196 fn test_apply_to_pts_positive() {
197 let off = AlignmentOffset::new(0, 50.0, 0.9);
198 assert!((off.apply_to_pts(1000.0) - 1050.0).abs() < f64::EPSILON);
199 }
200
201 #[test]
202 fn test_apply_to_pts_negative() {
203 let off = AlignmentOffset::new(0, -30.0, 0.9);
204 assert!((off.apply_to_pts(1000.0) - 970.0).abs() < f64::EPSILON);
205 }
206
207 #[test]
208 fn test_apply_to_pts_zero() {
209 let off = AlignmentOffset::new(0, 0.0, 1.0);
210 assert!((off.apply_to_pts(500.0) - 500.0).abs() < f64::EPSILON);
211 }
212
213 #[test]
214 fn test_within_tolerance_true() {
215 let off = AlignmentOffset::new(0, 5.0, 0.9);
216 assert!(off.is_within_tolerance(10.0));
217 }
218
219 #[test]
220 fn test_within_tolerance_false() {
221 let off = AlignmentOffset::new(0, 20.0, 0.9);
222 assert!(!off.is_within_tolerance(10.0));
223 }
224
225 #[test]
228 fn test_is_synchronized_true() {
229 let ta = TemporalAlignment::new(0, 5.0, 0.01, true);
230 assert!(ta.is_synchronized());
231 }
232
233 #[test]
234 fn test_is_synchronized_not_aligned() {
235 let ta = TemporalAlignment::new(0, 5.0, 0.01, false);
236 assert!(!ta.is_synchronized());
237 }
238
239 #[test]
240 fn test_is_synchronized_high_drift() {
241 let ta = TemporalAlignment::new(0, 5.0, 0.5, true);
242 assert!(!ta.is_synchronized());
243 }
244
245 #[test]
246 fn test_predicted_drift() {
247 let ta = TemporalAlignment::new(0, 0.0, 0.05, true);
248 assert!((ta.predicted_drift_ms(100.0) - 5.0).abs() < f64::EPSILON);
249 }
250
251 #[test]
254 fn test_aligner_empty() {
255 let mut aligner = StreamAligner::default_aligner();
256 aligner.align_streams(&[]);
257 assert!((aligner.max_offset_ms()).abs() < f64::EPSILON);
258 assert!(!aligner.all_synchronized());
259 }
260
261 #[test]
262 fn test_aligner_applies_confident_offset() {
263 let mut aligner = StreamAligner::default_aligner();
264 let offsets = [AlignmentOffset::new(0, 8.0, 0.95)];
265 aligner.align_streams(&offsets);
266 let al = aligner.get_alignment(0).expect("al should be valid");
267 assert!((al.applied_offset_ms - 8.0).abs() < f64::EPSILON);
268 assert!(al.aligned);
269 }
270
271 #[test]
272 fn test_aligner_skips_low_confidence() {
273 let mut aligner = StreamAligner::default_aligner();
274 let offsets = [AlignmentOffset::new(0, 15.0, 0.2)];
275 aligner.align_streams(&offsets);
276 let al = aligner.get_alignment(0).expect("al should be valid");
277 assert!((al.applied_offset_ms).abs() < f64::EPSILON);
278 assert!(!al.aligned);
279 }
280
281 #[test]
282 fn test_aligner_max_offset() {
283 let mut aligner = StreamAligner::default_aligner();
284 let offsets = [
285 AlignmentOffset::new(0, 3.0, 0.9),
286 AlignmentOffset::new(1, 7.0, 0.9),
287 AlignmentOffset::new(2, 1.5, 0.9),
288 ];
289 aligner.align_streams(&offsets);
290 assert!((aligner.max_offset_ms() - 7.0).abs() < f64::EPSILON);
291 }
292
293 #[test]
294 fn test_aligner_all_synchronized() {
295 let mut aligner = StreamAligner::default_aligner();
296 let offsets = [
297 AlignmentOffset::new(0, 2.0, 0.95),
298 AlignmentOffset::new(1, 4.0, 0.85),
299 ];
300 aligner.align_streams(&offsets);
301 assert!(aligner.all_synchronized());
302 }
303
304 #[test]
305 fn test_aligner_synchronized_count() {
306 let mut aligner = StreamAligner::default_aligner();
307 let offsets = [
308 AlignmentOffset::new(0, 2.0, 0.95), AlignmentOffset::new(1, 50.0, 0.10), ];
311 aligner.align_streams(&offsets);
312 assert_eq!(aligner.synchronized_count(), 1);
313 }
314
315 #[test]
316 fn test_aligner_get_alignment_missing() {
317 let aligner = StreamAligner::default_aligner();
318 assert!(aligner.get_alignment(99).is_none());
319 }
320}