Skip to content

fix(RTC): stop the remote audio wedge watchdog firing when nothing is being sent - #3087

Merged
JonathanLennox merged 3 commits into
jitsi:masterfrom
JonathanLennox:audio-wedge-p2p-gate
Aug 13, 2026
Merged

fix(RTC): stop the remote audio wedge watchdog firing when nothing is being sent#3087
JonathanLennox merged 3 commits into
jitsi:masterfrom
JonathanLennox:audio-wedge-p2p-gate

Conversation

@JonathanLennox

Copy link
Copy Markdown
Member

The remote audio wedge watchdog added in #3051 / #3062 fires on the absence of inbound RTP, but zero inbound RTP is not exclusive to the wedge. A field report shows it recycling a healthy source 23 times over 15 minutes in an ordinary two-person call.

What happened in the field

A 2-participant call keeps its JVB session alive as a fallback and merely suspends it for the duration of the P2P session, which leaves every eligibility condition the watchdog tests satisfied while no RTP can arrive:

  • _removeRemoteJVBTracks() only detaches the tracks from the JitsiParticipant and emits TRACK_REMOVED; TraceablePeerConnection.remoteTracks — what the detector iterates — still holds them for the rest of the call.
  • setMediaTransferActive(false) deactivates the sender encodings on both peers, so the bridge has nothing to forward and the session reads zero inbound RTP.
  • The JVB stats collector keeps polling (stopRemoteStats runs only in _stopJvbSession), so INBOUND_AUDIO_STATS keeps arriving with zeros, and the detector itself only stops in JingleSessionPC.close().
  • Mute state is signaling-derived, so the sources stay unmuted.

From the log, bracketed exactly by the suspend/resume pair:

16:11:31.537  Starting remote audio wedge detector (wedgeTimeout=15000ms)
16:11:31.714  Received AudioSourcesMap: [{"source":"ebece0cc-a0","owner":"ebece0cc","ssrc":2360120133}]
16:11:31.745  Peer to peer connection established!
16:11:31.747  TPC[id=1,type=JVB] Suspending media transfer.
16:12:01.498  Detected wedged remote audio source ebece0cc-a0 ... Triggering recovery.   <- 1st of 23
16:27:11.707  Detected wedged remote audio source ebece0cc-a0 ... Triggering recovery.   <- 23rd
16:27:13.789  Resuming media transfer over the JVB connection... / Peer to peer connection closed!

Each firing ran a source-remove plus source-add — two offer/answer cycles — on the suspended peerconnection, with the freshly added track discarded by the conference every time (Trying to add remote JVB track, when in P2P - IGNORED).

Changes

Three separate commits:

  1. Skip detection while media transfer is suspended. Bail out of _evaluate when the peerconnection's audioTransferActive is false, clearing the streaks so the detection window restarts on resume instead of counting the suspended period against the sources. This is the fix for the report above.

  2. Keep a proven receive m-line exempt for the peerconnection's lifetime. A source confirmed healthy by receiving RTP was re-armed as soon as its SSRC stopped being mapped, so that a future source reusing the SSRC would be re-evaluated from scratch. That is backwards: addRemoteSsrc() reports an SSRC as new only once, so processSourceMap creates the receive m-line once per SSRC and every later appearance of it in a source map is an in-place remap — same m-line, transceiver and track, metadata only, no SDP change. A remap inherits an m-line that already demuxes that SSRC correctly and cannot be wedged, whichever source now occupies it.

  3. Cap recoveries at 2 per receive slot. A bridge that discards silence forwards nothing at all for a sender that is digitally silent from the start of its stream: AudioLevelReader lets the first few packets of a stream through (forwarded-silence-packets-limit, default 3) and discards every silence packet after that, and AudioSsrcCache allocates the send SSRC — and sends the AudioSourcesMap that makes the client create the receive m-line — on a packet that got through. If that short head-of-stream burst is spent before the m-line is negotiated, the source is mapped and unmuted yet permanently at zero RTP without being wedged, and recycling cannot change that. The attempts are counted per rewritten SSRC: the m-line cannot carry the count because a recovery replaces it (source-remove then source-add mints a fresh msid), so a per-m-line budget would reset on every attempt and never converge; the source name cannot either, since it would hand a fresh budget to whichever source the bridge next remaps onto a slot recycling has already failed to revive.

