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..a32693beb 100644 --- a/lib/src/participant/local.dart +++ b/lib/src/participant/local.dart @@ -994,47 +994,69 @@ 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). + // 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 { + await room.engine.publisher?.pc.removeTrack(sender); + } catch (e) { + logger.warning('failed to remove the backup codec sender: $e'); + } + } + rethrow; + } await room.engine.negotiate();