Repository navigation
Conversation
🦋 Changeset detectedLatest commit: 1bc82e7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
ydiarra
marked this pull request as ready for review
October 20, 2026 08:50
ydiarra
requested review from
1egoman,
lukasIO and
xianshijing-lk
as code owners
October 20, 2026 08:50
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 theLocalTrack(_isUpstreamPaused), and the null track is set on theRTCRtpSenderonly.After a full reconnect,
republishAllTracks()unpublishes every track and publishes it again withpublishOrRepublishTrack(). The track keepsisUpstreamPaused === true, but:MediaStreamTrack, so media flows to the SFU again;AddTrackRequestis sent withmuted: track.isMuted, so the server sees the track as unmuted;pauseUpstream()again does nothing, because of theif (this._isUpstreamPaused === true) returnguard.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 withresumeUpstream()followed bypauseUpstream().The same thing happens to a backup codec published while the track is paused:
createSimulcastSender()attaches the clone even thoughpauseUpstream()had nulled the other senders.Repro
track.pauseUpstream().room.simulateScenario('full-reconnect').track.isUpstreamPausedis stilltrue.Fix
RTCEngine.createSender()andcreateSimulcastSender()callreplaceTrack(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 putsmediaStreamTrackback ontrack.senderand the backup codec senders.AddTrackRequestannouncesmuted: track.isMuted || track.isUpstreamPaused, which is whatonTrackUpstreamPausedandupdateInfo()already send. The backup codec request frompublishAdditionalCodecForTrack()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 getsreplaceTrack(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 hasmuted: true, and so does the request for its backup codec.pnpm test,tsc --noEmitand 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 onLocalTrackPublishedwithresumeUpstream()and thenpauseUpstream()(workadventure/workadventure#6800). We'll remove that once this is released.