@@ -811,6 +811,8 @@ extension PublisherRtcManager on RtcManager {
811811 tracks[audioTrack.trackId] = audioTrack;
812812 var updatedTrack = audioTrack.copyWith (stopTrackOnMute: stopTrackOnMute);
813813
814+ var needsRenegotiation = false ;
815+
814816 for (final option in publishOptions) {
815817 if (option.trackType != audioTrack.trackType) continue ;
816818
@@ -819,7 +821,8 @@ extension PublisherRtcManager on RtcManager {
819821 final mediaTrackClone = await audioTrack.mediaTrack.clone ();
820822 final trackToPublish = audioTrack.copyWith (mediaTrack: mediaTrackClone);
821823
822- final cachedTransceiver = transceiversManager.get (option)? .transceiver;
824+ final cachedBundle = transceiversManager.get (option);
825+ final cachedTransceiver = cachedBundle? .transceiver;
823826 if (cachedTransceiver == null ) {
824827 final transceiverResult = await _addTransceiver (
825828 trackToPublish,
@@ -854,6 +857,13 @@ extension PublisherRtcManager on RtcManager {
854857 trackPublishOptions: publishOptions,
855858 );
856859
860+ // Reusing a transceiver via replaceTrack does not fire
861+ // onRenegotiationNeeded. If its previous negotiation never reached the
862+ // SFU, the SFU still doesn't know about it, so force a renegotiation.
863+ if (cachedBundle != null && ! cachedBundle.negotiated) {
864+ needsRenegotiation = true ;
865+ }
866+
857867 _logger.v (
858868 () => '[publishAudioTrack] cached transceiver: $cachedTransceiver ' ,
859869 );
@@ -864,6 +874,10 @@ extension PublisherRtcManager on RtcManager {
864874 );
865875 }
866876
877+ if (needsRenegotiation) {
878+ _forceRenegotiation ('[publishAudioTrack]' );
879+ }
880+
867881 // Notify listeners.
868882 onLocalTrackPublished? .call (updatedTrack);
869883 tracks[updatedTrack.trackId] = updatedTrack;
@@ -901,6 +915,8 @@ extension PublisherRtcManager on RtcManager {
901915 );
902916 }
903917
918+ var needsRenegotiation = false ;
919+
904920 for (final option in publishOptions) {
905921 if (option.trackType != videoTrack.trackType) continue ;
906922
@@ -909,7 +925,8 @@ extension PublisherRtcManager on RtcManager {
909925 final mediaTrackClone = await videoTrack.mediaTrack.clone ();
910926 final trackToPublish = videoTrack.copyWith (mediaTrack: mediaTrackClone);
911927
912- final cachedTransceiver = transceiversManager.get (option)? .transceiver;
928+ final cachedBundle = transceiversManager.get (option);
929+ final cachedTransceiver = cachedBundle? .transceiver;
913930 if (cachedTransceiver == null ) {
914931 final transceiverResult = await _addTransceiver (
915932 trackToPublish,
@@ -936,6 +953,13 @@ extension PublisherRtcManager on RtcManager {
936953
937954 transceiversManager.update (option, track: trackToPublish);
938955
956+ // Reusing a transceiver via replaceTrack does not fire
957+ // onRenegotiationNeeded. If its previous negotiation never reached the
958+ // SFU, the SFU still doesn't know about it, so force a renegotiation.
959+ if (cachedBundle != null && ! cachedBundle.negotiated) {
960+ needsRenegotiation = true ;
961+ }
962+
939963 _logger.v (
940964 () => '[publishVideoTrack] cached transceiver: $cachedTransceiver ' ,
941965 );
@@ -946,6 +970,10 @@ extension PublisherRtcManager on RtcManager {
946970 );
947971 }
948972
973+ if (needsRenegotiation) {
974+ _forceRenegotiation ('[publishVideoTrack]' );
975+ }
976+
949977 // Notify listeners.
950978 onLocalTrackPublished? .call (updatedTrack);
951979 tracks[updatedTrack.trackId] = updatedTrack;
@@ -1002,6 +1030,39 @@ extension PublisherRtcManager on RtcManager {
10021030 ];
10031031 }
10041032
1033+ /// Explicitly triggers a publisher renegotiation.
1034+ void _forceRenegotiation (String tag) {
1035+ final pub = publisher;
1036+ if (pub == null ) return ;
1037+
1038+ if (pub.isReconnecting) {
1039+ _logger.v (
1040+ () => '$tag skipping forced renegotiation — reconnect in progress' ,
1041+ );
1042+ return ;
1043+ }
1044+
1045+ _logger.v (
1046+ () =>
1047+ '$tag forcing renegotiation for a reused transceiver the SFU never '
1048+ 'acknowledged' ,
1049+ );
1050+
1051+ pub.onRenegotiationNeeded? .call (pub);
1052+ }
1053+
1054+ /// Forces a publisher renegotiation if any transceiver sending [trackId]
1055+ /// was never acknowledged by the SFU.
1056+ void _renegotiateIfUnacknowledged (String trackId, String tag) {
1057+ final hasUnacknowledged = transceiversManager
1058+ .findAll ((t) => t.track.trackId == trackId)
1059+ .any ((t) => t.transceiver.sender.track != null && ! t.negotiated);
1060+
1061+ if (hasUnacknowledged) {
1062+ _forceRenegotiation (tag);
1063+ }
1064+ }
1065+
10051066 Future <Result <rtc.RTCRtpTransceiver >> _addTransceiver (
10061067 RtcLocalTrack track,
10071068 SfuPublishOptions publishOptions,
@@ -1181,13 +1242,15 @@ extension PublisherRtcManager on RtcManager {
11811242 tracks[trackId] = updatedTrack;
11821243 onLocalTrackMuted? .call (updatedTrack, false );
11831244
1245+ _renegotiateIfUnacknowledged (trackId, '[unmuteTrack]' );
11841246 return Result .success (updatedTrack);
11851247 }
11861248
11871249 // Otherwise simply enable it again
11881250 track.enable ();
11891251 onLocalTrackMuted? .call (track, false );
11901252
1253+ _renegotiateIfUnacknowledged (trackId, '[unmuteTrack]' );
11911254 return Result .success (track);
11921255 }
11931256
0 commit comments