foreground calling implemented and working fine

ui_ux_rollout_merge_audio_video_call
WaseemAbbasi22 4 weeks ago
parent ea0087e6f9
commit 84fe851ec8

@ -1,19 +1,28 @@
import 'package:get_it/get_it.dart'; 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/services/signalr_service.dart';
import 'package:test_sa/modules/cx_module/chat/call/services/webrtc_service.dart';
/// Global service locator instance /// Global service locator instance
final getIt = GetIt.instance; final getIt = GetIt.instance;
/// Setup dependency injection /// Setup dependency injection
Future<void> setupServiceLocator() async { Future<void> setupServiceLocator() async {
// SignalR Service - ONLY ONE instance for the entire app // SignalR Service - ONLY ONE instance for the entire app
getIt.registerLazySingleton<SignalRService>( getIt.registerLazySingleton<SignalRService>(
() => SignalRService(), () => SignalRService(),
); );
// WebRTC Service - ONLY ONE instance for the entire app
getIt.registerLazySingleton<WebRTCService>(
() => WebRTCService(),
);
print('✅ [DI] Service locator initialized'); print('✅ [DI] Service locator initialized');
print(' - SignalRService registered as singleton (hashCode: ${getIt<SignalRService>().hashCode})'); print(' - SignalRService registered as singleton (hashCode: ${getIt<SignalRService>().hashCode})');
print(' - WebRTCService registered as singleton (hashCode: ${getIt<WebRTCService>().hashCode})');
} }
Future<void> resetServices() async { Future<void> resetServices() async {
await getIt<SignalRService>().reset(); await getIt<SignalRService>().reset();
print('✅ [DI] All services reset'); print('✅ [DI] All services reset');
} }

@ -361,8 +361,8 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => CleanerProvider(), lazy: true), ChangeNotifierProvider(create: (_) => CleanerProvider(), lazy: true),
// EXPORT MODULE PROVIDERS (3) // EXPORT MODULE PROVIDERS (3)
ChangeNotifierProvider(create: (_) => AssetColumnsProvider(), lazy: true), ChangeNotifierProvider(create: (_) => AssetColumnsProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => ExportFormatProvider(), lazy: true), ChangeNotifierProvider(create: (_) => ExportFormatProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => ExportApiProvider(), lazy: true), ChangeNotifierProvider(create: (_) => ExportApiProvider(), lazy: true),
// UTILITY PROVIDERS (1) // UTILITY PROVIDERS (1)

