From 722fd24cd72aa43d27dc07e3bc328b17d08cb487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 27 Feb 2026 18:13:59 +0100 Subject: [PATCH 1/2] fix(webrtc): reuse inactive publisher transceivers to avoid sender/transceiver churn `RTCEngine.createSender`/`createSimulcastSender` always created new publisher transceivers via addTransceiver(). During repeated unpublish/publish cycles, this could accumulate inactive transceivers and increase memory usage. Add a reuse path that: - looks for an inactive publisher transceiver of matching media kind - switches it back to sendonly - replaces its sender track instead of creating a new transceiver - applies to both primary sender creation and simulcast sender creation This reduces transceiver churn and keeps memory growth bounded in restart-heavy flows (e.g. screenshare stop/start loops). --- src/room/RTCEngine.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/room/RTCEngine.ts b/src/room/RTCEngine.ts index b49c4f0894..e03492164e 100644 --- a/src/room/RTCEngine.ts +++ b/src/room/RTCEngine.ts @@ -910,6 +910,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit track.codec = opts.videoCodec; } + const reusedSender = await this.reuseInactivePublisherSender(track.mediaStreamTrack, streams); + if (reusedSender) { + return reusedSender; + } + const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly', streams }; if (encodings) { transceiverInit.sendEncodings = encodings; @@ -932,6 +937,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit if (!this.pcManager) { throw new UnexpectedConnectionState('publisher is closed'); } + const reusedSender = await this.reuseInactivePublisherSender(simulcastTrack.mediaStreamTrack); + if (reusedSender) { + if (!opts.videoCodec) { + return; + } + track.setSimulcastTrackSender(opts.videoCodec, reusedSender); + return reusedSender; + } + const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' }; if (encodings) { transceiverInit.sendEncodings = encodings; @@ -948,6 +962,34 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit return transceiver.sender; } + private async reuseInactivePublisherSender(track: MediaStreamTrack, streams: MediaStream[] = []) { + if (!this.pcManager) { + return; + } + + const transceiver = this.pcManager.publisher + .getTransceivers() + .find( + (candidate) => + candidate.direction === 'inactive' && + candidate.mid !== null && + candidate.sender.track === null && + candidate.receiver.track?.kind === track.kind && + candidate.sender.transport?.state !== 'closed', + ); + + if (!transceiver) { + return; + } + + transceiver.direction = 'sendonly'; + if ('setStreams' in transceiver.sender && streams.length > 0) { + transceiver.sender.setStreams(...streams); + } + await transceiver.sender.replaceTrack(track); + return transceiver.sender; + } + private async createRTCRtpSender(track: MediaStreamTrack) { if (!this.pcManager) { throw new UnexpectedConnectionState('publisher is closed'); From d3b394d51fed667d371251067d26698ecae56027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Sat, 28 Feb 2026 14:09:18 +0100 Subject: [PATCH 2/2] Adding changeset for reusing inactive publisher transceivers in RTCEngine to reduce memory growth on republish --- .changeset/sweet-planes-open.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sweet-planes-open.md diff --git a/.changeset/sweet-planes-open.md b/.changeset/sweet-planes-open.md new file mode 100644 index 0000000000..115a97a450 --- /dev/null +++ b/.changeset/sweet-planes-open.md @@ -0,0 +1,5 @@ +--- +'livekit-client': patch +--- + +Reuse inactive publisher transceivers in RTCEngine to reduce memory growth on republish