From c695133ca230e1c37507f601fa6ec7e830fa4cad Mon Sep 17 00:00:00 2001 From: Gautam S Date: Tue, 29 Sep 2026 11:38:23 +0530 Subject: [PATCH 1/2] Fix the backup codec publish on native platforms createSimulcastTransceiverSender passed the TrackType enum's toString, 'tracktype.video', as the media kind for the capability lookup. The desktop plugin refuses it and the darwin plugin reads it as audio. The transceiver is always video, so pass 'video', the same as the primary publish does. The transceiver was also added to the publisher peer connection before the server was told about it, and nothing removed it when the publish failed afterwards, so the next offer carried a media section the server could not match and it bound the track's frames to whichever video track was pending. Remove the sender and drop the simulcast entry when anything fails before the add track request succeeds. --- lib/src/core/engine.dart | 20 +++++++- lib/src/participant/local.dart | 90 ++++++++++++++++++++-------------- 2 files changed, 73 insertions(+), 37 deletions(-) diff --git a/lib/src/core/engine.dart b/lib/src/core/engine.dart index ba82dfefb..200268081 100644 --- a/lib/src/core/engine.dart +++ b/lib/src/core/engine.dart @@ -1706,7 +1706,25 @@ extension EngineInternalMethods on Engine { kind: rtc.RTCRtpMediaType.RTCRtpMediaTypeVideo, init: transceiverInit, ); - await setPreferredCodec(transceiver, track.kind.toString().toLowerCase(), videoCodec); + try { + // The transceiver is always video (the kind above), and the plugin's + // capability lookup wants the plain media kind. `track.kind.toString()` + // yields 'tracktype.video', which the native desktop plugins refuse + // ("kind is null or empty") and the darwin plugin reads as audio. + await setPreferredCodec(transceiver, 'video', videoCodec); + } catch (_) { + // The transceiver is already on the peer connection and the server has + // not been told about it. Left behind, the next offer carries a media + // section the server has no signalled track for, and the server binds + // it to whichever video track is pending, so this track's frames can + // surface under another publication. Take it back out before failing. + try { + await publisher!.pc.removeTrack(transceiver.sender); + } catch (e) { + logger.warning('failed to remove the backup codec sender: $e'); + } + rethrow; + } return transceiver.sender; } diff --git a/lib/src/participant/local.dart b/lib/src/participant/local.dart index 61cd38434..86e085a20 100644 --- a/lib/src/participant/local.dart +++ b/lib/src/participant/local.dart @@ -994,47 +994,65 @@ class LocalParticipant extends Participant { final layers = Utils.computeVideoLayers(dimensions, encodings, isSVCCodec(backupCodec)); - simulcastTrack.sender = await room.engine.createSimulcastTransceiverSender( - track, - simulcastTrack, - encodings, - publication, - backupCodec, - ); + late final lk_models.TrackInfo trackInfo; + try { + simulcastTrack.sender = await room.engine.createSimulcastTransceiverSender( + track, + simulcastTrack, + encodings, + publication, + backupCodec, + ); - // the backup codec publishes over its own sender, so it needs the same - // degradation preference the primary sender resolved to. - await track.applyDegradationPreference(simulcastTrack.sender); + // the backup codec publishes over its own sender, so it needs the same + // degradation preference the primary sender resolved to. + await track.applyDegradationPreference(simulcastTrack.sender); - final cid = simulcastTrack.sender!.senderId; + final cid = simulcastTrack.sender!.senderId; - final req = lk_rtc.AddTrackRequest( - cid: cid, - name: - options.name ?? - (track.source == TrackSource.screenShareVideo - ? VideoPublishOptions.defaultScreenShareName - : VideoPublishOptions.defaultCameraName), - type: track.kind.toPBType(), - source: track.source.toPBType(), - muted: track.muted, - layers: layers, - sid: publication.sid, - simulcastCodecs: [ - lk_rtc.SimulcastCodec( - codec: backupCodec.toLowerCase(), - cid: cid, - ), - ], - ); + final req = lk_rtc.AddTrackRequest( + cid: cid, + name: + options.name ?? + (track.source == TrackSource.screenShareVideo + ? VideoPublishOptions.defaultScreenShareName + : VideoPublishOptions.defaultCameraName), + type: track.kind.toPBType(), + source: track.source.toPBType(), + muted: track.muted, + layers: layers, + sid: publication.sid, + simulcastCodecs: [ + lk_rtc.SimulcastCodec( + codec: backupCodec.toLowerCase(), + cid: cid, + ), + ], + ); - // video specific - if (dimensions.width > 0 && dimensions.height > 0) { - req.width = dimensions.width; - req.height = dimensions.height; - } + // video specific + if (dimensions.width > 0 && dimensions.height > 0) { + req.width = dimensions.width; + req.height = dimensions.height; + } - final trackInfo = await room.engine.addTrack(req); + trackInfo = await room.engine.addTrack(req); + } catch (_) { + // The server never learned about this sender. Drop the local record and + // the sender itself, so the peer connection carries no transceiver the + // server cannot match to a signalled track (see + // Engine.createSimulcastTransceiverSender for what happens if it does). + track.simulcastCodecs.remove(backupCodec); + final sender = simulcastTrack.sender; + if (sender != null) { + try { + await room.engine.publisher?.pc.removeTrack(sender); + } catch (e) { + logger.warning('failed to remove the backup codec sender: $e'); + } + } + rethrow; + } await room.engine.negotiate(); From 446b362cfc0466dd1407a02346699b04258b14f0 Mon Sep 17 00:00:00 2001 From: Gautam S Date: Tue, 29 Sep 2026 11:43:36 +0530 Subject: [PATCH 2/2] Remove only this attempt's backup codec entry on failure A republish that raced the failure owns the codec key by then, so a stale attempt must not erase the newer entry (Devin's review). --- lib/src/participant/local.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/participant/local.dart b/lib/src/participant/local.dart index 86e085a20..a32693beb 100644 --- a/lib/src/participant/local.dart +++ b/lib/src/participant/local.dart @@ -1042,7 +1042,11 @@ class LocalParticipant extends Participant { // the sender itself, so the peer connection carries no transceiver the // server cannot match to a signalled track (see // Engine.createSimulcastTransceiverSender for what happens if it does). - track.simulcastCodecs.remove(backupCodec); + // The map is keyed by codec alone, so only this attempt's own entry goes: + // a republish that raced this failure owns the key by now. + if (identical(track.simulcastCodecs[backupCodec], simulcastTrack)) { + track.simulcastCodecs.remove(backupCodec); + } final sender = simulcastTrack.sender; if (sender != null) { try {