Skip to content

fix: skip duplicate backup codec publish instead of throwing - #1010

Open
davibittencourtome wants to merge 1 commit into
livekit:mainfrom
davibittencourtome:fix/duplicate-backup-codec-publish-crash
Open

fix: skip duplicate backup codec publish instead of throwing#1010
davibittencourtome wants to merge 1 commit into
livekit:mainfrom
davibittencourtome:fix/duplicate-backup-codec-publish-crash

Conversation

@davibittencourtome

Copy link
Copy Markdown

Fixes #1000

Problem

When the server sends a SubscribedQualityUpdate requesting a backup codec that is already added (or whose publication is still in flight), the SDK crashes the app with:

java.lang.IllegalStateException: VP8 already added!
    at io.livekit.android.room.track.LocalVideoTrack.addSimulcastTrack(LocalVideoTrack.kt)
    at io.livekit.android.room.participant.LocalParticipant.handleSubscribedQualityUpdate(LocalParticipant.kt)
    at io.livekit.android.room.RTCEngine.onSubscribedQualityUpdate(RTCEngine.kt)
    at io.livekit.android.room.SignalClient.handleSignalResponseImpl(SignalClient.kt)

The exception is thrown on the SDK's internal coroutine dispatcher (DefaultDispatcher-worker-N), so there is no way for the application to catch it — it takes down the whole process during a live call.

We hit this in production (self-hosted SFU, publisher on VP9 with the default VP8 backup, PREFER_REGRESSION). Repeated identical updates are expected server behavior by design: MediaTrack.Restart() (ICE restart / session resume) and MediaTrack.SetMuted(false) (unmuting / enabling the camera) both re-emit the current state — dynacastManager.ForceUpdate() explicitly skips the "changed" check. A client therefore has to handle a repeated SubscribedQualityUpdate idempotently.

Root cause

There is a window between the two updates in which the codec entry exists but its sender is not yet attached:

  1. First update arrives → setPublishingCodecs finds no entry for VP8 → returns it as a new codec → publishAdditionalCodecForTrack calls addSimulcastTrack, which inserts the entry, and then launches a coroutine to create the transceiver. simulcastTrackInfo.sender is only assigned inside that coroutine, after the signaling round-trip.
  2. Second (identical) update arrives before the sender is attached → setPublishingCodecs sees simulcastInfo?.sender == null and treats the codec as new again → addSimulcastTrack finds the key already present and throws.

The check at setPublishingCodecs conflates "never started" with "started, still in flight".

Fix

Make addSimulcastTrack idempotent: if the codec is already present, log a warning and return null instead of throwing; publishAdditionalCodecForTrack treats null as a no-op and returns, so no duplicate transceiver is created and no duplicate AddTrackRequest is sent.

This mirrors the JS SDK, where LocalVideoTrack.addSimulcastTrack logs and returns undefined for an already-added codec and the caller bails out.

Note the dedup is intentionally per track instance (the simulcastCodecs map lives on the LocalVideoTrack): a track republished after a reconnect is a new track/sid and must still go through backup-codec publication normally. Session-level dedup would break that case.

Testing

  • addSimulcastTrackForAlreadyAddedCodecIsNoOp — calling addSimulcastTrack twice for the same codec no longer throws; the second call returns null.
  • duplicateSubscribedQualityUpdateDoesNotRepublishBackupCodec — receiving the same SubscribedQualityUpdate twice keeps a single backup transceiver (2 transceivers total).
  • Full LocalParticipantMockE2ETest suite passes.

A duplicate SubscribedQualityUpdate arriving while the backup codec
publication is still in flight (sender not yet attached) re-enters
addSimulcastTrack and crashed the app with
IllegalStateException("VP8 already added!").

Make addSimulcastTrack idempotent: log and return null for an
already-added codec, and bail out of publishAdditionalCodecForTrack so
no duplicate transceiver or AddTrackRequest is created. Mirrors the JS
SDK behavior. Dedup is per track instance, so a track republished after
a reconnect still publishes its backup codec normally.

Fixes livekit#1000
@changeset-bot

changeset-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 39f3ce4

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

@CLAassistant

CLAassistant commented Aug 25, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@devin-ai-integration devin-ai-integration Bot 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

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.

Exception & crash: VP8 already added

2 participants