Skip to content

Fix transceiver reusing problem in single-pc mode - #1151

Merged
cnderrauber merged 4 commits into
mainfrom
fix/blink-style-track-event-diff
Jun 11, 2026
Merged

cnderrauber merged 4 commits into
mainfrom
fix/blink-style-track-event-diff

Conversation

@cnderrauber

Copy link
Copy Markdown
Contributor

Sfu could send empty transceiver to subscriber in single pc
mode when publisher unbpulishes track before answer sdp is
created. The empty transceiver can be reused in later subscription
that changes msid in renegotiation, libwebrtc will not fire
onTrack event in the msid changed case cause subscrption failure.
Chrome detects the msid change and synthsize onTrack event to handle
this case. This PR do the same thing as browser.

Sfu could send empty transceiver to subscriber in single pc
mode when publisher unbpulishes track before answer sdp is
created. The empty transceiver can be reused in later subscription
that changes msid in renegotiation, libwebrtc will not fire
onTrack event in the msid changed case cause subscrption failure.
Chrome detects the msid change and synthsize onTrack event to handle
this case. This PR do the same thing as browser.
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Changeset

The following package versions will be affected by this PR:

Package Bump
libwebrtc patch
livekit patch
webrtc-sys patch

cnderrauber and others added 2 commits June 10, 2026 10:23
libwebrtc is typically compiled with `-fno-exceptions`. In the C++ shim we
were calling `transceiver_->mid().value()` on an `std::optional<std::string>`
— which on an empty optional invokes `std::__throw_bad_optional_access()`,
which without exception support `abort()`s the process. The Rust binding
expected this call to return `Err` on no-mid, but the abort happens before
cxx-bridge can translate the throw to an `Err`.

This was exposed by the new `process_remote_track_addition` pass walking
all transceivers after every `setRemoteDescription`: a transceiver freshly
created by a local `AddTrack` is in `pc.transceivers()` but doesn't have
a mid assigned yet until the negotiation completes. The diff iterated to
it, called `mid()`, and the process SIGABRTed.

Fix:
- C++ shim: use `.value_or("")` instead of `.value()`, returning an empty
  string when no mid is set.
- Rust binding: treat empty string as `None` so the public API contract
  (`mid() -> Option<String>`) is unchanged.

Reproduced deterministically on x86_64 Linux with
`test_v1_publish_ten_video_and_ten_audio_tracks` against a single-PC SFU.
Without this fix the test SIGABRTs on the second negotiation round after
the first track publish. With this fix the test passes consistently.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@xianshijing-lk xianshijing-lk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

one question

}

let mut dispatched = self.dispatched_streams.lock();
dispatched.retain(|key| current.contains_key(key));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

any chance that (mid, stream_id) disappears for one SDP update but later reappears again ?

if this will happen, the current code will dispatch MediaTrack again. Is this intentional behavior ?

@cnderrauber cnderrauber Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is intentional, in case like:

  1. client subscribe, sfu sdp: mid:1, streamid_id: stream_A
  2. client unsubscribe, sfu sdp: mid:1, no stream
  3. client resubscribe after awhile, sfu reuse mid:1 to send track and client dispatch the track again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

just want to double check, at any condition should we clear this dispatched_streams so that we will dipspatch the tracks again ?

dispatched.retain(|key| current.contains_key(key)); here will also remove all keys don't exist in current streams, so we can dispatch it again when the track has been resubscribed.

let stream = streams.remove(0);
let mid = transceiver.mid().unwrap_or_default();
let already_dispatched = !mid.is_empty()
&& !self.dispatched_streams.lock().insert((mid, stream.id()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

just want to double check, at any condition should we clear this dispatched_streams so that we will dipspatch the tracks again ?

@cnderrauber
cnderrauber merged commit 034ca3f into main Jun 11, 2026
22 checks passed
@cnderrauber
cnderrauber deleted the fix/blink-style-track-event-diff branch June 11, 2026 01:51
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