11import 'dart:async' ;
22import 'dart:io' ;
3+ import 'dart:math' ;
34
45import 'package:flutter/widgets.dart' hide Notification;
56
@@ -153,6 +154,7 @@ class CallBloc extends Bloc<CallEvent, CallState> with WidgetsBindingObserver im
153154 on < _PeerConnectionEvent > (_onPeerConnectionEvent, transformer: sequential ());
154155 on < CallScreenEvent > (_onCallScreenEvent, transformer: sequential ());
155156 on < CallConfigEvent > (_onConfigEvent, transformer: sequential ());
157+ on < _CallActionRenegotiate > (_onCallActionRenegotiate, transformer: droppable ());
156158
157159 navigator.mediaDevices.ondevicechange = (event) {
158160 add (const _NavigatorMediaDevicesChange ());
@@ -1119,6 +1121,8 @@ class CallBloc extends Bloc<CallEvent, CallState> with WidgetsBindingObserver im
11191121 proximityEnabled: state.shouldListenToProximity,
11201122 );
11211123
1124+ final releaseLock = await _peerConnectionManager.acquireModificationLock (activeCall.callId);
1125+
11221126 try {
11231127 final jsep = event.jsep;
11241128 if (jsep != null ) {
@@ -1141,6 +1145,7 @@ class CallBloc extends Bloc<CallEvent, CallState> with WidgetsBindingObserver im
11411145 '__onCallSignalingEventUpdating: glare detected via pre-check (signalingState=$signalingState ), rolling back local offer' ,
11421146 );
11431147 await peerConnection.setLocalDescription (RTCSessionDescription ('' , 'rollback' ));
1148+ _dispatchRenegotiation (event.callId);
11441149 }
11451150
11461151 try {
@@ -1154,6 +1159,7 @@ class CallBloc extends Bloc<CallEvent, CallState> with WidgetsBindingObserver im
11541159 '__onCallSignalingEventUpdating: glare detected via catch ($e ), rolling back local offer and retrying' ,
11551160 );
11561161 await peerConnection.setLocalDescription (RTCSessionDescription ('' , 'rollback' ));
1162+ _dispatchRenegotiation (event.callId);
11571163 await peerConnection.setRemoteDescription (remoteDescription);
11581164 } else {
11591165 rethrow ;
@@ -1164,7 +1170,9 @@ class CallBloc extends Bloc<CallEvent, CallState> with WidgetsBindingObserver im
11641170
11651171 // According to RFC 8829 5.6 (https://datatracker.ietf.org/doc/html/rfc8829#section-5.6),
11661172 // localDescription should be set before sending the answer to transition into stable state.
1167- await peerConnection.setLocalDescription (localDescription);
1173+ await peerConnection
1174+ .setLocalDescription (localDescription)
1175+ .catchError ((error) => throw RtcJsepErrorParser .parse (error));
11681176
11691177 await _signalingClient? .execute (
11701178 UpdateRequest (
@@ -1178,10 +1186,13 @@ class CallBloc extends Bloc<CallEvent, CallState> with WidgetsBindingObserver im
11781186 });
11791187 }
11801188 } catch (e, s) {
1189+ _logger.warning ('__onCallSignalingEventUpdating - error:' , e, s);
11811190 callErrorReporter.handle (e, s, '__onCallSignalingEventUpdating && jsep error:' );
11821191
11831192 _peerConnectionManager.completeError (event.callId, e);
11841193 add (_ResetStateEvent .completeCall (event.callId));
1194+ } finally {
1195+ releaseLock ();
11851196 }
11861197 }
11871198
@@ -1482,6 +1493,8 @@ class CallBloc extends Bloc<CallEvent, CallState> with WidgetsBindingObserver im
14821493 if (currentVideoTrack != null ) {
14831494 currentVideoTrack.enabled = event.enabled;
14841495 emit (state.copyWithMappedActiveCall (event.callId, (call) => call.copyWith (video: event.enabled)));
1496+ // Helps recover from video-stuck states when user taps to toggle video
1497+ _dispatchRenegotiation (event.callId);
14851498 return ;
14861499 }
14871500
@@ -2003,7 +2016,9 @@ class CallBloc extends Bloc<CallEvent, CallState> with WidgetsBindingObserver im
20032016
20042017 // According to RFC 8829 5.6 (https://datatracker.ietf.org/doc/html/rfc8829#section-5.6),
20052018 // localDescription should be set before sending the answer to transition into stable state.
2006- await peerConnection.setLocalDescription (localDescription).catchError ((e) => throw SDPConfigurationError (e));
2019+ await peerConnection
2020+ .setLocalDescription (localDescription)
2021+ .catchError ((error) => throw RtcJsepErrorParser .parse (error));
20072022
20082023 await _signalingClient? .execute (
20092024 AcceptRequest (
@@ -2906,6 +2921,78 @@ class CallBloc extends Bloc<CallEvent, CallState> with WidgetsBindingObserver im
29062921 await _signalingClient? .execute (updateRequest);
29072922 }
29082923
2924+ /// Proactive renegotiation handler for recovery scenarios (glare rollback, video toggle, etc.).
2925+ ///
2926+ /// Uses [acquireModificationLock] to serialize with [__onCallSignalingEventUpdating] ,
2927+ /// and [droppable] transformer to discard redundant retries queued during an active cycle.
2928+ Future <void > _onCallActionRenegotiate (_CallActionRenegotiate event, Emitter <CallState > emit) async {
2929+ final callId = event.callId;
2930+ final activeCall = state.activeCalls.firstWhereOrNull ((call) => call.callId == callId);
2931+ if (activeCall == null ) return ;
2932+
2933+ final lineId = activeCall.line;
2934+
2935+ final peerConnection = await _peerConnectionManager.retrieve (callId);
2936+ if (peerConnection == null ) return ;
2937+
2938+ final releaseLock = await _peerConnectionManager.acquireModificationLock (callId);
2939+
2940+ final pcSignalingState = await peerConnection.getSignalingState ();
2941+ _logger.warning (() => '_onCallActionRenegotiate signalingState: $pcSignalingState ' );
2942+ final connectionState = await peerConnection.getConnectionState ();
2943+ _logger.warning (() => '_onCallActionRenegotiate connectionState: $connectionState ' );
2944+
2945+ if (connectionState == RTCPeerConnectionState .RTCPeerConnectionStateNew ) {
2946+ _logger.warning ('_onCallActionRenegotiate skipped due to new connection state' );
2947+ releaseLock ();
2948+ return ;
2949+ }
2950+
2951+ if (pcSignalingState != RTCSignalingState .RTCSignalingStateStable ) {
2952+ _logger.warning ('_onCallActionRenegotiate skipped due to non-stable signaling state' );
2953+ releaseLock ();
2954+ return ;
2955+ }
2956+
2957+ try {
2958+ final localDescription = await peerConnection.createOffer ({});
2959+ sdpMunger? .apply (localDescription);
2960+ // According to RFC 8829 5.6 (https://datatracker.ietf.org/doc/html/rfc8829#section-5.6),
2961+ // localDescription should be set before sending the offer to transition into have-local-offer state.
2962+ await peerConnection
2963+ .setLocalDescription (localDescription)
2964+ .catchError ((error) => throw RtcJsepErrorParser .parse (error));
2965+
2966+ final updateRequest = UpdateRequest (
2967+ transaction: WebtritSignalingClient .generateTransactionId (),
2968+ line: lineId,
2969+ callId: callId,
2970+ jsep: localDescription.toMap (),
2971+ );
2972+ await _signalingClient? .execute (updateRequest);
2973+ } catch (e, s) {
2974+ _logger.warning ('_onCallActionRenegotiate error' , e, s);
2975+ callErrorReporter.handle (e, s, '_onCallActionRenegotiate error:' );
2976+ } finally {
2977+ releaseLock ();
2978+ }
2979+ }
2980+
2981+ /// Schedules a renegotiation with a random delay to reduce glare probability.
2982+ ///
2983+ /// Inspired by the 491 Request Pending handling described in RFC 5407.
2984+ void _dispatchRenegotiation (String callId, {Duration ? delayOverride}) async {
2985+ final randomDelay = delayOverride ?? Duration (milliseconds: Random ().nextInt (6000 ).clamp (1000 , 5000 ));
2986+ _logger.warning (() => '_dispatchRenegotiation for callId: $callId with random delay: $randomDelay ' );
2987+ Future .delayed (randomDelay).then ((_) {
2988+ if (isClosed) return ;
2989+ final activeCall = state.activeCalls.firstWhereOrNull ((call) => call.callId == callId);
2990+ if (activeCall == null ) return ;
2991+
2992+ add (_CallActionRenegotiate (callId));
2993+ });
2994+ }
2995+
29092996 void _addToRecents (ActiveCall activeCall) {
29102997 final number = activeCall.handle.value;
29112998 final username = activeCall.displayName;
0 commit comments