#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AlignmentOffset {
pub stream_index: usize,
pub offset_ms: f64,
pub confidence: f64,
}
impl AlignmentOffset {
#[must_use]
pub fn new(stream_index: usize, offset_ms: f64, confidence: f64) -> Self {
Self {
stream_index,
offset_ms,
confidence,
}
}
#[must_use]
pub fn apply_to_pts(&self, raw_pts_ms: f64) -> f64 {
raw_pts_ms + self.offset_ms
}
#[must_use]
pub fn is_within_tolerance(&self, tolerance_ms: f64) -> bool {
self.offset_ms.abs() <= tolerance_ms
}
}
#[derive(Debug, Clone)]
pub struct TemporalAlignment {
pub stream_index: usize,
pub applied_offset_ms: f64,
pub drift_rate_ms_per_s: f64,
pub aligned: bool,
}
impl TemporalAlignment {
#[must_use]
pub fn new(
stream_index: usize,
applied_offset_ms: f64,
drift_rate_ms_per_s: f64,
aligned: bool,
) -> Self {
Self {
stream_index,
applied_offset_ms,
drift_rate_ms_per_s,
aligned,
}
}
#[must_use]
pub fn is_synchronized(&self) -> bool {
self.aligned && self.drift_rate_ms_per_s.abs() < 0.1
}
#[must_use]
pub fn predicted_drift_ms(&self, duration_s: f64) -> f64 {
self.drift_rate_ms_per_s * duration_s
}
}
#[derive(Debug, Clone)]
pub struct StreamAlignerConfig {
pub tolerance_ms: f64,
pub max_drift_ms_per_s: f64,
pub min_confidence: f64,
}
impl Default for StreamAlignerConfig {
fn default() -> Self {
Self {
tolerance_ms: 10.0,
max_drift_ms_per_s: 0.5,
min_confidence: 0.60,
}
}
}
#[derive(Debug)]
pub struct StreamAligner {
config: StreamAlignerConfig,
alignments: Vec<TemporalAlignment>,
}
impl StreamAligner {
#[must_use]
pub fn new(config: StreamAlignerConfig) -> Self {
Self {
config,
alignments: Vec::new(),
}
}
#[must_use]
pub fn default_aligner() -> Self {
Self::new(StreamAlignerConfig::default())
}
pub fn align_streams(&mut self, offsets: &[AlignmentOffset]) -> &[TemporalAlignment] {
self.alignments.clear();
for off in offsets {
let aligned = off.confidence >= self.config.min_confidence;
let applied = if aligned { off.offset_ms } else { 0.0 };
let drift = 0.0_f64;
self.alignments.push(TemporalAlignment::new(
off.stream_index,
applied,
drift,
aligned,
));
}
&self.alignments
}
#[must_use]
pub fn max_offset_ms(&self) -> f64 {
self.alignments
.iter()
.map(|a| a.applied_offset_ms.abs())
.fold(0.0_f64, f64::max)
}
#[must_use]
pub fn synchronized_count(&self) -> usize {
self.alignments
.iter()
.filter(|a| a.is_synchronized())
.count()
}
#[must_use]
pub fn all_synchronized(&self) -> bool {
!self.alignments.is_empty()
&& self
.alignments
.iter()
.all(TemporalAlignment::is_synchronized)
}
#[must_use]
pub fn get_alignment(&self, stream_index: usize) -> Option<&TemporalAlignment> {
self.alignments
.iter()
.find(|a| a.stream_index == stream_index)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apply_to_pts_positive() {
let off = AlignmentOffset::new(0, 50.0, 0.9);
assert!((off.apply_to_pts(1000.0) - 1050.0).abs() < f64::EPSILON);
}
#[test]
fn test_apply_to_pts_negative() {
let off = AlignmentOffset::new(0, -30.0, 0.9);
assert!((off.apply_to_pts(1000.0) - 970.0).abs() < f64::EPSILON);
}
#[test]
fn test_apply_to_pts_zero() {
let off = AlignmentOffset::new(0, 0.0, 1.0);
assert!((off.apply_to_pts(500.0) - 500.0).abs() < f64::EPSILON);
}
#[test]
fn test_within_tolerance_true() {
let off = AlignmentOffset::new(0, 5.0, 0.9);
assert!(off.is_within_tolerance(10.0));
}
#[test]
fn test_within_tolerance_false() {
let off = AlignmentOffset::new(0, 20.0, 0.9);
assert!(!off.is_within_tolerance(10.0));
}
#[test]
fn test_is_synchronized_true() {
let ta = TemporalAlignment::new(0, 5.0, 0.01, true);
assert!(ta.is_synchronized());
}
#[test]
fn test_is_synchronized_not_aligned() {
let ta = TemporalAlignment::new(0, 5.0, 0.01, false);
assert!(!ta.is_synchronized());
}
#[test]
fn test_is_synchronized_high_drift() {
let ta = TemporalAlignment::new(0, 5.0, 0.5, true);
assert!(!ta.is_synchronized());
}
#[test]
fn test_predicted_drift() {
let ta = TemporalAlignment::new(0, 0.0, 0.05, true);
assert!((ta.predicted_drift_ms(100.0) - 5.0).abs() < f64::EPSILON);
}
#[test]
fn test_aligner_empty() {
let mut aligner = StreamAligner::default_aligner();
aligner.align_streams(&[]);
assert!((aligner.max_offset_ms()).abs() < f64::EPSILON);
assert!(!aligner.all_synchronized());
}
#[test]
fn test_aligner_applies_confident_offset() {
let mut aligner = StreamAligner::default_aligner();
let offsets = [AlignmentOffset::new(0, 8.0, 0.95)];
aligner.align_streams(&offsets);
let al = aligner.get_alignment(0).expect("al should be valid");
assert!((al.applied_offset_ms - 8.0).abs() < f64::EPSILON);
assert!(al.aligned);
}
#[test]
fn test_aligner_skips_low_confidence() {
let mut aligner = StreamAligner::default_aligner();
let offsets = [AlignmentOffset::new(0, 15.0, 0.2)];
aligner.align_streams(&offsets);
let al = aligner.get_alignment(0).expect("al should be valid");
assert!((al.applied_offset_ms).abs() < f64::EPSILON);
assert!(!al.aligned);
}
#[test]
fn test_aligner_max_offset() {
let mut aligner = StreamAligner::default_aligner();
let offsets = [
AlignmentOffset::new(0, 3.0, 0.9),
AlignmentOffset::new(1, 7.0, 0.9),
AlignmentOffset::new(2, 1.5, 0.9),
];
aligner.align_streams(&offsets);
assert!((aligner.max_offset_ms() - 7.0).abs() < f64::EPSILON);
}
#[test]
fn test_aligner_all_synchronized() {
let mut aligner = StreamAligner::default_aligner();
let offsets = [
AlignmentOffset::new(0, 2.0, 0.95),
AlignmentOffset::new(1, 4.0, 0.85),
];
aligner.align_streams(&offsets);
assert!(aligner.all_synchronized());
}
#[test]
fn test_aligner_synchronized_count() {
let mut aligner = StreamAligner::default_aligner();
let offsets = [
AlignmentOffset::new(0, 2.0, 0.95), AlignmentOffset::new(1, 50.0, 0.10), ];
aligner.align_streams(&offsets);
assert_eq!(aligner.synchronized_count(), 1);
}
#[test]
fn test_aligner_get_alignment_missing() {
let aligner = StreamAligner::default_aligner();
assert!(aligner.get_alignment(99).is_none());
}
}