|
|
|
@ -26,8 +26,11 @@ class CallManager extends ChangeNotifier {
|
|
|
|
|
|
|
|
|
|
|
|
// Services - Retrieved from DI
|
|
|
|
// Services - Retrieved from DI
|
|
|
|
SignalRService get _signalRService => getIt<SignalRService>();
|
|
|
|
SignalRService get _signalRService => getIt<SignalRService>();
|
|
|
|
|
|
|
|
WebRTCService get _webrtcService => getIt<WebRTCService>();
|
|
|
|
final CallKitService _callKitService = CallKitService();
|
|
|
|
final CallKitService _callKitService = CallKitService();
|
|
|
|
WebRTCService? _webrtcService;
|
|
|
|
|
|
|
|
|
|
|
|
// Public getter for WebRTC service (used by call pages and ChatProvider)
|
|
|
|
|
|
|
|
WebRTCService get webrtcService => _webrtcService;
|
|
|
|
|
|
|
|
|
|
|
|
// Call state
|
|
|
|
// Call state
|
|
|
|
CallSession? _currentCall;
|
|
|
|
CallSession? _currentCall;
|
|
|
|
@ -61,6 +64,13 @@ class CallManager extends ChangeNotifier {
|
|
|
|
static String? _cachedAuthToken;
|
|
|
|
static String? _cachedAuthToken;
|
|
|
|
static String? _cachedEmployeeNumber;
|
|
|
|
static String? _cachedEmployeeNumber;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Pending call data for background acceptance
|
|
|
|
|
|
|
|
Map<String, dynamic>? _pendingCallData;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL FIX: WebRTC initialization synchronization
|
|
|
|
|
|
|
|
Completer<void>? _webrtcInitCompleter;
|
|
|
|
|
|
|
|
String? _pendingOfferSdp; // Queue for offer that arrives during initialization
|
|
|
|
|
|
|
|
|
|
|
|
// Getters
|
|
|
|
// Getters
|
|
|
|
CallSession? get currentCall => _currentCall;
|
|
|
|
CallSession? get currentCall => _currentCall;
|
|
|
|
CallStatus get callStatus => _callStatus;
|
|
|
|
CallStatus get callStatus => _callStatus;
|
|
|
|
@ -70,7 +80,6 @@ class CallManager extends ChangeNotifier {
|
|
|
|
bool get isCameraOn => _isCameraOn;
|
|
|
|
bool get isCameraOn => _isCameraOn;
|
|
|
|
bool get isPeerMuted => _isPeerMuted;
|
|
|
|
bool get isPeerMuted => _isPeerMuted;
|
|
|
|
bool get isPeerCameraOn => _isPeerCameraOn;
|
|
|
|
bool get isPeerCameraOn => _isPeerCameraOn;
|
|
|
|
WebRTCService? get webrtcService => _webrtcService;
|
|
|
|
|
|
|
|
bool get isCallInProgress => _callStatus != CallStatus.idle;
|
|
|
|
bool get isCallInProgress => _callStatus != CallStatus.idle;
|
|
|
|
|
|
|
|
|
|
|
|
/// Initialize CallManager with user credentials
|
|
|
|
/// Initialize CallManager with user credentials
|
|
|
|
@ -384,6 +393,20 @@ class CallManager extends ChangeNotifier {
|
|
|
|
log(' Caller: $callerName ($callerId)', name: 'CallManager');
|
|
|
|
log(' Caller: $callerName ($callerId)', name: 'CallManager');
|
|
|
|
log(' Video: $isVideoCall', name: 'CallManager');
|
|
|
|
log(' Video: $isVideoCall', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Store pending call data for background acceptance
|
|
|
|
|
|
|
|
_pendingCallData = {
|
|
|
|
|
|
|
|
'callId': callId,
|
|
|
|
|
|
|
|
'callerId': callerId,
|
|
|
|
|
|
|
|
'callerName': callerName,
|
|
|
|
|
|
|
|
'isVideoCall': isVideoCall,
|
|
|
|
|
|
|
|
'conversationId': extraData?['conversationId'],
|
|
|
|
|
|
|
|
'moduleId': extraData?['moduleId'],
|
|
|
|
|
|
|
|
'referenceId': extraData?['referenceId'],
|
|
|
|
|
|
|
|
'callerEmployeeNumber': extraData?['callerEmployeeNumber'],
|
|
|
|
|
|
|
|
'timestamp': DateTime.now().toIso8601String(),
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
log('💾 [INCOMING] Pending call data stored for background acceptance', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL FIX: Auto-initialize CallManager if not already initialized
|
|
|
|
// CRITICAL FIX: Auto-initialize CallManager if not already initialized
|
|
|
|
// This handles the case when app is in background and context is null
|
|
|
|
// This handles the case when app is in background and context is null
|
|
|
|
if (_userId == null || _authToken == null) {
|
|
|
|
if (_userId == null || _authToken == null) {
|
|
|
|
@ -551,7 +574,48 @@ class CallManager extends ChangeNotifier {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
log('═══════════════════════════════════════════', name: 'CallManager');
|
|
|
|
log('═══════════════════════════════════════════', name: 'CallManager');
|
|
|
|
log('✅ [ACCEPT] Accepting call...', name: 'CallManager');
|
|
|
|
log('✅ [ACCEPT] Accepting call...', name: 'CallManager');
|
|
|
|
log('🔍 [ACCEPT] CODE VERSION: 2026-07-19-v3-DEBUG', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Handle background acceptance scenario
|
|
|
|
|
|
|
|
if (_currentCall == null && _pendingCallData != null) {
|
|
|
|
|
|
|
|
log('🔧 [ACCEPT] No current call but pending data exists - restoring from background', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' Pending call ID: ${_pendingCallData!['callId']}', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' Pending caller: ${_pendingCallData!['callerName']}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Restore call session from pending data
|
|
|
|
|
|
|
|
_currentCall = CallSession(
|
|
|
|
|
|
|
|
callId: _pendingCallData!['callId'] as String,
|
|
|
|
|
|
|
|
type: (_pendingCallData!['isVideoCall'] as bool) ? CallType.video : CallType.audio,
|
|
|
|
|
|
|
|
direction: CallDirection.incoming,
|
|
|
|
|
|
|
|
peerId: _pendingCallData!['callerId'] as String,
|
|
|
|
|
|
|
|
peerName: _pendingCallData!['callerName'] as String,
|
|
|
|
|
|
|
|
peerAvatar: null,
|
|
|
|
|
|
|
|
startTime: DateTime.now(),
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_updateCallStatus(CallStatus.incomingRinging);
|
|
|
|
|
|
|
|
log('✅ [ACCEPT] Call session restored from pending data', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure CallManager is initialized with pending call context
|
|
|
|
|
|
|
|
if (_userId == null || _authToken == null) {
|
|
|
|
|
|
|
|
log('⚠️ [ACCEPT] CallManager not initialized, attempting auto-initialization...', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (_cachedUserId != null && _cachedAuthToken != null && _cachedEmployeeNumber != null) {
|
|
|
|
|
|
|
|
await initialize(
|
|
|
|
|
|
|
|
userId: _cachedUserId!,
|
|
|
|
|
|
|
|
authToken: _cachedAuthToken!,
|
|
|
|
|
|
|
|
conversationId: _pendingCallData!['conversationId'] as String?,
|
|
|
|
|
|
|
|
moduleId: _pendingCallData!['moduleId'] as String?,
|
|
|
|
|
|
|
|
referenceId: _pendingCallData!['referenceId'] as String?,
|
|
|
|
|
|
|
|
employeeNumber: _cachedEmployeeNumber,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
log('✅ [ACCEPT] CallManager initialized for background call', name: 'CallManager');
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
log('❌ [ACCEPT] Cannot initialize - no cached credentials', name: 'CallManager');
|
|
|
|
|
|
|
|
_cleanup();
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (_currentCall == null || _callStatus != CallStatus.incomingRinging) {
|
|
|
|
if (_currentCall == null || _callStatus != CallStatus.incomingRinging) {
|
|
|
|
log('⚠️ [ACCEPT] No incoming call to accept', name: 'CallManager');
|
|
|
|
log('⚠️ [ACCEPT] No incoming call to accept', name: 'CallManager');
|
|
|
|
@ -576,19 +640,30 @@ class CallManager extends ChangeNotifier {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
log('✅ [ACCEPT] Permissions granted', name: 'CallManager');
|
|
|
|
log('✅ [ACCEPT] Permissions granted', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure SignalR connected
|
|
|
|
// CRITICAL: Ensure SignalR connected before accepting
|
|
|
|
log('🔌 [ACCEPT] Ensuring SignalR connection...', name: 'CallManager');
|
|
|
|
log('🔌 [ACCEPT] Ensuring SignalR connection...', name: 'CallManager');
|
|
|
|
log(' Current SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
|
|
|
|
log(' Current SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
if (!await _signalRService.ensureConnected()) {
|
|
|
|
if (!await _signalRService.ensureConnected()) {
|
|
|
|
log('❌ [ACCEPT] SignalR connection failed', name: 'CallManager');
|
|
|
|
log('❌ [ACCEPT] SignalR connection failed', name: 'CallManager');
|
|
|
|
log(' Final state: ${_signalRService.connectionState}', name: 'CallManager');
|
|
|
|
log(' Final state: ${_signalRService.connectionState}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final context = navigatorKey.currentContext;
|
|
|
|
|
|
|
|
if (context != null) {
|
|
|
|
|
|
|
|
CallErrorHandler.showSignalRNotConnected(context);
|
|
|
|
|
|
|
|
}
|
|
|
|
_cleanup();
|
|
|
|
_cleanup();
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
log('✅ [ACCEPT] SignalR connected', name: 'CallManager');
|
|
|
|
log('✅ [ACCEPT] SignalR connected', name: 'CallManager');
|
|
|
|
log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
|
|
|
|
log(' SignalR state: ${_signalRService.connectionState}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Ensure handlers are registered
|
|
|
|
|
|
|
|
if (!_handlersRegistered) {
|
|
|
|
|
|
|
|
log('⚠️ [ACCEPT] Call handlers not registered, registering now...', name: 'CallManager');
|
|
|
|
|
|
|
|
_registerCallHandlers();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_updateCallStatus(CallStatus.connecting);
|
|
|
|
_updateCallStatus(CallStatus.connecting);
|
|
|
|
|
|
|
|
|
|
|
|
// Cancel timeout timer
|
|
|
|
// Cancel timeout timer
|
|
|
|
@ -611,14 +686,15 @@ class CallManager extends ChangeNotifier {
|
|
|
|
_conversationId ?? '',
|
|
|
|
_conversationId ?? '',
|
|
|
|
]);
|
|
|
|
]);
|
|
|
|
log('✅ [ACCEPT] AnswerCallAsync invoked successfully', name: 'CallManager');
|
|
|
|
log('✅ [ACCEPT] AnswerCallAsync invoked successfully', name: 'CallManager');
|
|
|
|
log(' Waiting for backend to process...', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
// Clear pending call data after successful accept
|
|
|
|
|
|
|
|
_pendingCallData = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize WebRTC
|
|
|
|
// Initialize WebRTC
|
|
|
|
log('🔧 [ACCEPT] Initializing WebRTC...', name: 'CallManager');
|
|
|
|
log('🔧 [ACCEPT] Initializing WebRTC...', name: 'CallManager');
|
|
|
|
log(' Call type: ${_currentCall!.type.name}', name: 'CallManager');
|
|
|
|
log(' Call type: ${_currentCall!.type.name}', name: 'CallManager');
|
|
|
|
await _initializeWebRTC();
|
|
|
|
await _initializeWebRTC();
|
|
|
|
log('✅ [ACCEPT] WebRTC initialized', name: 'CallManager');
|
|
|
|
log('✅ [ACCEPT] WebRTC initialized', name: 'CallManager');
|
|
|
|
log(' WebRTC service: ${_webrtcService != null ? "Created" : "NULL"}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Navigate to call screen
|
|
|
|
// Navigate to call screen
|
|
|
|
log('🧭 [ACCEPT] Navigating to call screen...', name: 'CallManager');
|
|
|
|
log('🧭 [ACCEPT] Navigating to call screen...', name: 'CallManager');
|
|
|
|
@ -627,20 +703,17 @@ class CallManager extends ChangeNotifier {
|
|
|
|
|
|
|
|
|
|
|
|
log('✅ [ACCEPT] Call accepted successfully', name: 'CallManager');
|
|
|
|
log('✅ [ACCEPT] Call accepted successfully', name: 'CallManager');
|
|
|
|
log('ℹ️ [ACCEPT] Now waiting for caller to send offer via OnOfferAsync...', name: 'CallManager');
|
|
|
|
log('ℹ️ [ACCEPT] Now waiting for caller to send offer via OnOfferAsync...', name: 'CallManager');
|
|
|
|
log('ℹ️ [ACCEPT] Expected sequence:', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' 1. Caller receives OnCallAcceptedAsync', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' 2. Caller creates offer and sends OfferAsync', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' 3. We receive OnOfferAsync', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' 4. We create answer and send AnswerOfferAsync', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' 5. Caller receives OnAnswerOfferAsync', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' 6. ICE candidates exchange', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' 7. Connection established', name: 'CallManager');
|
|
|
|
|
|
|
|
log('═══════════════════════════════════════════', name: 'CallManager');
|
|
|
|
log('═══════════════════════════════════════════', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
log('❌ [ACCEPT] Error accepting call: $e',
|
|
|
|
log('❌ [ACCEPT] Error accepting call: $e',
|
|
|
|
name: 'CallManager', error: e, stackTrace: stackTrace);
|
|
|
|
name: 'CallManager', error: e, stackTrace: stackTrace);
|
|
|
|
_cleanup();
|
|
|
|
_cleanup();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final context = navigatorKey.currentContext;
|
|
|
|
|
|
|
|
if (context != null) {
|
|
|
|
|
|
|
|
CallErrorHandler.showGenericError(context, 'Failed to accept call: $e');
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -721,13 +794,16 @@ class CallManager extends ChangeNotifier {
|
|
|
|
log('📤 [HANGUP] Invoking HangUpAsync...', name: 'CallManager');
|
|
|
|
log('📤 [HANGUP] Invoking HangUpAsync...', name: 'CallManager');
|
|
|
|
log(' From: ${_myEmployeeNumber ?? ""}', name: 'CallManager');
|
|
|
|
log(' From: ${_myEmployeeNumber ?? ""}', name: 'CallManager');
|
|
|
|
log(' To: $peerId', name: 'CallManager');
|
|
|
|
log(' To: $peerId', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' Module ID: ${_moduleId ?? "1"}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL FIX: HangUpAsync only takes 2 parameters (from, to)
|
|
|
|
// CRITICAL FIX: Backend HangUpAsync expects 3 parameters:
|
|
|
|
// The old code was sending moduleId as 3rd parameter which caused:
|
|
|
|
// 1. fromEmployeeNumber (string)
|
|
|
|
// "Failed to invoke 'HangUpAsync' due to an error on the server"
|
|
|
|
// 2. toEmployeeNumber (string)
|
|
|
|
|
|
|
|
// 3. moduleId (int or string)
|
|
|
|
await _signalRService.invoke('HangUpAsync', args: [
|
|
|
|
await _signalRService.invoke('HangUpAsync', args: [
|
|
|
|
_myEmployeeNumber ?? '',
|
|
|
|
_myEmployeeNumber ?? '',
|
|
|
|
peerId,
|
|
|
|
peerId,
|
|
|
|
|
|
|
|
_moduleId ?? '1', // Add moduleId as 3rd parameter
|
|
|
|
]);
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
log('✅ [HANGUP] HangUpAsync invoked successfully', name: 'CallManager');
|
|
|
|
log('✅ [HANGUP] HangUpAsync invoked successfully', name: 'CallManager');
|
|
|
|
@ -795,7 +871,7 @@ class CallManager extends ChangeNotifier {
|
|
|
|
log('🎤 [CONTROL] Toggling mute: $_isMuted -> ${!_isMuted}', name: 'CallManager');
|
|
|
|
log('🎤 [CONTROL] Toggling mute: $_isMuted -> ${!_isMuted}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
_isMuted = !_isMuted;
|
|
|
|
_isMuted = !_isMuted;
|
|
|
|
_webrtcService?.setMicrophoneMuted(_isMuted);
|
|
|
|
_webrtcService.setMicrophoneMuted(_isMuted);
|
|
|
|
|
|
|
|
|
|
|
|
if (await _signalRService.ensureConnected()) {
|
|
|
|
if (await _signalRService.ensureConnected()) {
|
|
|
|
await _signalRService.invoke('AudioToggle', args: [
|
|
|
|
await _signalRService.invoke('AudioToggle', args: [
|
|
|
|
@ -813,7 +889,7 @@ class CallManager extends ChangeNotifier {
|
|
|
|
log('🔊 [CONTROL] Toggling speaker: $_isSpeakerOn -> ${!_isSpeakerOn}', name: 'CallManager');
|
|
|
|
log('🔊 [CONTROL] Toggling speaker: $_isSpeakerOn -> ${!_isSpeakerOn}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
_isSpeakerOn = !_isSpeakerOn;
|
|
|
|
_isSpeakerOn = !_isSpeakerOn;
|
|
|
|
await _webrtcService?.setSpeakerphoneEnabled(_isSpeakerOn);
|
|
|
|
await _webrtcService.setSpeakerphoneEnabled(_isSpeakerOn);
|
|
|
|
notifyListeners();
|
|
|
|
notifyListeners();
|
|
|
|
|
|
|
|
|
|
|
|
log('✅ [CONTROL] Speaker toggled: $_isSpeakerOn', name: 'CallManager');
|
|
|
|
log('✅ [CONTROL] Speaker toggled: $_isSpeakerOn', name: 'CallManager');
|
|
|
|
@ -826,7 +902,7 @@ class CallManager extends ChangeNotifier {
|
|
|
|
log('📹 [CONTROL] Toggling camera: $_isCameraOn -> ${!_isCameraOn}', name: 'CallManager');
|
|
|
|
log('📹 [CONTROL] Toggling camera: $_isCameraOn -> ${!_isCameraOn}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
_isCameraOn = !_isCameraOn;
|
|
|
|
_isCameraOn = !_isCameraOn;
|
|
|
|
_webrtcService?.setCameraEnabled(_isCameraOn);
|
|
|
|
_webrtcService.setCameraEnabled(_isCameraOn);
|
|
|
|
|
|
|
|
|
|
|
|
if (await _signalRService.ensureConnected()) {
|
|
|
|
if (await _signalRService.ensureConnected()) {
|
|
|
|
await _signalRService.invoke('CameraToggle', args: [
|
|
|
|
await _signalRService.invoke('CameraToggle', args: [
|
|
|
|
@ -844,7 +920,7 @@ class CallManager extends ChangeNotifier {
|
|
|
|
if (_currentCall?.type != CallType.video || !_isCameraOn) return;
|
|
|
|
if (_currentCall?.type != CallType.video || !_isCameraOn) return;
|
|
|
|
|
|
|
|
|
|
|
|
log('🔄 [CONTROL] Switching camera...', name: 'CallManager');
|
|
|
|
log('🔄 [CONTROL] Switching camera...', name: 'CallManager');
|
|
|
|
await _webrtcService?.switchCamera();
|
|
|
|
await _webrtcService.switchCamera();
|
|
|
|
log('✅ [CONTROL] Camera switched', name: 'CallManager');
|
|
|
|
log('✅ [CONTROL] Camera switched', name: 'CallManager');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -882,13 +958,25 @@ class CallManager extends ChangeNotifier {
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _initializeWebRTC() async {
|
|
|
|
Future<void> _initializeWebRTC() async {
|
|
|
|
log('🔧 [WEBRTC] Initializing WebRTC...', name: 'CallManager');
|
|
|
|
log('🔧 [WEBRTC] Initializing WebRTC...', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' Current call type: ${_currentCall!.type.name}', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' WebRTC service hashCode: ${_webrtcService.hashCode}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL FIX: Create a completer to track initialization completion
|
|
|
|
|
|
|
|
_webrtcInitCompleter = Completer<void>();
|
|
|
|
|
|
|
|
|
|
|
|
_webrtcService = WebRTCService();
|
|
|
|
try {
|
|
|
|
|
|
|
|
// CRITICAL FIX: Setup callbacks BEFORE initializing WebRTC
|
|
|
|
|
|
|
|
log('🔧 [WEBRTC] Setting up callbacks BEFORE initialization...', name: 'CallManager');
|
|
|
|
_setupWebRTCCallbacks();
|
|
|
|
_setupWebRTCCallbacks();
|
|
|
|
|
|
|
|
log('✅ [WEBRTC] Callbacks set up', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Verify callback was actually set
|
|
|
|
|
|
|
|
log('🔍 [WEBRTC] Verifying callback assignment...', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' onIceConnectionStateChange is null?: ${_webrtcService.onIceConnectionStateChange == null}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
if (_currentCall!.type == CallType.audio) {
|
|
|
|
if (_currentCall!.type == CallType.audio) {
|
|
|
|
log('🎵 [WEBRTC] Initializing for audio call', name: 'CallManager');
|
|
|
|
log('🎵 [WEBRTC] Initializing for audio call', name: 'CallManager');
|
|
|
|
await _webrtcService!.initializeForAudioCall();
|
|
|
|
await _webrtcService.initializeForAudioCall();
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL FIX: Speaker is OFF by default in WebRTC service (earpiece mode)
|
|
|
|
// CRITICAL FIX: Speaker is OFF by default in WebRTC service (earpiece mode)
|
|
|
|
// Update the CallManager state to reflect this
|
|
|
|
// Update the CallManager state to reflect this
|
|
|
|
@ -897,17 +985,65 @@ class CallManager extends ChangeNotifier {
|
|
|
|
notifyListeners();
|
|
|
|
notifyListeners();
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
log('📹 [WEBRTC] Initializing for video call', name: 'CallManager');
|
|
|
|
log('📹 [WEBRTC] Initializing for video call', name: 'CallManager');
|
|
|
|
await _webrtcService!.initializeForVideoCall();
|
|
|
|
await _webrtcService.initializeForVideoCall();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Verify callback is still set after initialization
|
|
|
|
|
|
|
|
log('🔍 [WEBRTC] Post-initialization callback verification...', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' onIceConnectionStateChange is null?: ${_webrtcService.onIceConnectionStateChange == null}', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' Peer connection initialized?: ${_webrtcService.isPeerConnectionInitialized}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
log('✅ [WEBRTC] WebRTC initialized', name: 'CallManager');
|
|
|
|
log('✅ [WEBRTC] WebRTC initialized', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL FIX: Signal that WebRTC initialization is complete
|
|
|
|
|
|
|
|
if (!_webrtcInitCompleter!.isCompleted) {
|
|
|
|
|
|
|
|
_webrtcInitCompleter!.complete();
|
|
|
|
|
|
|
|
log('✅ [WEBRTC] Initialization completer signaled', name: 'CallManager');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL FIX: Process any pending offer that arrived during initialization
|
|
|
|
|
|
|
|
if (_pendingOfferSdp != null) {
|
|
|
|
|
|
|
|
log('📥 [WEBRTC] Processing pending offer that arrived during initialization...', name: 'CallManager');
|
|
|
|
|
|
|
|
final offerSdp = _pendingOfferSdp!;
|
|
|
|
|
|
|
|
_pendingOfferSdp = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Process the offer now that WebRTC is ready
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
log('🔧 [WEBRTC] Creating answer for pending offer...', name: 'CallManager');
|
|
|
|
|
|
|
|
final answer = await _webrtcService.createAnswer(offerSdp);
|
|
|
|
|
|
|
|
log('✅ [WEBRTC] Answer created for pending offer (length: ${answer.sdp?.length ?? 0})', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log('📤 [WEBRTC] Sending pending answer via AnswerOfferAsync...', name: 'CallManager');
|
|
|
|
|
|
|
|
await _signalRService.invoke('AnswerOfferAsync', args: [
|
|
|
|
|
|
|
|
_currentCall!.peerId,
|
|
|
|
|
|
|
|
answer.sdp ?? '',
|
|
|
|
|
|
|
|
_currentCall!.callId,
|
|
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
log('✅ [WEBRTC] Pending answer sent successfully', name: 'CallManager');
|
|
|
|
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
|
|
|
|
log('❌ [WEBRTC] Error processing pending offer: $e',
|
|
|
|
|
|
|
|
name: 'CallManager', error: e, stackTrace: stackTrace);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
|
|
|
|
log('❌ [WEBRTC] Initialization error: $e', name: 'CallManager', error: e, stackTrace: stackTrace);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Complete the completer with error
|
|
|
|
|
|
|
|
if (!_webrtcInitCompleter!.isCompleted) {
|
|
|
|
|
|
|
|
_webrtcInitCompleter!.completeError(e, stackTrace);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rethrow;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _setupWebRTCCallbacks() {
|
|
|
|
void _setupWebRTCCallbacks() {
|
|
|
|
log('🔧 [WEBRTC] Setting up callbacks...', name: 'CallManager');
|
|
|
|
log('🔧 [WEBRTC] Setting up callbacks...', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' CallManager hashCode: $hashCode', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' WebRTC service hashCode: ${_webrtcService.hashCode}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
_webrtcService!.onIceCandidate = (RTCIceCandidate candidate) {
|
|
|
|
_webrtcService.onIceCandidate = (RTCIceCandidate candidate) {
|
|
|
|
log('🧊 [WEBRTC] ICE candidate generated', name: 'CallManager');
|
|
|
|
log('🧊 [WEBRTC-CALLBACK] ICE candidate generated in CallManager', name: 'CallManager');
|
|
|
|
log(' Candidate: ${candidate.candidate?.substring(0, 50)}...', name: 'CallManager');
|
|
|
|
log(' Candidate: ${candidate.candidate?.substring(0, 50)}...', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
final candidateJson = jsonEncode({
|
|
|
|
final candidateJson = jsonEncode({
|
|
|
|
@ -923,29 +1059,43 @@ class CallManager extends ChangeNotifier {
|
|
|
|
]);
|
|
|
|
]);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_webrtcService!.onRemoteStream = (MediaStream stream) {
|
|
|
|
_webrtcService.onRemoteStream = (MediaStream stream) {
|
|
|
|
log('📡 [WEBRTC] Remote stream received', name: 'CallManager');
|
|
|
|
log('📡 [WEBRTC-CALLBACK] Remote stream received in CallManager', name: 'CallManager');
|
|
|
|
log(' Audio tracks: ${stream.getAudioTracks().length}', name: 'CallManager');
|
|
|
|
log(' Audio tracks: ${stream.getAudioTracks().length}', name: 'CallManager');
|
|
|
|
log(' Video tracks: ${stream.getVideoTracks().length}', name: 'CallManager');
|
|
|
|
log(' Video tracks: ${stream.getVideoTracks().length}', name: 'CallManager');
|
|
|
|
notifyListeners();
|
|
|
|
notifyListeners();
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
_webrtcService!.onIceConnectionStateChange = (RTCIceConnectionState state) {
|
|
|
|
_webrtcService.onIceConnectionStateChange = (RTCIceConnectionState state) {
|
|
|
|
log('🔗 [WEBRTC] ICE state: ${state.toString()}', name: 'CallManager');
|
|
|
|
log('═══════════════════════════════════════════', name: 'CallManager');
|
|
|
|
|
|
|
|
log('🔗 [WEBRTC-CALLBACK] ICE state change RECEIVED in CallManager!', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' State: ${state.toString()}', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' Current call status: ${_callStatus.name}', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' CallManager hashCode: $hashCode', name: 'CallManager');
|
|
|
|
|
|
|
|
log('═══════════════════════════════════════════', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) {
|
|
|
|
if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) {
|
|
|
|
log('✅ [WEBRTC] ICE connection established', name: 'CallManager');
|
|
|
|
log('✅ [WEBRTC-CALLBACK] ICE connection established - updating status to CONNECTED', name: 'CallManager');
|
|
|
|
_updateCallStatus(CallStatus.connected);
|
|
|
|
_updateCallStatus(CallStatus.connected);
|
|
|
|
_startCallDurationTimer();
|
|
|
|
_startCallDurationTimer();
|
|
|
|
|
|
|
|
log('✅ [WEBRTC-CALLBACK] Status updated and timer started', name: 'CallManager');
|
|
|
|
} else if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) {
|
|
|
|
} else if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) {
|
|
|
|
log('❌ [WEBRTC] ICE connection failed', name: 'CallManager');
|
|
|
|
log('❌ [WEBRTC-CALLBACK] ICE connection failed', name: 'CallManager');
|
|
|
|
_cleanup();
|
|
|
|
// CRITICAL FIX: Don't cleanup immediately - try to notify peer first
|
|
|
|
|
|
|
|
_handleIceConnectionFailure();
|
|
|
|
} else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) {
|
|
|
|
} else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) {
|
|
|
|
log('⚠️ [WEBRTC] ICE connection disconnected', name: 'CallManager');
|
|
|
|
log('⚠️ [WEBRTC-CALLBACK] ICE connection disconnected', name: 'CallManager');
|
|
|
|
|
|
|
|
// CRITICAL FIX: Also handle disconnect gracefully
|
|
|
|
|
|
|
|
_handleIceDisconnection();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
log('ℹ️ [WEBRTC-CALLBACK] ICE state: ${state.toString()}', name: 'CallManager');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
log('✅ [WEBRTC] Callbacks set up', name: 'CallManager');
|
|
|
|
log('✅ [WEBRTC] Callbacks set up', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' onIceCandidate set?: ${_webrtcService.onIceCandidate != null}', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' onRemoteStream set?: ${_webrtcService.onRemoteStream != null}', name: 'CallManager');
|
|
|
|
|
|
|
|
log(' onIceConnectionStateChange set?: ${_webrtcService.onIceConnectionStateChange != null}', name: 'CallManager');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _navigateToCallScreen() {
|
|
|
|
void _navigateToCallScreen() {
|
|
|
|
@ -992,8 +1142,8 @@ class CallManager extends ChangeNotifier {
|
|
|
|
_callTimeoutTimer?.cancel();
|
|
|
|
_callTimeoutTimer?.cancel();
|
|
|
|
_callDurationTimer?.cancel();
|
|
|
|
_callDurationTimer?.cancel();
|
|
|
|
|
|
|
|
|
|
|
|
_webrtcService?.dispose();
|
|
|
|
// Dispose WebRTC service (it's a singleton, just cleanup the peer connection)
|
|
|
|
_webrtcService = null;
|
|
|
|
_webrtcService.dispose();
|
|
|
|
|
|
|
|
|
|
|
|
if (_currentCall != null) {
|
|
|
|
if (_currentCall != null) {
|
|
|
|
_callKitService.endCall(_currentCall!.callId);
|
|
|
|
_callKitService.endCall(_currentCall!.callId);
|
|
|
|
@ -1080,20 +1230,9 @@ class CallManager extends ChangeNotifier {
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// Ensure WebRTC is initialized before creating offer
|
|
|
|
// Ensure WebRTC is initialized before creating offer
|
|
|
|
if (_webrtcService == null) {
|
|
|
|
|
|
|
|
log('⚠️ [EVENT] WebRTC not initialized yet, waiting...', name: 'CallManager');
|
|
|
|
|
|
|
|
// Wait a bit for WebRTC to initialize (it should be initializing in parallel)
|
|
|
|
|
|
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (_webrtcService == null) {
|
|
|
|
|
|
|
|
log('❌ [EVENT] WebRTC still not initialized after wait', name: 'CallManager');
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CALLER SIDE: Create and send offer
|
|
|
|
// CALLER SIDE: Create and send offer
|
|
|
|
log('🔧 [EVENT] [CALLER] Creating SDP offer...', name: 'CallManager');
|
|
|
|
log('🔧 [EVENT] [CALLER] Creating SDP offer...', name: 'CallManager');
|
|
|
|
final offer = await _webrtcService!.createOffer();
|
|
|
|
final offer = await _webrtcService.createOffer();
|
|
|
|
log('✅ [EVENT] [CALLER] SDP offer created (length: ${offer.sdp?.length ?? 0})', name: 'CallManager');
|
|
|
|
log('✅ [EVENT] [CALLER] SDP offer created (length: ${offer.sdp?.length ?? 0})', name: 'CallManager');
|
|
|
|
log(' Offer SDP type: ${offer.type}', name: 'CallManager');
|
|
|
|
log(' Offer SDP type: ${offer.type}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
@ -1208,7 +1347,6 @@ class CallManager extends ChangeNotifier {
|
|
|
|
log('📞 [EVENT] 📥 OnOfferAsync received', name: 'CallManager');
|
|
|
|
log('📞 [EVENT] 📥 OnOfferAsync received', name: 'CallManager');
|
|
|
|
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
|
|
|
|
log(' Args count: ${args?.length ?? 0}', name: 'CallManager');
|
|
|
|
log(' Current status: ${_callStatus.name}', name: 'CallManager');
|
|
|
|
log(' Current status: ${_callStatus.name}', name: 'CallManager');
|
|
|
|
log(' WebRTC initialized: ${_webrtcService != null}', name: 'CallManager');
|
|
|
|
|
|
|
|
log('═══════════════════════════════════════════', name: 'CallManager');
|
|
|
|
log('═══════════════════════════════════════════', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
if (args == null || args.isEmpty) {
|
|
|
|
if (args == null || args.isEmpty) {
|
|
|
|
@ -1225,13 +1363,21 @@ class CallManager extends ChangeNotifier {
|
|
|
|
|
|
|
|
|
|
|
|
log(' Offer SDP length: ${offerSdp.length}', name: 'CallManager');
|
|
|
|
log(' Offer SDP length: ${offerSdp.length}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
if (_webrtcService == null) {
|
|
|
|
// CRITICAL FIX: Wait for WebRTC initialization to complete before processing offer
|
|
|
|
log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager');
|
|
|
|
if (!_webrtcService.isFullyInitialized) {
|
|
|
|
|
|
|
|
log('⏳ [EVENT] Waiting for WebRTC Initialization', name: 'CallManager');
|
|
|
|
|
|
|
|
await _webrtcService.waitForInitialization();
|
|
|
|
|
|
|
|
log('✅ [EVENT] WebRTC Initialization Complete', name: 'CallManager');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Verify peer connection is ready
|
|
|
|
|
|
|
|
if (!_webrtcService.isPeerConnectionInitialized) {
|
|
|
|
|
|
|
|
log('❌ [EVENT] Peer connection not initialized - cannot process offer', name: 'CallManager');
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log('🔧 [EVENT] Creating SDP answer...', name: 'CallManager');
|
|
|
|
log('🔧 [EVENT] Creating SDP answer...', name: 'CallManager');
|
|
|
|
final answer = await _webrtcService!.createAnswer(offerSdp);
|
|
|
|
final answer = await _webrtcService.createAnswer(offerSdp);
|
|
|
|
log('✅ [EVENT] SDP answer created (length: ${answer.sdp?.length ?? 0})', name: 'CallManager');
|
|
|
|
log('✅ [EVENT] SDP answer created (length: ${answer.sdp?.length ?? 0})', name: 'CallManager');
|
|
|
|
log(' Answer SDP type: ${answer.type}', name: 'CallManager');
|
|
|
|
log(' Answer SDP type: ${answer.type}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
@ -1274,11 +1420,6 @@ class CallManager extends ChangeNotifier {
|
|
|
|
|
|
|
|
|
|
|
|
log(' Answer SDP length: ${answerSdp.length}', name: 'CallManager');
|
|
|
|
log(' Answer SDP length: ${answerSdp.length}', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
if (_webrtcService == null) {
|
|
|
|
|
|
|
|
log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager');
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log('🔧 [EVENT] Setting remote answer...', name: 'CallManager');
|
|
|
|
log('🔧 [EVENT] Setting remote answer...', name: 'CallManager');
|
|
|
|
await _webrtcService!.setRemoteAnswer(answerSdp);
|
|
|
|
await _webrtcService!.setRemoteAnswer(answerSdp);
|
|
|
|
log('✅ [EVENT] Remote answer set successfully', name: 'CallManager');
|
|
|
|
log('✅ [EVENT] Remote answer set successfully', name: 'CallManager');
|
|
|
|
@ -1305,11 +1446,6 @@ class CallManager extends ChangeNotifier {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (_webrtcService == null) {
|
|
|
|
|
|
|
|
log('⚠️ [EVENT] WebRTC service not initialized', name: 'CallManager');
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final candidateData = jsonDecode(candidateJson) as Map<String, dynamic>;
|
|
|
|
final candidateData = jsonDecode(candidateJson) as Map<String, dynamic>;
|
|
|
|
final candidate = RTCIceCandidate(
|
|
|
|
final candidate = RTCIceCandidate(
|
|
|
|
candidateData['candidate'] as String?,
|
|
|
|
candidateData['candidate'] as String?,
|
|
|
|
@ -1319,7 +1455,7 @@ class CallManager extends ChangeNotifier {
|
|
|
|
|
|
|
|
|
|
|
|
log('🧊 [EVENT] Adding remote ICE candidate...', name: 'CallManager');
|
|
|
|
log('🧊 [EVENT] Adding remote ICE candidate...', name: 'CallManager');
|
|
|
|
log(' Candidate: ${candidate.candidate?.substring(0, 50)}...', name: 'CallManager');
|
|
|
|
log(' Candidate: ${candidate.candidate?.substring(0, 50)}...', name: 'CallManager');
|
|
|
|
await _webrtcService!.addIceCandidate(candidate);
|
|
|
|
await _webrtcService.addIceCandidate(candidate);
|
|
|
|
log('✅ [EVENT] Remote ICE candidate added', name: 'CallManager');
|
|
|
|
log('✅ [EVENT] Remote ICE candidate added', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
@ -1437,4 +1573,53 @@ class CallManager extends ChangeNotifier {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
log('═══════════════════════════════════════════', name: 'CallManager');
|
|
|
|
log('═══════════════════════════════════════════', name: 'CallManager');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== ICE Connection Failure Handlers ====================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Handle ICE connection failure
|
|
|
|
|
|
|
|
Future<void> _handleIceConnectionFailure() async {
|
|
|
|
|
|
|
|
log('❌ [ICE] Connection failed - attempting graceful hangup...', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
// Try to notify peer via HangUpAsync before cleanup
|
|
|
|
|
|
|
|
if (_currentCall != null && await _signalRService.ensureConnected()) {
|
|
|
|
|
|
|
|
log('📤 [ICE] Notifying peer about connection failure...', name: 'CallManager');
|
|
|
|
|
|
|
|
await _signalRService.invoke('HangUpAsync', args: [
|
|
|
|
|
|
|
|
_myEmployeeNumber ?? '',
|
|
|
|
|
|
|
|
_currentCall!.peerId,
|
|
|
|
|
|
|
|
_moduleId ?? '1',
|
|
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
log('⚠️ [ICE] Failed to notify peer: $e', name: 'CallManager');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Cleanup and show error to user
|
|
|
|
|
|
|
|
_cleanup();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final context = navigatorKey.currentContext;
|
|
|
|
|
|
|
|
if (context != null) {
|
|
|
|
|
|
|
|
CallErrorHandler.showGenericError(
|
|
|
|
|
|
|
|
context,
|
|
|
|
|
|
|
|
'Connection failed. Please check your network and try again.',
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Handle ICE connection disconnection
|
|
|
|
|
|
|
|
Future<void> _handleIceDisconnection() async {
|
|
|
|
|
|
|
|
log('⚠️ [ICE] Connection disconnected - waiting for reconnection...', name: 'CallManager');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Wait a few seconds to see if connection recovers
|
|
|
|
|
|
|
|
await Future.delayed(const Duration(seconds: 5));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check if still disconnected
|
|
|
|
|
|
|
|
final state = _webrtcService.getIceConnectionState();
|
|
|
|
|
|
|
|
if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) {
|
|
|
|
|
|
|
|
log('❌ [ICE] Still disconnected after 5s - treating as failure', name: 'CallManager');
|
|
|
|
|
|
|
|
await _handleIceConnectionFailure();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
log('✅ [ICE] Connection recovered: $state', name: 'CallManager');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|