@@ -8,11 +8,20 @@ use str0m::{
88use tokio:: time:: Instant ;
99
1010const MIN_SR_UPDATE_INTERVAL : Duration = Duration :: from_millis ( 200 ) ;
11+ const MAX_DRIFT_PPM : f64 = 50_000.0 ;
12+ const MAX_RTP_GAP_SECS : f64 = 10.0 ;
1113
1214#[ derive( Debug , Clone , Copy ) ]
1315struct ClockReference {
1416 rtp_time : MediaTime ,
1517 ntp_time : SystemTime ,
18+ arrival_ts : Instant ,
19+ }
20+
21+ impl ClockReference {
22+ fn server_time_at_anchor ( & self , ntp_delta : Duration ) -> Instant {
23+ self . arrival_ts + ntp_delta
24+ }
1625}
1726
1827#[ derive( Debug ) ]
@@ -24,6 +33,9 @@ pub struct Synchronizer {
2433 base_rtp : Option < MediaTime > ,
2534 base_server_time : Option < Instant > ,
2635 pub estimated_clock_drift_ppm : f64 ,
36+ /// The server Instant that corresponds to a specific NTP time, representing
37+ /// the minimum observed propagation delay.
38+ ntp_anchor : Option < ClockReference > ,
2739}
2840
2941impl Synchronizer {
@@ -36,6 +48,7 @@ impl Synchronizer {
3648 base_rtp : None ,
3749 base_server_time : None ,
3850 estimated_clock_drift_ppm : 0.0 ,
51+ ntp_anchor : None ,
3952 }
4053 }
4154
@@ -45,18 +58,46 @@ impl Synchronizer {
4558 }
4659
4760 if self . base_rtp . is_none ( ) {
48- self . base_rtp = Some ( packet. rtp_ts ) ;
49- self . base_server_time = Some ( packet. arrival_ts ) ;
61+ self . reset_baseline ( packet. rtp_ts , packet. arrival_ts ) ;
5062 }
5163
5264 let base_rtp = self . base_rtp . unwrap ( ) ;
5365 let mut base_server_time = self . base_server_time . unwrap ( ) ;
5466
5567 let rtp_delta = ( packet. rtp_ts . numer ( ) as i64 ) . wrapping_sub ( base_rtp. numer ( ) as i64 ) ;
68+ let max_ticks = ( MAX_RTP_GAP_SECS * self . clock_rate . get ( ) as f64 ) as i64 ;
69+
70+ // Auto-reset on massive RTP leaps to prevent timeline corruption
71+ if rtp_delta. abs ( ) > max_ticks {
72+ self . reset_baseline ( packet. rtp_ts , packet. arrival_ts ) ;
73+ packet. playout_time = packet. arrival_ts ;
74+ return ;
75+ }
76+
5677 let drift = self . estimated_clock_drift_ppm / 1_000_000.0 ;
57- let drift_correction = 1.0 / ( 1.0 + drift) ;
58- let seconds_delta = rtp_delta as f64 / self . clock_rate . get ( ) as f64 * drift_correction;
78+ let drift_correction = 1.0 / ( 1.0 + drift) . max ( 0.001 ) ;
79+
80+ // 1. If we have SR info, we can calculate the NTP time of this packet and use it for alignment.
81+ let mut ntp_expected_playout = None ;
82+ if let Some ( latest) = self . latest_sr {
83+ let rtp_delta = ( packet. rtp_ts . numer ( ) as i64 ) . wrapping_sub ( latest. rtp_time . numer ( ) as i64 ) ;
84+ let ntp_delta_secs = rtp_delta as f64 / self . clock_rate . get ( ) as f64 * drift_correction;
85+ let ntp_pkt = if ntp_delta_secs >= 0.0 {
86+ latest. ntp_time + Duration :: from_secs_f64 ( ntp_delta_secs)
87+ } else {
88+ latest. ntp_time - Duration :: from_secs_f64 ( -ntp_delta_secs)
89+ } ;
90+
91+ if let Some ( anchor) = self . ntp_anchor {
92+ let ntp_delta = ntp_pkt
93+ . duration_since ( anchor. ntp_time )
94+ . unwrap_or ( Duration :: ZERO ) ;
95+ ntp_expected_playout = Some ( anchor. server_time_at_anchor ( ntp_delta) ) ;
96+ }
97+ }
5998
99+ // 2. Fallback/Standard path: use the local RTP-based baseline
100+ let seconds_delta = rtp_delta as f64 / self . clock_rate . get ( ) as f64 * drift_correction;
60101 let mut expected_playout = if seconds_delta >= 0.0 {
61102 base_server_time + Duration :: from_secs_f64 ( seconds_delta)
62103 } else {
@@ -65,7 +106,14 @@ impl Synchronizer {
65106 . unwrap_or ( packet. arrival_ts )
66107 } ;
67108
68- // Minimum envelope filter: safely absorb all network jitter without bounding box bounce
109+ // 3. Re-align: If the NTP-based estimate is significantly different, or if we just want
110+ // to sync multiple tracks, we should prioritize the NTP timeline.
111+ if let Some ( ntp_playout) = ntp_expected_playout {
112+ // We use the NTP playout if it's available, as it's synchronized across all tracks.
113+ expected_playout = ntp_playout;
114+ }
115+
116+ // Minimum envelope filter: absorbs network jitter
69117 if packet. arrival_ts < expected_playout {
70118 let error = expected_playout. duration_since ( packet. arrival_ts ) ;
71119 if let Some ( new_base) = base_server_time. checked_sub ( error) {
@@ -78,6 +126,15 @@ impl Synchronizer {
78126 packet. playout_time = expected_playout;
79127 }
80128
129+ fn reset_baseline ( & mut self , rtp_ts : MediaTime , arrival_ts : Instant ) {
130+ self . base_rtp = Some ( rtp_ts) ;
131+ self . base_server_time = Some ( arrival_ts) ;
132+ self . first_sr = None ;
133+ self . latest_sr = None ;
134+ self . ntp_anchor = None ;
135+ self . estimated_clock_drift_ppm = 0.0 ;
136+ }
137+
81138 fn add_sender_report ( & mut self , sr : SenderInfo , now : Instant ) {
82139 if let Some ( last_time) = self . last_sr_time
83140 && now. duration_since ( last_time) < MIN_SR_UPDATE_INTERVAL
@@ -88,6 +145,7 @@ impl Synchronizer {
88145 let current = ClockReference {
89146 rtp_time : sr. rtp_time ,
90147 ntp_time : sr. ntp_time ,
148+ arrival_ts : now,
91149 } ;
92150
93151 if let Some ( last) = self . latest_sr {
@@ -97,7 +155,7 @@ impl Synchronizer {
97155 return ;
98156 }
99157 } else {
100- self . first_sr = Some ( current) ; // Permanent lock on the first report
158+ self . first_sr = Some ( current) ;
101159 }
102160
103161 self . latest_sr = Some ( current) ;
@@ -107,6 +165,22 @@ impl Synchronizer {
107165 self . estimated_clock_drift_ppm = Self :: compute_clock_drift ( & first, & latest) ;
108166 histogram ! ( "rtp_sync_clock_drift_ppm" ) . record ( self . estimated_clock_drift_ppm ) ;
109167 }
168+
169+ // Update the NTP anchor with a minimum envelope filter
170+ if let Some ( anchor) = self . ntp_anchor {
171+ let ntp_delta = current
172+ . ntp_time
173+ . duration_since ( anchor. ntp_time )
174+ . unwrap_or ( Duration :: ZERO ) ;
175+ let expected_server = anchor. arrival_ts + ntp_delta;
176+ if now < expected_server {
177+ // This SR arrived earlier than the previous anchor relative to NTP.
178+ // It represents a lower propagation delay.
179+ self . ntp_anchor = Some ( current) ;
180+ }
181+ } else {
182+ self . ntp_anchor = Some ( current) ;
183+ }
110184 }
111185
112186 fn compute_clock_drift ( first : & ClockReference , current : & ClockReference ) -> f64 {
@@ -125,9 +199,15 @@ impl Synchronizer {
125199 }
126200
127201 let expected_rtp_delta = sender_ntp_delta_secs * first. rtp_time . frequency ( ) . get ( ) as f64 ;
202+
203+ if expected_rtp_delta <= 0.0 {
204+ return 0.0 ;
205+ }
206+
128207 let drift_ratio = ( sender_rtp_delta as f64 - expected_rtp_delta) / expected_rtp_delta;
129208
130- drift_ratio * 1_000_000.0
209+ // Clamp drift to physical boundaries (+/- 5%) to avoid infinity during pauses
210+ ( drift_ratio * 1_000_000.0 ) . clamp ( -MAX_DRIFT_PPM , MAX_DRIFT_PPM )
131211 }
132212
133213 pub fn is_synchronized ( & self ) -> bool {
@@ -159,7 +239,6 @@ mod tests {
159239 let mut sync = Synchronizer :: new ( VIDEO_FREQUENCY ) ;
160240 let base_time = Instant :: now ( ) ;
161241
162- // 1. First packet establishes baseline (simulated with 100ms jitter delay)
163242 let p1_arrival = base_time + Duration :: from_millis ( 100 ) ;
164243 let mut p1 = RtpPacket {
165244 rtp_ts : MediaTime :: from_90khz ( 90_000 ) ,
@@ -169,29 +248,22 @@ mod tests {
169248 sync. process ( & mut p1, None ) ;
170249 assert_eq ! ( p1. playout_time, p1_arrival) ;
171250
172- // 2. Second packet arrives exactly 1 second of media later, but with NO jitter.
173- // This packet arrives 900ms after the first packet instead of 1000ms.
174251 let p2_arrival = base_time + Duration :: from_secs ( 1 ) ;
175252 let mut p2 = RtpPacket {
176253 rtp_ts : MediaTime :: from_90khz ( 180_000 ) ,
177254 arrival_ts : p2_arrival,
178255 ..Default :: default ( )
179256 } ;
180257 sync. process ( & mut p2, None ) ;
181-
182- // The baseline should shift backward seamlessly! The playout matches arrival.
183258 assert_eq ! ( p2. playout_time, p2_arrival) ;
184259
185- // 3. Third packet arrives 2 seconds of media later, but with 50ms of jitter again.
186260 let p3_arrival = base_time + Duration :: from_secs ( 2 ) + Duration :: from_millis ( 50 ) ;
187261 let mut p3 = RtpPacket {
188262 rtp_ts : MediaTime :: from_90khz ( 270_000 ) ,
189263 arrival_ts : p3_arrival,
190264 ..Default :: default ( )
191265 } ;
192266 sync. process ( & mut p3, None ) ;
193-
194- // The expected playout time MUST strip the jitter and map exactly to the updated server timeline.
195267 let expected_p3_playout = base_time + Duration :: from_secs ( 2 ) ;
196268 assert_eq ! ( p3. playout_time, expected_p3_playout) ;
197269 }
@@ -243,7 +315,6 @@ mod tests {
243315 assert_eq ! ( sync_perfect. estimated_clock_drift_ppm. round( ) as i64 , 0 ) ;
244316 assert_eq ! ( sync_drifting. estimated_clock_drift_ppm. round( ) as i64 , 1000 ) ;
245317
246- // Verify alignment 10 seconds in.
247318 let event_time = base_time + Duration :: from_secs ( 10 ) ;
248319
249320 let mut p_perf = RtpPacket {
@@ -260,7 +331,6 @@ mod tests {
260331 } ;
261332 sync_drifting. process ( & mut p_drift, None ) ;
262333
263- // The absolute offset perfectly aligns both playout clocks to the exact same Instant base
264334 let diff = if p_perf. playout_time > p_drift. playout_time {
265335 p_perf. playout_time - p_drift. playout_time
266336 } else {
@@ -308,7 +378,6 @@ mod tests {
308378 ) ;
309379 }
310380
311- // Test 10s into the future
312381 let event_time = base_time + Duration :: from_secs ( 10 ) ;
313382
314383 let mut p_perf = RtpPacket {
@@ -325,12 +394,128 @@ mod tests {
325394 } ;
326395 sync_drifting. process ( & mut p_drift, None ) ;
327396
328- // Even with fully decoupled NTP uptime bases, the single shared server baseline aligns flawlessly.
329397 let diff = if p_perf. playout_time > p_drift. playout_time {
330398 p_perf. playout_time - p_drift. playout_time
331399 } else {
332400 p_drift. playout_time - p_perf. playout_time
333401 } ;
334402 assert ! ( diff < Duration :: from_micros( 1 ) ) ;
335403 }
404+
405+ #[ test]
406+ fn test_massive_rtp_gap_resets_baseline ( ) {
407+ let mut sync = Synchronizer :: new ( VIDEO_FREQUENCY ) ;
408+ let base_time = Instant :: now ( ) ;
409+
410+ // 1. Normal packet
411+ let p1_arrival = base_time;
412+ let mut p1 = RtpPacket {
413+ rtp_ts : MediaTime :: from_90khz ( 90_000 ) ,
414+ arrival_ts : p1_arrival,
415+ ..Default :: default ( )
416+ } ;
417+ sync. process ( & mut p1, None ) ;
418+
419+ // 2. Massive gap (e.g. 15 seconds)
420+ let p2_arrival = base_time + Duration :: from_secs ( 15 ) ;
421+ let mut p2 = RtpPacket {
422+ rtp_ts : MediaTime :: from_90khz ( 90_000 + ( 15 * 90_000 ) ) ,
423+ arrival_ts : p2_arrival,
424+ ..Default :: default ( )
425+ } ;
426+ sync. process ( & mut p2, None ) ;
427+
428+ // Expect the baseline to reset, mapping playout exactly to the new arrival
429+ assert_eq ! ( p2. playout_time, p2_arrival) ;
430+ assert_eq ! ( sync. base_rtp. unwrap( ) , p2. rtp_ts) ;
431+ assert_eq ! ( sync. estimated_clock_drift_ppm, 0.0 ) ;
432+ }
433+
434+ #[ test]
435+ fn test_ntp_alignment_recovers_from_initial_delay ( ) {
436+ let mut sync = Synchronizer :: new ( VIDEO_FREQUENCY ) ;
437+ let base_time = Instant :: now ( ) ;
438+ let ntp_base = UNIX_EPOCH + Duration :: from_secs ( NTP_UNIX_OFFSET_SECS + 1000 ) ;
439+
440+ // 1. First packet arrives with 5s delay. Baseline pins to 5s.
441+ let mut p1 = RtpPacket {
442+ rtp_ts : MediaTime :: from_90khz ( 0 ) ,
443+ arrival_ts : base_time + Duration :: from_secs ( 5 ) ,
444+ ..Default :: default ( )
445+ } ;
446+ sync. process ( & mut p1, None ) ;
447+ assert_eq ! ( p1. playout_time - base_time, Duration :: from_secs( 5 ) ) ;
448+
449+ // 2. An SR arrives that reveals the true NTP.
450+ // Even if the packet carrying the SR is late, the anchor filter
451+ // will establish a mapping.
452+ let mut p2 = RtpPacket {
453+ rtp_ts : MediaTime :: from_90khz ( 90_000 ) , // 1s media later
454+ arrival_ts : base_time + Duration :: from_secs ( 6 ) , // Still 5s late
455+ ..Default :: default ( )
456+ } ;
457+ sync. process (
458+ & mut p2,
459+ Some ( create_sr ( MediaTime :: from_90khz ( 0 ) , ntp_base) ) ,
460+ ) ;
461+ // It's still late because we haven't seen a fast packet yet.
462+ assert_eq ! ( p2. playout_time - base_time, Duration :: from_secs( 6 ) ) ;
463+
464+ // 3. A fast packet arrives (only 100ms delay).
465+ // Sent at T=2s (RTP=180,000). Arrives at T=2.1s.
466+ let mut p3 = RtpPacket {
467+ rtp_ts : MediaTime :: from_90khz ( 180_000 ) ,
468+ arrival_ts : base_time + Duration :: from_secs ( 2 ) + Duration :: from_millis ( 100 ) ,
469+ ..Default :: default ( )
470+ } ;
471+ sync. process ( & mut p3, None ) ;
472+
473+ // The playout MUST snap back to the low-latency timeline!
474+ assert_eq ! ( p3. playout_time - base_time, Duration :: from_millis( 2100 ) ) ;
475+ }
476+
477+ #[ test]
478+ fn test_independent_streams_align_via_shared_ntp ( ) {
479+ let base_time = Instant :: now ( ) ;
480+ let ntp_base = UNIX_EPOCH + Duration :: from_secs ( NTP_UNIX_OFFSET_SECS + 1000 ) ;
481+
482+ let mut sync1 = Synchronizer :: new ( VIDEO_FREQUENCY ) ;
483+ let mut sync2 = Synchronizer :: new ( VIDEO_FREQUENCY ) ;
484+
485+ // Stream 1: Low delay (100ms)
486+ let mut p1 = RtpPacket {
487+ rtp_ts : MediaTime :: from_90khz ( 0 ) ,
488+ arrival_ts : base_time + Duration :: from_millis ( 100 ) ,
489+ ..Default :: default ( )
490+ } ;
491+ sync1. process ( & mut p1, Some ( create_sr ( MediaTime :: from_90khz ( 0 ) , ntp_base) ) ) ;
492+
493+ // Stream 2: High delay (5s)
494+ let mut p2 = RtpPacket {
495+ rtp_ts : MediaTime :: from_90khz ( 0 ) ,
496+ arrival_ts : base_time + Duration :: from_secs ( 5 ) ,
497+ ..Default :: default ( )
498+ } ;
499+ sync2. process ( & mut p2, Some ( create_sr ( MediaTime :: from_90khz ( 0 ) , ntp_base) ) ) ;
500+
501+ // Now both have SRs. Stream 2 is still "late" because it hasn't seen a fast packet.
502+ // But if we send a packet that arrives with low delay for Stream 2:
503+ let mut p3 = RtpPacket {
504+ rtp_ts : MediaTime :: from_90khz ( 90_000 ) ,
505+ arrival_ts : base_time + Duration :: from_secs ( 1 ) + Duration :: from_millis ( 100 ) ,
506+ ..Default :: default ( )
507+ } ;
508+ sync2. process ( & mut p3, None ) ;
509+
510+ // And Stream 1 sends its own packet at the same media time:
511+ let mut p4 = RtpPacket {
512+ rtp_ts : MediaTime :: from_90khz ( 90_000 ) ,
513+ arrival_ts : base_time + Duration :: from_secs ( 1 ) + Duration :: from_millis ( 100 ) ,
514+ ..Default :: default ( )
515+ } ;
516+ sync1. process ( & mut p4, None ) ;
517+
518+ // They must be perfectly aligned now!
519+ assert_eq ! ( p3. playout_time, p4. playout_time) ;
520+ }
336521}
0 commit comments