Testing

npm run lint and the karma suite are green at each commit, not just at the tip (559 / 560 / 563 tests). Six new RemoteAudioWedgeDetector specs cover: no firing while suspended, the window restarting on resume, a healthy source staying exempt across unmap/remap, recoveries stopping at the cap, a fresh budget on a new receive slot, and an exhausted budget staying with a slot that is remapped to another source.

🤖 Generated with Claude Code

JonathanLennox and others added 3 commits August 13, 2026 13:43
A 2-participant call keeps its JVB session alive as a fallback and merely
suspends it for the duration of the P2P session, which leaves every eligibility
condition the watchdog tests satisfied while no RTP can arrive:

- _removeRemoteJVBTracks() only detaches the tracks from the JitsiParticipant
  and emits TRACK_REMOVED, so TraceablePeerConnection.remoteTracks - what the
  detector iterates - still holds them for the rest of the call.
- setMediaTransferActive(false) deactivates the sender encodings on both peers,
  so the bridge has nothing to forward and the session reads zero inbound RTP.
- The JVB stats collector keeps polling (stopRemoteStats runs only in
  _stopJvbSession), so INBOUND_AUDIO_STATS keeps arriving with zeros, and the
  detector itself only stops in JingleSessionPC.close().
- Mute state is signaling-derived, so the sources stay unmuted.

A field report shows the result: 23 recoveries on one source over 15 minutes,
one every ~40s, each recycling it via source-remove/source-add on the suspended
peerconnection, ending only when P2P did. Skip evaluation while media transfer
is suspended, clearing the streaks so the window restarts on resume.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A source confirmed healthy by receiving RTP was re-armed as soon as its SSRC
stopped being mapped, on the rationale that a future source reusing the SSRC
should be re-evaluated from scratch. That is backwards. Under SSRC rewriting
addRemoteSsrc() reports an SSRC as new only once for the lifetime of the
peerconnection, so processSourceMap creates the receive m-line once per SSRC and
every later appearance of it in a source map is an in-place remap: same m-line,
transceiver and track, metadata only, no SDP change. A remap therefore inherits
an m-line that is already demuxing that SSRC correctly and cannot be wedged,
whichever source now occupies it.

Keep the confirmed-healthy mark for the lifetime of the peerconnection, so a
remap onto a working m-line is never recycled just because its new owner happens
to be silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zero inbound RTP is not exclusive to the wedge, so a recovery that does not help
must not repeat indefinitely. A bridge that discards silence forwards nothing at
all for a sender that is digitally silent from the start of its stream: JVB's
AudioLevelReader lets the first few packets of a stream through
(forwarded-silence-packets-limit, default 3) and discards every silence packet
after that, and the audio SSRC cache allocates the send SSRC - and sends the
AudioSourcesMap that makes the client create the receive m-line - on a packet
that got through. If that short head-of-stream burst is spent before the m-line
is negotiated, the source is mapped and unmuted yet permanently at zero RTP
without being wedged, and recycling it cannot change that.

Cap recoveries at 2, so the unrecoverable case costs a couple of renegotiations
rather than one every detection window for the rest of the call.

Count the attempts per receive slot, keyed on the rewritten SSRC. The m-line
cannot carry the count because a recovery replaces it - source-remove then
source-add mints a fresh msid - so a per-m-line budget would reset on every
attempt and never converge. The source name cannot either: it would hand a fresh
budget to whichever source the bridge next remaps onto a slot that recycling has
already failed to revive. The SSRC is the identity a recovery preserves, and a
genuinely new slot for a source is a genuinely new wedge opportunity, so it
still gets a budget of its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@jallamsetty1 jallamsetty1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

jitsi-ci Bot pushed a commit to jitsi/jitsi-pr-tests-pages that referenced this pull request Aug 13, 2026
@JonathanLennox
JonathanLennox merged commit 7142f3e into jitsi:master Aug 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants