Relax the upper bound of the one-shot drift correction sanity check from deadline + maxMediaRunningTimeDelay to deadline + 2*maxMediaRunningTimeDelay. This prevents rejecting corrections where the adjusted PTS is only slightly ahead of the media time due to jitter.
* Fix data race contributing to wrong value of ended_at
Fix `endedAt == startedAt` (zero duration) when all tracks are removed before `Synchronizer.End()` is called.
This is a race in the egress shutdown path: track workers call `RemoveTrack` (which deletes from `p.tracks`) before the pipeline calls `End()`. When `End()` iterates participants to find `maxPTS`, the tracks map is already empty, so `maxPTS = 0` and `endedAt = startedAt`.
The bug was latent before the duration inflation fix but masked by `time.Now()` fallback.
* extract LastPTSAdjusted for better encapsulation
* adding LastPTSAdjusted
This change introduces an explicit sender report sync mode model in the synchronizer and adds a new one-shot drift correction path for mixer-oriented pipelines.
The main motivation is SIP silence suppression (which frequently doesn't get signalled properly) drift in room composite recordings. In that scenario, RTP timestamps can remain continuous while wall clock time advances much faster, which means the existing silence gap handling does not trigger. Over time, the audio track falls behind the mixer window and samples get dropped before the late correction path kicks in.
* Rename log field "pID" to "participantID" for consistency
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* tidy
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
End() previously computed endedAt as wallclock + max PTS offset, which included the late-subscription base offset (e.g. +44s for a track that joined 44s late), inflating reported duration far beyond actual recording time. This replaces the offset-based computation with the actual max adjusted PTS reached by any track, making both endedAt and the drain ceiling directly reflect the media timeline.
Changes aimed to reduce duplication a bit. Extracted 2 functions which are relatively independent, wanted to do more but that requires either having functions with a lot of params of some sort of context which doesn't make it really easier to read (high coupling with caller).
Smaller tidying up changes: replacing mono with time and interface with any
- Fix potential deadlock between Synchronizer.End() and TrackSynchronizer.GetPTS()
caused by inverse lock ordering
- getPTSWithoutRebase() and getPTSWithRebase() now release the TrackSynchronizer lock
before calling into Synchronizer methods
Problem
A deadlock could occur when two goroutines acquired locks in inverse order:
Thread A (End()): Synchronizer.Lock() x
Thread B (GetPTS()):
────────────────────────────────────────
Thread A (End()):
Thread B (GetPTS()): TrackSynchronizer.Lock() x
────────────────────────────────────────
Thread A (End()): → p.getMaxOffset() → t.Lock() BLOCKED
Thread B (GetPTS()):
────────────────────────────────────────
Thread A (End()):
Thread B (GetPTS()): → getExternalMediaDeadline() → s.RLock() BLOCKED
Lock order in End(): Synchronizer → participantSynchronizer → TrackSynchronizer
Lock order in GetPTS(): TrackSynchronizer → Synchronizer (inverse)
Going forward we need to avoid invocking any actions on syncrhonizer from the track synchronizer while holding its lock. Following that rule will ensure no lock inversion deadlock could happen.
* add parser for H265 SEI to extract timestamp metadata to attach custom trailer
* adding h265 sei parser
* add unit test for h265 timestamp parser
* clear H265 SEI NALs after parsing timestamp
* Support av1 publishing
* use full codec string
* Update readersampleprovider.go
* Update readersampleprovider.go
---------
Co-authored-by: Paul Wells <paulwe@gmail.com>
* pass interceptors to subscriber (required for stats interceptor)
* Update engine.go
* allow custom interceptors to be added without losing default interceptors
* go fmt
It is possible to see duplicate sender report at track synchronizer
because of how RTCP compound packets are handled. pion/srtp will forward
the full compound RTCP packet to all the read streams. The synchronizer
fields RTCP packets for all SSRCs it is managing, i. e. all the tracks
it is trying to synchronize. So, it will receive the same compound RTCP
packet once per SSRC contained in the compound packet.