import 'dart:async'; import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; /// WebRTC service for handling peer-to-peer audio/video connections class WebRTCService { // Peer connection RTCPeerConnection? _peerConnection; // Media streams MediaStream? _localStream; MediaStream? _remoteStream; // Video renderers (for future video support) RTCVideoRenderer? localRenderer; RTCVideoRenderer? remoteRenderer; // Callbacks Function(MediaStream stream)? onLocalStream; Function(MediaStream stream)? onRemoteStream; Function(RTCIceCandidate candidate)? onIceCandidate; Function(RTCIceConnectionState state)? onIceConnectionStateChange; Function()? onCallEnded; // ICE candidate queue (for candidates received before remote description) 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; // Track if we're using test mode (Google STUN only) static bool _useTestMode = false; /// Check if WebRTC is fully initialized bool get isFullyInitialized => _isFullyInitialized; /// Check if peer connection is ready bool get isPeerConnectionInitialized => _peerConnection != null && _isFullyInitialized; /// Wait for WebRTC initialization to complete Future waitForInitialization() async { if (_isFullyInitialized) { return; } if (_initializationCompleter != null) { await _initializationCompleter!.future; } } /// Enable test mode (use only Google STUN servers for debugging) static void enableTestMode() { _useTestMode = true; } /// Disable test mode (use backend TURN servers) static void disableTestMode() { _useTestMode = false; } // Configuration static const Map _mediaConstraints = { 'audio': true, 'video': false, }; // Video constraints for video calls static const Map _videoConstraints = { 'audio': true, 'video': { 'facingMode': 'user', 'width': {'ideal': 1280}, 'height': {'ideal': 720}, }, }; // ICE servers configuration for TEST MODE (Google STUN only) static const Map _testConfiguration = { 'iceServers': [ { 'urls': [ 'stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302', 'stun:stun2.l.google.com:19302', ] }, ], 'sdpSemantics': 'unified-plan', 'iceTransportPolicy': 'all', 'iceCandidatePoolSize': 10, }; // ICE servers configuration (from backend) final Map _configuration = { 'iceServers': [ // Primary STUN servers (Google's public STUN - highly reliable) { 'urls': [ 'stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302', ] }, // Backend TURN server (PRIMARY) { 'urls': [ 'turn:15.185.116.59:3479', 'turn:15.185.116.59:3479?transport=tcp', ], 'username': 'admin', 'credential': 'admin' }, // FALLBACK: Public TURN servers { 'urls': 'turn:a.relay.metered.ca:80', 'username': 'e14d93f87f9ff517f1bee797', 'credential': 'PGfCGmR6CR2aM9OY', }, { 'urls': 'turn:a.relay.metered.ca:443', 'username': 'e14d93f87f9ff517f1bee797', 'credential': 'PGfCGmR6CR2aM9OY', }, ], 'sdpSemantics': 'unified-plan', 'iceTransportPolicy': 'all', 'iceCandidatePoolSize': 10, 'bundlePolicy': 'max-bundle', 'rtcpMuxPolicy': 'require', }; /// Initialize WebRTC for audio call Future initializeForAudioCall() async { _initializationCompleter = Completer(); _isFullyInitialized = false; try { _isVideoCall = false; try { _localStream = await navigator.mediaDevices.getUserMedia(_mediaConstraints); final audioTracks = _localStream!.getAudioTracks(); for (var i = 0; i < audioTracks.length; i++) { final track = audioTracks[i]; if (!track.enabled) { track.enabled = true; } } if (audioTracks.isEmpty) { throw Exception('No audio tracks captured from microphone'); } } catch (e) { throw Exception('Microphone access denied or unavailable'); } _peerConnection = await createPeerConnection(_configuration); if (_peerConnection == null) { throw Exception('Failed to create peer connection'); } final tracks = _localStream!.getTracks(); for (var i = 0; i < tracks.length; i++) { final track = tracks[i]; await _peerConnection!.addTrack(track, _localStream!); } _setupPeerConnectionListeners(); try { await Helper.setSpeakerphoneOn(false); } catch (e) { // Ignore speakerphone errors } _isFullyInitialized = true; if (!_initializationCompleter!.isCompleted) { _initializationCompleter!.complete(); } } catch (e, stackTrace) { log('❌ [WebRTC] Initialization error: $e\nStack: $stackTrace', name: 'WebRTCService'); if (_initializationCompleter != null && !_initializationCompleter!.isCompleted) { _initializationCompleter!.completeError(e, stackTrace); } _isFullyInitialized = false; await dispose(); rethrow; } } /// Initialize WebRTC for video call /// Initialize WebRTC for video call Future initializeForVideoCall() async { _initializationCompleter = Completer(); _isFullyInitialized = false; try { _isVideoCall = true; // Initialize video renderers localRenderer = RTCVideoRenderer(); remoteRenderer = RTCVideoRenderer(); await localRenderer!.initialize(); await remoteRenderer!.initialize(); // Get camera + microphone stream first try { _localStream = await navigator.mediaDevices.getUserMedia(_videoConstraints); } catch (e) { log( '❌ [WebRTC] Failed to get video stream: $e', name: 'WebRTCService', ); throw Exception('Camera or microphone access denied or unavailable'); } if (_localStream == null) { throw Exception('Local video stream is null'); } // Attach local stream to renderer localRenderer!.srcObject = _localStream; // Create peer connection final config = _useTestMode ? _testConfiguration : _configuration; _peerConnection = await createPeerConnection(config); if (_peerConnection == null) { throw Exception('Failed to create peer connection'); } // Setup listeners _setupPeerConnectionListeners(); // Add local tracks final tracks = _localStream!.getTracks(); if (tracks.isEmpty) { throw Exception('No media tracks found'); } for (final track in tracks) { await _peerConnection!.addTrack( track, _localStream!, ); } _isFullyInitialized = true; if (!_initializationCompleter!.isCompleted) { _initializationCompleter!.complete(); } log( '✅ [WebRTC] Video initialization completed', name: 'WebRTCService', ); } catch (e, stackTrace) { log( '❌ [WebRTC] Video initialization error: $e\nStack: $stackTrace', name: 'WebRTCService', error: e, stackTrace: stackTrace, ); if (_initializationCompleter != null && !_initializationCompleter!.isCompleted) { _initializationCompleter!.completeError( e, stackTrace, ); } _isFullyInitialized = false; await dispose(); rethrow; } } /// Setup peer connection event listeners void _setupPeerConnectionListeners() { _peerConnection!.onIceCandidate = (RTCIceCandidate candidate) { onIceCandidate?.call(candidate); }; _peerConnection!.onIceGatheringState = (RTCIceGatheringState state) { // Ice gathering state changed }; _peerConnection!.onIceConnectionState = (RTCIceConnectionState state) { onIceConnectionStateChange?.call(state); }; _peerConnection!.onTrack = (RTCTrackEvent event) { if (event.streams.isNotEmpty) { final stream = event.streams[0]; if (_remoteStream == null || _remoteStream!.id != stream.id) { _remoteStream = stream; if (remoteRenderer != null && _isVideoCall) { try { remoteRenderer!.srcObject = _remoteStream; Future.delayed(const Duration(milliseconds: 100), () { if (remoteRenderer != null && remoteRenderer!.srcObject == null && _remoteStream != null) { remoteRenderer!.srcObject = _remoteStream; } }); Future.delayed(const Duration(milliseconds: 300), () { if (remoteRenderer != null && remoteRenderer!.srcObject == null && _remoteStream != null) { remoteRenderer!.srcObject = _remoteStream; } }); } catch (e) { // Ignore errors } } onRemoteStream?.call(_remoteStream!); } else { if (remoteRenderer != null && _isVideoCall && remoteRenderer!.srcObject == null && _remoteStream != null) { try { remoteRenderer!.srcObject = _remoteStream; } catch (e) { // Ignore errors } } onRemoteStream?.call(_remoteStream!); } } }; _peerConnection!.onConnectionState = (RTCPeerConnectionState state) { if (state == RTCPeerConnectionState.RTCPeerConnectionStateFailed) { log('🔌 [WebRTC] Connection state: ${state.toString()}', name: 'WebRTCService'); } }; } /// Create SDP offer (caller side) Future createOffer() async { try { if (_peerConnection == null) { throw Exception('Peer connection not initialized'); } final offer = await _peerConnection!.createOffer({ 'offerToReceiveAudio': true, 'offerToReceiveVideo': _isVideoCall, 'iceRestart': false, }); await _peerConnection!.setLocalDescription(offer); return offer; } catch (e, stackTrace) { log('❌ [WebRTC] Error creating offer: $e\nStack: $stackTrace', name: 'WebRTCService'); rethrow; } } /// Create SDP answer (callee side) Future createAnswer(String offerSdp) async { try { if (_peerConnection == null) { throw Exception('Peer connection not initialized'); } final offer = RTCSessionDescription(offerSdp, 'offer'); await _peerConnection!.setRemoteDescription(offer); _remoteDescriptionSet = true; await _flushIceCandidateQueue(); final answer = await _peerConnection!.createAnswer({ 'offerToReceiveAudio': true, 'offerToReceiveVideo': _isVideoCall, }); await _peerConnection!.setLocalDescription(answer); return answer; } catch (e, stackTrace) { log('❌ [WebRTC] Error creating answer: $e\nStack: $stackTrace', name: 'WebRTCService'); rethrow; } } /// Set remote answer (caller side) Future setRemoteAnswer(String answerSdp) async { try { if (_peerConnection == null) { throw Exception('Peer connection not initialized'); } final answer = RTCSessionDescription(answerSdp, 'answer'); await _peerConnection!.setRemoteDescription(answer); _remoteDescriptionSet = true; await _flushIceCandidateQueue(); } catch (e, stackTrace) { log('❌ [WebRTC] Error setting remote answer: $e\nStack: $stackTrace', name: 'WebRTCService'); rethrow; } } /// Send SDP answer (convenience method for callee) Future sendSDPAnswer(String? offerSdp) async { if (offerSdp == null) { throw Exception('Offer SDP is null'); } return await createAnswer(offerSdp); } /// Add remote ICE candidate Future addIceCandidate(RTCIceCandidate candidate) async { try { // If peer connection not created yet, queue the candidate if (_peerConnection == null) { _iceCandidateQueue.add(candidate); return; } // If remote description not set yet, queue the candidate if (!_remoteDescriptionSet) { _iceCandidateQueue.add(candidate); return; } await _peerConnection!.addCandidate(candidate); } catch (e) { log('⚠️ [WebRTC] Error adding ICE candidate: $e', name: 'WebRTCService'); // Don't rethrow - ICE candidate errors shouldn't break the call } } /// Flush queued ICE candidates Future _flushIceCandidateQueue() async { if (_iceCandidateQueue.isEmpty) { return; } for (final candidate in _iceCandidateQueue) { try { await _peerConnection!.addCandidate(candidate); } catch (e) { log('⚠️ [WebRTC] Error adding queued candidate: $e', name: 'WebRTCService'); } } _iceCandidateQueue.clear(); } /// Toggle microphone mute void setMicrophoneMuted(bool muted) { if (_localStream != null) { final audioTracks = _localStream!.getAudioTracks(); for (final track in audioTracks) { track.enabled = !muted; } } } /// Toggle camera on/off for video calls void setCameraEnabled(bool enabled) { if (_localStream != null) { final videoTracks = _localStream!.getVideoTracks(); for (final track in videoTracks) { track.enabled = enabled; } } } /// Switch between front and rear camera Future switchCamera() async { if (_localStream != null) { final videoTracks = _localStream!.getVideoTracks(); if (videoTracks.isNotEmpty) { try { await Helper.switchCamera(videoTracks.first); } catch (e) { log('❌ [WebRTC] Error switching camera: $e', name: 'WebRTCService'); } } } } /// Enable/disable speakerphone Future setSpeakerphoneEnabled(bool enabled) async { try { await Helper.setSpeakerphoneOn(enabled); } catch (e) { log('❌ [WebRTC] Error setting speakerphone: $e', name: 'WebRTCService'); } } /// Get local stream MediaStream? get localStream => _localStream; /// Get remote stream MediaStream? get remoteStream => _remoteStream; /// Check if remote stream has video tracks bool get hasRemoteVideo { if (_remoteStream == null) return false; return _remoteStream!.getVideoTracks().isNotEmpty; } /// Force refresh remote renderer void refreshRemoteRenderer() { if (_remoteStream != null && remoteRenderer != null && _isVideoCall) { if (remoteRenderer!.srcObject == null) { remoteRenderer!.srcObject = _remoteStream; } } } /// Restart ICE negotiation (used when connection fails) Future restartIce() async { try { if (_peerConnection == null) { return null; } final offer = await _peerConnection!.createOffer({ 'offerToReceiveAudio': true, 'offerToReceiveVideo': _isVideoCall, 'iceRestart': true, }); await _peerConnection!.setLocalDescription(offer); return offer; } catch (e) { log('❌ [WebRTC] ICE restart failed: $e', name: 'WebRTCService'); return null; } } /// Get peer connection state RTCPeerConnectionState? getPeerConnectionState() { return _peerConnection?.connectionState; } /// Get ICE connection state RTCIceConnectionState? getIceConnectionState() { return _peerConnection?.iceConnectionState; } /// Dispose and cleanup Future dispose() async { try { if (_localStream != null) { _localStream!.getTracks().forEach((track) { track.stop(); }); await _localStream!.dispose(); _localStream = null; } if (_remoteStream != null) { _remoteStream!.getTracks().forEach((track) { track.stop(); }); await _remoteStream!.dispose(); _remoteStream = null; } if (localRenderer != null) { await localRenderer!.dispose(); localRenderer = null; } if (remoteRenderer != null) { await remoteRenderer!.dispose(); remoteRenderer = null; } if (_peerConnection != null) { await _peerConnection!.close(); await _peerConnection!.dispose(); _peerConnection = null; } _iceCandidateQueue.clear(); _remoteDescriptionSet = false; } catch (e) { log('⚠️ [WebRTC] Error during dispose: $e', name: 'WebRTCService'); } } }