Skip to content

fix: keep a paused upstream paused when the track is republished - #2162

Open
ydiarra wants to merge 2 commits into
livekit:mainfrom
ydiarra:fix/republish-paused-upstream
Open

ydiarra wants to merge 2 commits into
livekit:mainfrom
ydiarra:fix/republish-paused-upstream

Conversation

@ydiarra

@ydiarra ydiarra commented Oct 20, 2026 •

Copy link
Copy Markdown

Problem

pauseUpstream() stops sending a track without stopping it locally, so the app can keep showing its own preview. The paused state is stored on the LocalTrack (_isUpstreamPaused), and the null track is set on the RTCRtpSender only.

After a full reconnect, republishAllTracks() unpublishes every track and publishes it again with publishOrRepublishTrack(). The track keeps isUpstreamPaused === true, but:

  • the new sender is created with the real MediaStreamTrack, so media flows to the SFU again;
  • the AddTrackRequest is sent with muted: track.isMuted, so the server sees the track as unmuted;
  • calling pauseUpstream() again does nothing, because of the if (this._isUpstreamPaused === true) return guard.

updateInfo() resends the mute state (pub.isMuted || track.isUpstreamPaused), but only for tracks that are in a participant update received after the republish, so the server can stay out of sync. Even when it does fix the mute state, the sender still sends media. The app can only get out of this with resumeUpstream() followed by pauseUpstream().

The same thing happens to a backup codec published while the track is paused: createSimulcastSender() attaches the clone even though pauseUpstream() had nulled the other senders.

Repro

  1. Publish a camera or a microphone, then call track.pauseUpstream().
  2. Run room.simulateScenario('full-reconnect').
  3. Remote participants receive the media again, and the server reports the track as unmuted, while track.isUpstreamPaused is still true.

Fix

  • RTCEngine.createSender() and createSimulcastSender() call replaceTrack(null) on the new sender when the track is paused. This runs before the sender is assigned to the track and before negotiation, so no media is sent. resumeUpstream() works as before: it puts mediaStreamTrack back on track.sender and the backup codec senders.
  • The AddTrackRequest announces muted: track.isMuted || track.isUpstreamPaused, which is what onTrackUpstreamPaused and updateInfo() already send. The backup codec request from publishAdditionalCodecForTrack() does the same.

The change applies to every path that publishes a paused track again: full reconnect, E2EE toggle (setE2EEEnabled → republishAllTracks), or the app publishing a track it unpublished while paused.

Tests

Each new test failed before the fix and passes after it:

  • RTCEngine.test.ts: a paused track gets replaceTrack(null) on its new sender and on a new backup codec sender; a track that is not paused is left on its sender.
  • LocalParticipant.test.ts: the add track request of a paused track has muted: true, and so does the request for its backup codec.

pnpm test, tsc --noEmit and eslint pass. I didn't run the e2e suite against a server.

Context

We hit this in WorkAdventure, which never unpublishes the camera or microphone and uses pauseUpstream() instead. We currently work around it on LocalTrackPublished with resumeUpstream() and then pauseUpstream() (workadventure/workadventure#6800). We'll remove that once this is released.

@changeset-bot

changeset-bot Bot commented Oct 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1bc82e7

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

This PR includes changesets to release 1 package
Name Type
livekit-client 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

devin-ai-integration[bot]

This comment was marked as resolved.

@lukasIO lukasIO self-assigned this Oct 20, 2026

This branch has not been deployed

No deployments
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