From 84fe851ec80fdd5616591da9e205e4106c4020d6 Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:18:46 +0300 Subject: [PATCH] foreground calling implemented and working fine --- lib/core/di/service_locator.dart | 11 +- lib/main.dart | 4 +- .../chat/call/services/webrtc_service.dart | 80 ++++- lib/modules/cx_module/chat/chat_provider.dart | 34 +- .../cx_module/chat/services/call_manager.dart | 335 ++++++++++++++---- 5 files changed, 358 insertions(+), 106 deletions(-) diff --git a/lib/core/di/service_locator.dart b/lib/core/di/service_locator.dart index 34a10706..455b2ea1 100644 --- a/lib/core/di/service_locator.dart +++ b/lib/core/di/service_locator.dart @@ -1,19 +1,28 @@ import 'package:get_it/get_it.dart'; import 'package:test_sa/modules/cx_module/chat/services/signalr_service.dart'; +import 'package:test_sa/modules/cx_module/chat/call/services/webrtc_service.dart'; + /// Global service locator instance final getIt = GetIt.instance; + /// Setup dependency injection Future setupServiceLocator() async { // SignalR Service - ONLY ONE instance for the entire app getIt.registerLazySingleton( () => SignalRService(), ); + + // WebRTC Service - ONLY ONE instance for the entire app + getIt.registerLazySingleton( + () => WebRTCService(), + ); + print('✅ [DI] Service locator initialized'); print(' - SignalRService registered as singleton (hashCode: ${getIt().hashCode})'); + print(' - WebRTCService registered as singleton (hashCode: ${getIt().hashCode})'); } Future resetServices() async { await getIt().reset(); print('✅ [DI] All services reset'); } - diff --git a/lib/main.dart b/lib/main.dart index 30719d7d..a29f1b0a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -361,8 +361,8 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider(create: (_) => CleanerProvider(), lazy: true), // EXPORT MODULE PROVIDERS (3) - ChangeNotifierProvider(create: (_) => AssetColumnsProvider(), lazy: true), - ChangeNotifierProvider(create: (_) => ExportFormatProvider(), lazy: true), + ChangeNotifierProvider(create: (_) => AssetColumnsProvider(), lazy: true), + ChangeNotifierProvider(create: (_) => ExportFormatProvider(), lazy: true), ChangeNotifierProvider(create: (_) => ExportApiProvider(), lazy: true), // UTILITY PROVIDERS (1) diff --git a/lib/modules/cx_module/chat/call/services/webrtc_service.dart b/lib/modules/cx_module/chat/call/services/webrtc_service.dart index 4900b441..c3b8676b 100644 --- a/lib/modules/cx_module/chat/call/services/webrtc_service.dart +++ b/lib/modules/cx_module/chat/call/services/webrtc_service.dart @@ -27,6 +27,10 @@ class WebRTCService { final List _iceCandidateQueue = []; bool _remoteDescriptionSet = false; + // CRITICAL FIX: Atomic initialization tracking + Completer? _initializationCompleter; + bool _isFullyInitialized = false; + // Track if current call is video call bool _isVideoCall = false; @@ -123,10 +127,12 @@ class WebRTCService { /// Initialize WebRTC for audio call Future initializeForAudioCall() async { + // CRITICAL FIX: Create completer and mark as not initialized + _initializationCompleter = Completer(); + _isFullyInitialized = false; + try { - if (kDebugMode) { - log('🔧 [WebRTC] Initializing audio call', name: 'WebRTCService'); - } + log('🔧 [WebRTC] Initialization Started', name: 'WebRTCService'); _isVideoCall = false; @@ -186,22 +192,31 @@ class WebRTCService { // Setup peer connection event handlers _setupPeerConnectionListeners(); - // CRITICAL FIX: Keep speakerphone OFF by default for audio calls - // User can manually enable it if needed via the speaker button - log('🔇 [WebRTC] Keeping speakerphone OFF by default for audio call...', name: 'WebRTCService'); + // CRITICAL FIX: Set speakerphone OFF by default (use earpiece) + log('🔇 [WebRTC] Setting speakerphone OFF by default (earpiece mode)...', name: 'WebRTCService'); try { await Helper.setSpeakerphoneOn(false); log('✅ [WebRTC] Speakerphone disabled (earpiece mode)', name: 'WebRTCService'); } catch (e) { log('âš ī¸ [WebRTC] Failed to disable speakerphone: $e', name: 'WebRTCService'); - // Continue anyway - not critical } - if (kDebugMode) { - log('✅ [WebRTC] Audio call initialized', name: 'WebRTCService'); + // CRITICAL FIX: Mark as fully initialized AFTER everything is ready + _isFullyInitialized = true; + if (!_initializationCompleter!.isCompleted) { + _initializationCompleter!.complete(); } + log('✅ [WebRTC] Initialization Completed - PeerConnection Ready', name: 'WebRTCService'); + } catch (e, stackTrace) { log('❌ [WebRTC] Initialization error: $e', name: 'WebRTCService', error: e, stackTrace: stackTrace); + + // Complete with error + if (_initializationCompleter != null && !_initializationCompleter!.isCompleted) { + _initializationCompleter!.completeError(e, stackTrace); + } + _isFullyInitialized = false; + // Clean up any partial initialization await dispose(); rethrow; @@ -271,33 +286,50 @@ class WebRTCService { /// Setup peer connection event listeners void _setupPeerConnectionListeners() { + log('🔧 [WebRTC] Setting up peer connection listeners...', name: 'WebRTCService'); + // Handle ICE candidates _peerConnection!.onIceCandidate = (RTCIceCandidate candidate) { + log('🧊 [WebRTC] onIceCandidate triggered', name: 'WebRTCService'); if (onIceCandidate != null) { onIceCandidate!(candidate); + } else { + log('âš ī¸ [WebRTC] onIceCandidate callback is NULL!', name: 'WebRTCService'); } }; // Handle ICE gathering state changes _peerConnection!.onIceGatheringState = (RTCIceGatheringState state) { - if (kDebugMode && state == RTCIceGatheringState.RTCIceGatheringStateComplete) { + log('📊 [WebRTC] ICE gathering state: ${state.toString()}', name: 'WebRTCService'); + if (state == RTCIceGatheringState.RTCIceGatheringStateComplete) { log('✅ [WebRTC] ICE gathering completed', name: 'WebRTCService'); } }; // Handle ICE connection state changes _peerConnection!.onIceConnectionState = (RTCIceConnectionState state) { - if (kDebugMode) { - log('🔗 [WebRTC] ICE state: ${state.toString()}', name: 'WebRTCService'); - } + log('═══════════════════════════════════════════', name: 'WebRTCService'); + log('🔗 [WebRTC] onIceConnectionState TRIGGERED', name: 'WebRTCService'); + log(' State: ${state.toString()}', name: 'WebRTCService'); + log(' Callback is null?: ${onIceConnectionStateChange == null}', name: 'WebRTCService'); + log('═══════════════════════════════════════════', name: 'WebRTCService'); // Log critical failures if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) { log('❌ [WebRTC] ICE connection failed', name: 'WebRTCService'); } + if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) { + log('✅ [WebRTC] ICE connection CONNECTED!', name: 'WebRTCService'); + } + if (onIceConnectionStateChange != null) { + log('📤 [WebRTC] Forwarding ICE state to callback...', name: 'WebRTCService'); onIceConnectionStateChange!(state); + log('✅ [WebRTC] ICE state forwarded to callback', name: 'WebRTCService'); + } else { + log('❌ [WebRTC] CRITICAL: onIceConnectionStateChange callback is NULL!', name: 'WebRTCService'); + log(' This means CallManager did not set up callbacks properly!', name: 'WebRTCService'); } }; @@ -305,11 +337,11 @@ class WebRTCService { _peerConnection!.onTrack = (RTCTrackEvent event) { log('═══════════════════════════════════════════', name: 'WebRTCService'); log('📡 [WebRTC] onTrack event received!', name: 'WebRTCService'); - log(' Track kind: ${event.track?.kind}', name: 'WebRTCService'); - log(' Track ID: ${event.track?.id}', name: 'WebRTCService'); - log(' Track enabled: ${event.track?.enabled}', name: 'WebRTCService'); - log(' Track muted: ${event.track?.muted}', name: 'WebRTCService'); - // log(' Track readyState: ${event.track?.readyState}', name: 'WebRTCService'); + log(' Track kind: ${event.track.kind}', name: 'WebRTCService'); + log(' Track ID: ${event.track.id}', name: 'WebRTCService'); + log(' Track enabled: ${event.track.enabled}', name: 'WebRTCService'); + log(' Track muted: ${event.track.muted}', name: 'WebRTCService'); + // log(' Track readyState: ${event.track.readyState}', name: 'WebRTCService'); log(' Streams count: ${event.streams.length}', name: 'WebRTCService'); if (event.streams.isNotEmpty) { @@ -718,6 +750,18 @@ class WebRTCService { } /// Get peer connection state + /// CRITICAL FIX: Check if WebRTC is FULLY initialized (ready to process signaling) + bool get isFullyInitialized => _isFullyInitialized; + + /// CRITICAL FIX: Wait for WebRTC initialization to complete + Future waitForInitialization() async { + if (_initializationCompleter != null && !_initializationCompleter!.isCompleted) { + log('âŗ [WebRTC] Waiting for initialization to complete...', name: 'WebRTCService'); + await _initializationCompleter!.future; + log('✅ [WebRTC] Initialization wait complete', name: 'WebRTCService'); + } + } + RTCPeerConnectionState? getPeerConnectionState() { return _peerConnection?.connectionState; } diff --git a/lib/modules/cx_module/chat/chat_provider.dart b/lib/modules/cx_module/chat/chat_provider.dart index 75828244..7a7417da 100644 --- a/lib/modules/cx_module/chat/chat_provider.dart +++ b/lib/modules/cx_module/chat/chat_provider.dart @@ -348,16 +348,18 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { log('🔌 [ChatProvider] buildHubConnection called', name: 'ChatProvider'); log('đŸ’Ŧ Conversation ID: $conversationID', name: 'ChatProvider'); - // Use SignalR service from DI + // CRITICAL FIX: Use the SINGLETON SignalR service from DI + // DO NOT create a new connection - reuse the existing one final signalRService = _signalRService; log('🔍 [ChatProvider] SignalR service instance: ${signalRService.hashCode}', name: 'ChatProvider'); log(' Is connected: ${signalRService.isConnected}', name: 'ChatProvider'); log(' Connection state: ${signalRService.connectionState}', name: 'ChatProvider'); - // Initialize SignalR if not already connected + // CRITICAL FIX: Only initialize if not connected at all + // If already connected (from CallManager), just join the new conversation if (!signalRService.isConnected) { - log('🔌 [ChatProvider] Initializing SignalR connection...', name: 'ChatProvider'); + log('🔌 [ChatProvider] SignalR not connected, initializing...', name: 'ChatProvider'); final connected = await signalRService.initialize( userId: chatLoginResponse!.userId.toString(), @@ -372,17 +374,29 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { log('✅ [ChatProvider] SignalR initialized', name: 'ChatProvider'); log(' Connection ID: ${signalRService.connectionId}', name: 'ChatProvider'); } else { - log('✅ [ChatProvider] SignalR already connected', name: 'ChatProvider'); + log('✅ [ChatProvider] SignalR already connected - reusing existing connection', name: 'ChatProvider'); log(' Connection ID: ${signalRService.connectionId}', name: 'ChatProvider'); - // Just join the conversation if needed + // CRITICAL FIX: Just update the conversation context + // Do NOT call initialize() again - it will dispose the connection! + // The SignalRService.initialize() method already handles conversation switching if (conversationID.isNotEmpty) { - await signalRService.invoke("JoinConversation", args: [conversationID]); - log('✅ [ChatProvider] Joined conversation: $conversationID', name: 'ChatProvider'); + try { + await signalRService.invoke("JoinConversation", args: [conversationID]); + log('✅ [ChatProvider] Joined conversation: $conversationID', name: 'ChatProvider'); + } catch (e) { + log('âš ī¸ [ChatProvider] Error joining conversation (will retry): $e', name: 'ChatProvider'); + // If join fails, try to reconnect with the conversation + await signalRService.initialize( + userId: chatLoginResponse!.userId.toString(), + authToken: chatLoginResponse!.token ?? '', + conversationId: conversationID, + ); + } } } - // Use the connection from SignalRService + // CRITICAL FIX: Reference the singleton connection, don't create a new one chatHubConnection = signalRService.hubConnection; // Log user details @@ -399,8 +413,8 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { if (kDebugMode) { print('âš ī¸ Error building SignalR connection: $e'); } - // Clean up on error - await _disposeConnection(); + // CRITICAL FIX: Don't dispose on error - just log and continue + // The connection might still be usable for calls rethrow; } } diff --git a/lib/modules/cx_module/chat/services/call_manager.dart b/lib/modules/cx_module/chat/services/call_manager.dart index be82d583..fd496d6c 100644 --- a/lib/modules/cx_module/chat/services/call_manager.dart +++ b/lib/modules/cx_module/chat/services/call_manager.dart @@ -26,8 +26,11 @@ class CallManager extends ChangeNotifier { // Services - Retrieved from DI SignalRService get _signalRService => getIt(); + WebRTCService get _webrtcService => getIt(); final CallKitService _callKitService = CallKitService(); - WebRTCService? _webrtcService; + + // Public getter for WebRTC service (used by call pages and ChatProvider) + WebRTCService get webrtcService => _webrtcService; // Call state CallSession? _currentCall; @@ -61,6 +64,13 @@ class CallManager extends ChangeNotifier { static String? _cachedAuthToken; static String? _cachedEmployeeNumber; + // CRITICAL: Pending call data for background acceptance + Map? _pendingCallData; + + // CRITICAL FIX: WebRTC initialization synchronization + Completer? _webrtcInitCompleter; + String? _pendingOfferSdp; // Queue for offer that arrives during initialization + // Getters CallSession? get currentCall => _currentCall; CallStatus get callStatus => _callStatus; @@ -70,7 +80,6 @@ class CallManager extends ChangeNotifier { bool get isCameraOn => _isCameraOn; bool get isPeerMuted => _isPeerMuted; bool get isPeerCameraOn => _isPeerCameraOn; - WebRTCService? get webrtcService => _webrtcService; bool get isCallInProgress => _callStatus != CallStatus.idle; /// Initialize CallManager with user credentials @@ -384,6 +393,20 @@ class CallManager extends ChangeNotifier { log(' Caller: $callerName ($callerId)', 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 // This handles the case when app is in background and context is null if (_userId == null || _authToken == null) { @@ -551,7 +574,48 @@ class CallManager extends ChangeNotifier { try { log('═══════════════════════════════════════════', 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) { log('âš ī¸ [ACCEPT] No incoming call to accept', name: 'CallManager'); @@ -576,19 +640,30 @@ class CallManager extends ChangeNotifier { } log('✅ [ACCEPT] Permissions granted', name: 'CallManager'); - // Ensure SignalR connected + // CRITICAL: Ensure SignalR connected before accepting log('🔌 [ACCEPT] Ensuring SignalR connection...', name: 'CallManager'); log(' Current SignalR state: ${_signalRService.connectionState}', name: 'CallManager'); if (!await _signalRService.ensureConnected()) { log('❌ [ACCEPT] SignalR connection failed', name: 'CallManager'); log(' Final state: ${_signalRService.connectionState}', name: 'CallManager'); + + final context = navigatorKey.currentContext; + if (context != null) { + CallErrorHandler.showSignalRNotConnected(context); + } _cleanup(); return; } log('✅ [ACCEPT] SignalR connected', 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); // Cancel timeout timer @@ -611,14 +686,15 @@ class CallManager extends ChangeNotifier { _conversationId ?? '', ]); 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 log('🔧 [ACCEPT] Initializing WebRTC...', name: 'CallManager'); log(' Call type: ${_currentCall!.type.name}', name: 'CallManager'); await _initializeWebRTC(); log('✅ [ACCEPT] WebRTC initialized', name: 'CallManager'); - log(' WebRTC service: ${_webrtcService != null ? "Created" : "NULL"}', name: 'CallManager'); // Navigate to call screen 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] 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'); } catch (e, stackTrace) { log('❌ [ACCEPT] Error accepting call: $e', name: 'CallManager', error: e, stackTrace: stackTrace); _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(' From: ${_myEmployeeNumber ?? ""}', name: 'CallManager'); log(' To: $peerId', name: 'CallManager'); + log(' Module ID: ${_moduleId ?? "1"}', name: 'CallManager'); - // CRITICAL FIX: HangUpAsync only takes 2 parameters (from, to) - // The old code was sending moduleId as 3rd parameter which caused: - // "Failed to invoke 'HangUpAsync' due to an error on the server" + // CRITICAL FIX: Backend HangUpAsync expects 3 parameters: + // 1. fromEmployeeNumber (string) + // 2. toEmployeeNumber (string) + // 3. moduleId (int or string) await _signalRService.invoke('HangUpAsync', args: [ _myEmployeeNumber ?? '', peerId, + _moduleId ?? '1', // Add moduleId as 3rd parameter ]); log('✅ [HANGUP] HangUpAsync invoked successfully', name: 'CallManager'); @@ -795,7 +871,7 @@ class CallManager extends ChangeNotifier { log('🎤 [CONTROL] Toggling mute: $_isMuted -> ${!_isMuted}', name: 'CallManager'); _isMuted = !_isMuted; - _webrtcService?.setMicrophoneMuted(_isMuted); + _webrtcService.setMicrophoneMuted(_isMuted); if (await _signalRService.ensureConnected()) { await _signalRService.invoke('AudioToggle', args: [ @@ -813,7 +889,7 @@ class CallManager extends ChangeNotifier { log('🔊 [CONTROL] Toggling speaker: $_isSpeakerOn -> ${!_isSpeakerOn}', name: 'CallManager'); _isSpeakerOn = !_isSpeakerOn; - await _webrtcService?.setSpeakerphoneEnabled(_isSpeakerOn); + await _webrtcService.setSpeakerphoneEnabled(_isSpeakerOn); notifyListeners(); log('✅ [CONTROL] Speaker toggled: $_isSpeakerOn', name: 'CallManager'); @@ -826,7 +902,7 @@ class CallManager extends ChangeNotifier { log('📹 [CONTROL] Toggling camera: $_isCameraOn -> ${!_isCameraOn}', name: 'CallManager'); _isCameraOn = !_isCameraOn; - _webrtcService?.setCameraEnabled(_isCameraOn); + _webrtcService.setCameraEnabled(_isCameraOn); if (await _signalRService.ensureConnected()) { await _signalRService.invoke('CameraToggle', args: [ @@ -844,7 +920,7 @@ class CallManager extends ChangeNotifier { if (_currentCall?.type != CallType.video || !_isCameraOn) return; log('🔄 [CONTROL] Switching camera...', name: 'CallManager'); - await _webrtcService?.switchCamera(); + await _webrtcService.switchCamera(); log('✅ [CONTROL] Camera switched', name: 'CallManager'); } @@ -882,32 +958,92 @@ class CallManager extends ChangeNotifier { Future _initializeWebRTC() async { log('🔧 [WEBRTC] Initializing WebRTC...', name: 'CallManager'); + log(' Current call type: ${_currentCall!.type.name}', name: 'CallManager'); + log(' WebRTC service hashCode: ${_webrtcService.hashCode}', name: 'CallManager'); - _webrtcService = WebRTCService(); - _setupWebRTCCallbacks(); + // CRITICAL FIX: Create a completer to track initialization completion + _webrtcInitCompleter = Completer(); - if (_currentCall!.type == CallType.audio) { - log('đŸŽĩ [WEBRTC] Initializing for audio call', name: 'CallManager'); - await _webrtcService!.initializeForAudioCall(); + try { + // CRITICAL FIX: Setup callbacks BEFORE initializing WebRTC + log('🔧 [WEBRTC] Setting up callbacks BEFORE initialization...', name: 'CallManager'); + _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) { + log('đŸŽĩ [WEBRTC] Initializing for audio call', name: 'CallManager'); + await _webrtcService.initializeForAudioCall(); + + // CRITICAL FIX: Speaker is OFF by default in WebRTC service (earpiece mode) + // Update the CallManager state to reflect this + _isSpeakerOn = false; + log('✅ [WEBRTC] Speaker state synchronized: OFF (earpiece mode)', name: 'CallManager'); + notifyListeners(); + } else { + log('📹 [WEBRTC] Initializing for video call', name: 'CallManager'); + await _webrtcService.initializeForVideoCall(); + } - // CRITICAL FIX: Speaker is OFF by default in WebRTC service (earpiece mode) - // Update the CallManager state to reflect this - _isSpeakerOn = false; - log('✅ [WEBRTC] Speaker state synchronized: OFF (earpiece mode)', name: 'CallManager'); - notifyListeners(); - } else { - log('📹 [WEBRTC] Initializing for video call', name: 'CallManager'); - 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'); + + // 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); + } - log('✅ [WEBRTC] WebRTC initialized', name: 'CallManager'); + rethrow; + } } void _setupWebRTCCallbacks() { 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) { - log('🧊 [WEBRTC] ICE candidate generated', name: 'CallManager'); + _webrtcService.onIceCandidate = (RTCIceCandidate candidate) { + log('🧊 [WEBRTC-CALLBACK] ICE candidate generated in CallManager', name: 'CallManager'); log(' Candidate: ${candidate.candidate?.substring(0, 50)}...', name: 'CallManager'); final candidateJson = jsonEncode({ @@ -923,29 +1059,43 @@ class CallManager extends ChangeNotifier { ]); }; - _webrtcService!.onRemoteStream = (MediaStream stream) { - log('📡 [WEBRTC] Remote stream received', name: 'CallManager'); + _webrtcService.onRemoteStream = (MediaStream stream) { + log('📡 [WEBRTC-CALLBACK] Remote stream received in CallManager', name: 'CallManager'); log(' Audio tracks: ${stream.getAudioTracks().length}', name: 'CallManager'); log(' Video tracks: ${stream.getVideoTracks().length}', name: 'CallManager'); notifyListeners(); }; - _webrtcService!.onIceConnectionStateChange = (RTCIceConnectionState state) { - log('🔗 [WEBRTC] ICE state: ${state.toString()}', name: 'CallManager'); + _webrtcService.onIceConnectionStateChange = (RTCIceConnectionState state) { + 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) { - log('✅ [WEBRTC] ICE connection established', name: 'CallManager'); + log('✅ [WEBRTC-CALLBACK] ICE connection established - updating status to CONNECTED', name: 'CallManager'); _updateCallStatus(CallStatus.connected); _startCallDurationTimer(); + log('✅ [WEBRTC-CALLBACK] Status updated and timer started', name: 'CallManager'); } else if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) { - log('❌ [WEBRTC] ICE connection failed', name: 'CallManager'); - _cleanup(); + log('❌ [WEBRTC-CALLBACK] ICE connection failed', name: 'CallManager'); + // CRITICAL FIX: Don't cleanup immediately - try to notify peer first + _handleIceConnectionFailure(); } 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(' 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() { @@ -992,8 +1142,8 @@ class CallManager extends ChangeNotifier { _callTimeoutTimer?.cancel(); _callDurationTimer?.cancel(); - _webrtcService?.dispose(); - _webrtcService = null; + // Dispose WebRTC service (it's a singleton, just cleanup the peer connection) + _webrtcService.dispose(); if (_currentCall != null) { _callKitService.endCall(_currentCall!.callId); @@ -1080,20 +1230,9 @@ class CallManager extends ChangeNotifier { try { // 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 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(' Offer SDP type: ${offer.type}', name: 'CallManager'); @@ -1208,7 +1347,6 @@ class CallManager extends ChangeNotifier { log('📞 [EVENT] đŸ“Ĩ OnOfferAsync received', name: 'CallManager'); log(' Args count: ${args?.length ?? 0}', name: 'CallManager'); log(' Current status: ${_callStatus.name}', name: 'CallManager'); - log(' WebRTC initialized: ${_webrtcService != null}', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager'); if (args == null || args.isEmpty) { @@ -1225,13 +1363,21 @@ class CallManager extends ChangeNotifier { log(' Offer SDP length: ${offerSdp.length}', name: 'CallManager'); - if (_webrtcService == null) { - log('âš ī¸ [EVENT] WebRTC service not initialized', name: 'CallManager'); + // CRITICAL FIX: Wait for WebRTC initialization to complete before processing offer + 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; } 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(' Answer SDP type: ${answer.type}', name: 'CallManager'); @@ -1274,11 +1420,6 @@ class CallManager extends ChangeNotifier { 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'); await _webrtcService!.setRemoteAnswer(answerSdp); log('✅ [EVENT] Remote answer set successfully', name: 'CallManager'); @@ -1305,11 +1446,6 @@ class CallManager extends ChangeNotifier { return; } - if (_webrtcService == null) { - log('âš ī¸ [EVENT] WebRTC service not initialized', name: 'CallManager'); - return; - } - final candidateData = jsonDecode(candidateJson) as Map; final candidate = RTCIceCandidate( candidateData['candidate'] as String?, @@ -1319,7 +1455,7 @@ class CallManager extends ChangeNotifier { log('🧊 [EVENT] Adding remote ICE candidate...', 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'); } catch (e, stackTrace) { @@ -1437,4 +1573,53 @@ class CallManager extends ChangeNotifier { } log('═══════════════════════════════════════════', name: 'CallManager'); } + + // ==================== ICE Connection Failure Handlers ==================== + + /// Handle ICE connection failure + Future _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 _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'); + } + } }