@ -27,6 +27,10 @@ class WebRTCService {
final List<RTCIceCandidate> _iceCandidateQueue = []; final List<RTCIceCandidate> _iceCandidateQueue = [];
bool _remoteDescriptionSet = false; bool _remoteDescriptionSet = false;
// CRITICAL FIX: Atomic initialization tracking
Completer<void>? _initializationCompleter;
bool _isFullyInitialized = false;
// Track if current call is video call // Track if current call is video call
bool _isVideoCall = false; bool _isVideoCall = false;
@ -123,10 +127,12 @@ class WebRTCService {
/// Initialize WebRTC for audio call /// Initialize WebRTC for audio call
Future<void> initializeForAudioCall() async { Future<void> initializeForAudioCall() async {
// CRITICAL FIX: Create completer and mark as not initialized
_initializationCompleter = Completer<void>();
_isFullyInitialized = false;
try { try {
if (kDebugMode) { log('🔧 [WebRTC] Initialization Started', name: 'WebRTCService');
log('🔧 [WebRTC] Initializing audio call', name: 'WebRTCService');
}
_isVideoCall = false; _isVideoCall = false;
@ -186,22 +192,31 @@ class WebRTCService {
// Setup peer connection event handlers // Setup peer connection event handlers
_setupPeerConnectionListeners(); _setupPeerConnectionListeners();
// CRITICAL FIX: Keep speakerphone OFF by default for audio calls // CRITICAL FIX: Set speakerphone OFF by default (use earpiece)
// User can manually enable it if needed via the speaker button log('🔇 [WebRTC] Setting speakerphone OFF by default (earpiece mode)...', name: 'WebRTCService');
log('🔇 [WebRTC] Keeping speakerphone OFF by default for audio call...', name: 'WebRTCService');
try { try {
await Helper.setSpeakerphoneOn(false); await Helper.setSpeakerphoneOn(false);
log('✅ [WebRTC] Speakerphone disabled (earpiece mode)', name: 'WebRTCService'); log('✅ [WebRTC] Speakerphone disabled (earpiece mode)', name: 'WebRTCService');
} catch (e) { } catch (e) {
log('⚠️ [WebRTC] Failed to disable speakerphone: $e', name: 'WebRTCService'); log('⚠️ [WebRTC] Failed to disable speakerphone: $e', name: 'WebRTCService');
// Continue anyway - not critical
} }
if (kDebugMode) { // CRITICAL FIX: Mark as fully initialized AFTER everything is ready
log('✅ [WebRTC] Audio call initialized', name: 'WebRTCService'); _isFullyInitialized = true;
if (!_initializationCompleter!.isCompleted) {
_initializationCompleter!.complete();
} }
log('✅ [WebRTC] Initialization Completed - PeerConnection Ready', name: 'WebRTCService');
} catch (e, stackTrace) { } catch (e, stackTrace) {
log('❌ [WebRTC] Initialization error: $e', name: 'WebRTCService', error: e, stackTrace: 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 // Clean up any partial initialization
await dispose(); await dispose();
rethrow; rethrow;
@ -271,33 +286,50 @@ class WebRTCService {
/// Setup peer connection event listeners /// Setup peer connection event listeners
void _setupPeerConnectionListeners() { void _setupPeerConnectionListeners() {
log('🔧 [WebRTC] Setting up peer connection listeners...', name: 'WebRTCService');
// Handle ICE candidates // Handle ICE candidates
_peerConnection!.onIceCandidate = (RTCIceCandidate candidate) { _peerConnection!.onIceCandidate = (RTCIceCandidate candidate) {
log('🧊 [WebRTC] onIceCandidate triggered', name: 'WebRTCService');
if (onIceCandidate != null) { if (onIceCandidate != null) {
onIceCandidate!(candidate); onIceCandidate!(candidate);
} else {
log('⚠️ [WebRTC] onIceCandidate callback is NULL!', name: 'WebRTCService');
} }
}; };
// Handle ICE gathering state changes // Handle ICE gathering state changes
_peerConnection!.onIceGatheringState = (RTCIceGatheringState state) { _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'); log('✅ [WebRTC] ICE gathering completed', name: 'WebRTCService');
} }
}; };
// Handle ICE connection state changes // Handle ICE connection state changes
_peerConnection!.onIceConnectionState = (RTCIceConnectionState state) { _peerConnection!.onIceConnectionState = (RTCIceConnectionState state) {
if (kDebugMode) { log('═══════════════════════════════════════════', name: 'WebRTCService');
log('🔗 [WebRTC] ICE state: ${state.toString()}', 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 // Log critical failures
if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) { if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) {
log('❌ [WebRTC] ICE connection failed', name: 'WebRTCService'); log('❌ [WebRTC] ICE connection failed', name: 'WebRTCService');
} }
if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) {
log('✅ [WebRTC] ICE connection CONNECTED!', name: 'WebRTCService');
}
if (onIceConnectionStateChange != null) { if (onIceConnectionStateChange != null) {
log('📤 [WebRTC] Forwarding ICE state to callback...', name: 'WebRTCService');
onIceConnectionStateChange!(state); 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) { _peerConnection!.onTrack = (RTCTrackEvent event) {
log('═══════════════════════════════════════════', name: 'WebRTCService'); log('═══════════════════════════════════════════', name: 'WebRTCService');
log('📡 [WebRTC] onTrack event received!', name: 'WebRTCService'); log('📡 [WebRTC] onTrack event received!', name: 'WebRTCService');
log(' Track kind: ${event.track?.kind}', name: 'WebRTCService'); log(' Track kind: ${event.track.kind}', name: 'WebRTCService');
log(' Track ID: ${event.track?.id}', name: 'WebRTCService'); log(' Track ID: ${event.track.id}', name: 'WebRTCService');
log(' Track enabled: ${event.track?.enabled}', name: 'WebRTCService'); log(' Track enabled: ${event.track.enabled}', name: 'WebRTCService');
log(' Track muted: ${event.track?.muted}', name: 'WebRTCService'); log(' Track muted: ${event.track.muted}', name: 'WebRTCService');
// log(' Track readyState: ${event.track?.readyState}', name: 'WebRTCService'); // log(' Track readyState: ${event.track.readyState}', name: 'WebRTCService');
log(' Streams count: ${event.streams.length}', name: 'WebRTCService'); log(' Streams count: ${event.streams.length}', name: 'WebRTCService');
if (event.streams.isNotEmpty) { if (event.streams.isNotEmpty) {
@ -718,6 +750,18 @@ class WebRTCService {
} }
/// Get peer connection state /// 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<void> 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() { RTCPeerConnectionState? getPeerConnectionState() {
return _peerConnection?.connectionState; return _peerConnection?.connectionState;
} }

@ -348,16 +348,18 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
log('🔌 [ChatProvider] buildHubConnection called', name: 'ChatProvider'); log('🔌 [ChatProvider] buildHubConnection called', name: 'ChatProvider');
log('💬 Conversation ID: $conversationID', 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; final signalRService = _signalRService;
log('🔍 [ChatProvider] SignalR service instance: ${signalRService.hashCode}', name: 'ChatProvider'); log('🔍 [ChatProvider] SignalR service instance: ${signalRService.hashCode}', name: 'ChatProvider');
log(' Is connected: ${signalRService.isConnected}', name: 'ChatProvider'); log(' Is connected: ${signalRService.isConnected}', name: 'ChatProvider');
log(' Connection state: ${signalRService.connectionState}', 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) { if (!signalRService.isConnected) {
log('🔌 [ChatProvider] Initializing SignalR connection...', name: 'ChatProvider'); log('🔌 [ChatProvider] SignalR not connected, initializing...', name: 'ChatProvider');
final connected = await signalRService.initialize( final connected = await signalRService.initialize(
userId: chatLoginResponse!.userId.toString(), userId: chatLoginResponse!.userId.toString(),
@ -372,17 +374,29 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
log('✅ [ChatProvider] SignalR initialized', name: 'ChatProvider'); log('✅ [ChatProvider] SignalR initialized', name: 'ChatProvider');
log(' Connection ID: ${signalRService.connectionId}', name: 'ChatProvider'); log(' Connection ID: ${signalRService.connectionId}', name: 'ChatProvider');
} else { } 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'); 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) { if (conversationID.isNotEmpty) {
await signalRService.invoke("JoinConversation", args: [conversationID]); try {
log('✅ [ChatProvider] Joined conversation: $conversationID', name: 'ChatProvider'); 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; chatHubConnection = signalRService.hubConnection;
// Log user details // Log user details
@ -399,8 +413,8 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
if (kDebugMode) { if (kDebugMode) {
print('⚠️ Error building SignalR connection: $e'); print('⚠️ Error building SignalR connection: $e');
} }
// Clean up on error // CRITICAL FIX: Don't dispose on error - just log and continue
await _disposeConnection(); // The connection might still be usable for calls
rethrow; rethrow;
} }
} }

@ -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,32 +958,92 @@ 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');
_webrtcService = WebRTCService(); // CRITICAL FIX: Create a completer to track initialization completion
_setupWebRTCCallbacks(); _webrtcInitCompleter = Completer<void>();
if (_currentCall!.type == CallType.audio) { try {
log('🎵 [WEBRTC] Initializing for audio call', name: 'CallManager'); // CRITICAL FIX: Setup callbacks BEFORE initializing WebRTC
await _webrtcService!.initializeForAudioCall(); 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) // Verify callback is still set after initialization
// Update the CallManager state to reflect this log('🔍 [WEBRTC] Post-initialization callback verification...', name: 'CallManager');
_isSpeakerOn = false; log(' onIceConnectionStateChange is null?: ${_webrtcService.onIceConnectionStateChange == null}', name: 'CallManager');
log('✅ [WEBRTC] Speaker state synchronized: OFF (earpiece mode)', name: 'CallManager'); log(' Peer connection initialized?: ${_webrtcService.isPeerConnectionInitialized}', name: 'CallManager');
notifyListeners();
} else { log('✅ [WEBRTC] WebRTC initialized', name: 'CallManager');
log('📹 [WEBRTC] Initializing for video call', name: 'CallManager');
await _webrtcService!.initializeForVideoCall(); // 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() { 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');
}
}
} }

Loading…
Cancel
Save