import 'dart:async'; import 'dart:developer'; import 'package:flutter_webrtc/flutter_webrtc.dart'; 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; final List _iceCandidateQueue = []; bool _remoteDescriptionSet = false; Completer? _initializationCompleter; bool _isFullyInitialized = false; bool _isVideoCall = false; static bool _useTestMode = false; bool get isFullyInitialized => _isFullyInitialized; bool get isPeerConnectionInitialized => _peerConnection != null && _isFullyInitialized; Future waitForInitialization() async { if (_isFullyInitialized) { return; } if (_initializationCompleter != null) { await _initializationCompleter!.future; } } static void enableTestMode() { _useTestMode = true; } static void disableTestMode() { _useTestMode = false; } static const Map _mediaConstraints = { 'audio': true, 'video': false, }; static const Map _videoConstraints = { 'audio': true, 'video': { 'facingMode': 'user', 'width': {'ideal': 1280}, 'height': {'ideal': 720}, }, }; 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, }; //Need to move the to safe place.. final Map _configuration = { 'iceServers': [ { '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 { if (_isFullyInitialized) { return; } _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', ); } _setupPeerConnectionListeners(); final tracks = _localStream!.getTracks(); for (var i = 0; i < tracks.length; i++) { final track = tracks[i]; await _peerConnection!.addTrack( track, _localStream!, ); } try { await Helper.setSpeakerphoneOn(false); } catch (e) {} _isFullyInitialized = true; if (!_initializationCompleter!.isCompleted) { _initializationCompleter!.complete(); } } catch (e, stackTrace) { log('[WebRTC] Audio init error: $e', name: 'WebRTCService'); if (_initializationCompleter != null && !_initializationCompleter!.isCompleted) { _initializationCompleter!.completeError( e, stackTrace, ); } _isFullyInitialized = false; await dispose(); rethrow; } } //video call initialization Future initializeForVideoCall() async { if (_isFullyInitialized) { return; } _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) { throw Exception('Camera or microphone access denied or unavailable'); } if (_localStream == null) { throw Exception('Local video stream is null'); } for (final track in _localStream!.getTracks()) { if (!track.enabled) { track.enabled = true; } } // 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(); } } catch (e, stackTrace) { log('[WebRTC] Video init error: $e', name: 'WebRTCService'); if (_initializationCompleter != null && !_initializationCompleter!.isCompleted) { _initializationCompleter!.completeError( e, stackTrace, ); } _isFullyInitialized = false; await dispose(); rethrow; } } // Setup peer connection event listener void _setupPeerConnectionListeners() { _peerConnection!.onIceCandidate = (RTCIceCandidate candidate) { onIceCandidate?.call(candidate); }; _peerConnection!.onIceGatheringState = (RTCIceGatheringState state) {}; _peerConnection!.onIceConnectionState = (RTCIceConnectionState state) { log('[WEBRTC] ICE connection state: $state', name: 'WebRTCService'); onIceConnectionStateChange?.call(state); }; _peerConnection!.onTrack = (RTCTrackEvent event) { if (event.streams.isEmpty) { return; } final stream = event.streams.first; if (_remoteStream == null || _remoteStream!.id != stream.id) { _remoteStream = stream; if (_isVideoCall && remoteRenderer != null) { try { remoteRenderer!.srcObject = _remoteStream; // Safety refresh for renderer race condition 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) { log('[WEBRTC] Remote renderer error: $e', name: 'WebRTCService'); } } onRemoteStream?.call(_remoteStream!); } else { // Refresh renderer if stream exists but UI missed it if (_isVideoCall && remoteRenderer != null && remoteRenderer!.srcObject == null && _remoteStream != null) { try { remoteRenderer!.srcObject = _remoteStream; } catch (e) { log('[WEBRTC] Renderer refresh failed: $e', name: 'WebRTCService'); } } onRemoteStream?.call(_remoteStream!); } }; _peerConnection!.onConnectionState = (RTCPeerConnectionState state) { log('[WEBRTC] Connection state: $state', name: 'WebRTCService'); if (state == RTCPeerConnectionState.RTCPeerConnectionStateFailed || state == RTCPeerConnectionState.RTCPeerConnectionStateClosed) { onCallEnded?.call(); } }; } /// 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) { localRenderer!.srcObject = null; await localRenderer!.dispose(); localRenderer = null; } if (remoteRenderer != null) { remoteRenderer!.srcObject = null; await remoteRenderer!.dispose(); remoteRenderer = null; } if (_peerConnection != null) { await _peerConnection!.close(); await _peerConnection!.dispose(); _peerConnection = null; } _iceCandidateQueue.clear(); _remoteDescriptionSet = false; // Reset service state because this is a singleton _isFullyInitialized = false; _initializationCompleter = null; _isVideoCall = false; } catch (e) { log( '⚠️ [WebRTC] Error during dispose: $e', name: 'WebRTCService', ); } } }