Skip to content

Fix race condition in PeerConnectionTransport.trackBitrates#821

Open
adrian-niculescu wants to merge 1 commit into
livekit:mainfrom
adrian-niculescu:fix/track-bitrates-race-condition
Open

Fix race condition in PeerConnectionTransport.trackBitrates#821
adrian-niculescu wants to merge 1 commit into
livekit:mainfrom
adrian-niculescu:fix/track-bitrates-race-condition

Conversation

@adrian-niculescu
Copy link
Copy Markdown
Contributor

@adrian-niculescu adrian-niculescu commented Nov 24, 2025

Problem

Code references below are pinned to main@46da678.

PeerConnectionTransport.trackBitrates is a plain mutableMapOf<...>() (a LinkedHashMap) with no thread-safety. Reads and writes on it race inside a single LocalParticipant.negotiate() call.

engine.createSenderTransceiver(...) causes WebRTC to fire onRenegotiationNeeded. That goes through PublisherTransportObserver.onRenegotiationNeeded() and engine.negotiatePublisher() into the 20ms-debounced negotiate field. When the debounce closes, createAndSendOffer iterates the map on the RTC thread via ensureCodecBitrates(...).

Still inside the same negotiate() body, engine.registerTrackBitrateInfo(...) writes to the map on the calling coroutine's thread. The auto-trigger is intentional and documented in LocalParticipant: "PublisherTransportObserver.onRenegotiationNeeded() gets triggered automatically so no need to call negotiate manually."

If the debounce closes while a put is still in flight, iteration on the RTC thread can throw ConcurrentModificationException from ensureCodecBitrates. Any publish path that hits this createSenderTransceiver then registerTrackBitrateInfo sequence is exposed (SVC video with a maxBitrateBps encoding).

Fix

Route both registerTrackBitrateInfo overloads through executeRTCIfNotClosed { ... } so writes share the single-threaded RTC executor with the read site. Same pattern as updateRTCConfig and addIceCandidate in the same class. The RTC thread already serves as the lock for peer-connection-adjacent state.

The closed-state guard inside executeRTCIfNotClosed also makes post-close calls silently drop, matching the surrounding APIs.

Scope

Independent of #823, which added a Mutex around createAndSendOffer for concurrent offer creation. That PR did not touch registerTrackBitrateInfo, so this race remained on main. No API or behavioral change for callers beyond the write going through the RTC executor.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Nov 24, 2025

🦋 Changeset detected

Latest commit: 7ff2a49

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
client-sdk-android Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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.

1 participant