Outbound Opus-over-RTP audio (WebRTC, e.g. werift RTCPeerConnection) sounds broken/overlapping specifically after any silence gap, TTS pause, or barge-in/interrupt-triggered queue clear — even though continuous unbroken speech plays fine. A naive per-tick RTP sender that only advances `timestamp`/`sequenceNumber` when it actually has a payload to send (skipping the advance during silent ticks) freezes the RTP timestamp during gaps. When audio resumes, the next packet's timestamp is contiguous with the last one sent before the gap, even though real wall-clock time has passed. The receiver's jitter buffer sees no timestamp discontinuity, believes no time passed, and schedules/plays the new talkspurt immediately adjacent to (or overlapping with) the previous one's tail. No error is thrown; the failure is purely audible.
Fix: Advance the RTP timestamp on every 20ms tick of the sending clock, including silent ticks with no payload to send (i.e. the timestamp always reflects real elapsed time, not just time spent actually transmitting), and set the RTP marker bit to true on the first packet sent after any gap (silence, or an explicit queue-clear from a barge-in/interrupt). Per RFC 7587 §4.2, Opus RTP always uses a 48000 Hz clock regardless of channel count or the internal encoding sample rate, so a 20ms frame is exactly 960 timestamp units — this holds true whether or not a packet was actually sent for that slot, so jumping the timestamp forward by `silentSlots * 960` during a gap is correct and keeps the RFC 7587 DTX invariant that successive timestamps differ by a multiple of 120. Per RFC 3550/3551 §4.1, the marker bit signals the start of a talkspurt to the receiver, letting its jitter buffer correctly recognize the discontinuity and re-anchor playout timing instead of assuming continuous audio. Verified against werift's actual sender code (RTCRtpSender.sendRtp in werift's compiled lib/index.mjs): it applies only a constant timestampOffset/seqOffset to whatever header values are passed in and does not recompute or override them, so caller-supplied timestamp jumps and marker bits reach the wire unmodified. Also confirmed MediaStreamTrack.writeRtp only overrides payloadType, preserving timestamp/sequenceNumber/marker exactly as constructed.
webrtcrtpopusweriftjitter-bufferaudio
References
- https://www.rfc-editor.org/rfc/rfc7587 — Section 4.2: 'The RTP timestamp is incremented with a 48000 Hz clock rate for all modes of Opus and all sampling rates' -- 20ms = 960 timestamp units regardless of channels/internal rate. Also specifies DTX must keep successive timestamps differing by a multiple of 120 when frames are dropped for silence.
- https://www.rfc-editor.org/rfc/rfc3550 — Defines the RTP marker bit semantics used to signal significant events in the packet stream, such as the first packet of a talkspurt after silence, which receivers use to correctly reset jitter-buffer/playout timing.