|
|
|
@ -37,20 +37,31 @@ class WebRTCService {
|
|
|
|
// Track if we're using test mode (Google STUN only)
|
|
|
|
// Track if we're using test mode (Google STUN only)
|
|
|
|
static bool _useTestMode = false;
|
|
|
|
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<void> waitForInitialization() async {
|
|
|
|
|
|
|
|
if (_isFullyInitialized) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (_initializationCompleter != null) {
|
|
|
|
|
|
|
|
await _initializationCompleter!.future;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Enable test mode (use only Google STUN servers for debugging)
|
|
|
|
/// Enable test mode (use only Google STUN servers for debugging)
|
|
|
|
static void enableTestMode() {
|
|
|
|
static void enableTestMode() {
|
|
|
|
_useTestMode = true;
|
|
|
|
_useTestMode = true;
|
|
|
|
if (kDebugMode) {
|
|
|
|
|
|
|
|
log('⚙️ [WebRTC] Test mode enabled', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Disable test mode (use backend TURN servers)
|
|
|
|
/// Disable test mode (use backend TURN servers)
|
|
|
|
static void disableTestMode() {
|
|
|
|
static void disableTestMode() {
|
|
|
|
_useTestMode = false;
|
|
|
|
_useTestMode = false;
|
|
|
|
if (kDebugMode) {
|
|
|
|
|
|
|
|
log('⚙️ [WebRTC] Test mode disabled', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Configuration
|
|
|
|
// Configuration
|
|
|
|
@ -127,33 +138,17 @@ 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>();
|
|
|
|
_initializationCompleter = Completer<void>();
|
|
|
|
_isFullyInitialized = false;
|
|
|
|
_isFullyInitialized = false;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
log('🔧 [WebRTC] Initialization Started', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_isVideoCall = false;
|
|
|
|
_isVideoCall = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Get local audio stream
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
log('🎤 [WebRTC] Requesting microphone access...', name: 'WebRTCService');
|
|
|
|
|
|
|
|
_localStream = await navigator.mediaDevices.getUserMedia(_mediaConstraints);
|
|
|
|
_localStream = await navigator.mediaDevices.getUserMedia(_mediaConstraints);
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Verify audio tracks were captured
|
|
|
|
|
|
|
|
final audioTracks = _localStream!.getAudioTracks();
|
|
|
|
final audioTracks = _localStream!.getAudioTracks();
|
|
|
|
log('✅ [WebRTC] Local audio stream captured', name: 'WebRTCService');
|
|
|
|
|
|
|
|
log(' Audio tracks count: ${audioTracks.length}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < audioTracks.length; i++) {
|
|
|
|
for (var i = 0; i < audioTracks.length; i++) {
|
|
|
|
final track = audioTracks[i];
|
|
|
|
final track = audioTracks[i];
|
|
|
|
log(' Track $i: ${track.kind} - ID: ${track.id}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
log(' Track $i: enabled=${track.enabled}, muted=${track.muted}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL FIX: Ensure track is enabled
|
|
|
|
|
|
|
|
if (!track.enabled) {
|
|
|
|
if (!track.enabled) {
|
|
|
|
log('⚠️ [WebRTC] Track $i was disabled, enabling it', name: 'WebRTCService');
|
|
|
|
|
|
|
|
track.enabled = true;
|
|
|
|
track.enabled = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -163,211 +158,167 @@ class WebRTCService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
log('❌ [WebRTC] Failed to get audio stream: $e', name: 'WebRTCService', error: e);
|
|
|
|
|
|
|
|
throw Exception('Microphone access denied or unavailable');
|
|
|
|
throw Exception('Microphone access denied or unavailable');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create peer connection
|
|
|
|
|
|
|
|
log('🔧 [WebRTC] Creating peer connection...', name: 'WebRTCService');
|
|
|
|
|
|
|
|
_peerConnection = await createPeerConnection(_configuration);
|
|
|
|
_peerConnection = await createPeerConnection(_configuration);
|
|
|
|
|
|
|
|
|
|
|
|
if (_peerConnection == null) {
|
|
|
|
if (_peerConnection == null) {
|
|
|
|
throw Exception('Failed to create peer connection');
|
|
|
|
throw Exception('Failed to create peer connection');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
log('✅ [WebRTC] Peer connection created', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add local stream tracks to peer connection
|
|
|
|
|
|
|
|
log('📤 [WebRTC] Adding local tracks to peer connection...', name: 'WebRTCService');
|
|
|
|
|
|
|
|
final tracks = _localStream!.getTracks();
|
|
|
|
final tracks = _localStream!.getTracks();
|
|
|
|
for (var i = 0; i < tracks.length; i++) {
|
|
|
|
for (var i = 0; i < tracks.length; i++) {
|
|
|
|
final track = tracks[i];
|
|
|
|
final track = tracks[i];
|
|
|
|
log(' Adding track $i: ${track.kind} (ID: ${track.id})', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await _peerConnection!.addTrack(track, _localStream!);
|
|
|
|
await _peerConnection!.addTrack(track, _localStream!);
|
|
|
|
|
|
|
|
|
|
|
|
log(' ✅ Track $i added successfully', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
log('✅ [WebRTC] All local tracks added to peer connection', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Setup peer connection event handlers
|
|
|
|
|
|
|
|
_setupPeerConnectionListeners();
|
|
|
|
_setupPeerConnectionListeners();
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL FIX: Set speakerphone OFF by default (use earpiece)
|
|
|
|
|
|
|
|
log('🔇 [WebRTC] Setting speakerphone OFF by default (earpiece mode)...', name: 'WebRTCService');
|
|
|
|
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
await Helper.setSpeakerphoneOn(false);
|
|
|
|
await Helper.setSpeakerphoneOn(false);
|
|
|
|
log('✅ [WebRTC] Speakerphone disabled (earpiece mode)', name: 'WebRTCService');
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
log('⚠️ [WebRTC] Failed to disable speakerphone: $e', name: 'WebRTCService');
|
|
|
|
// Ignore speakerphone errors
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL FIX: Mark as fully initialized AFTER everything is ready
|
|
|
|
|
|
|
|
_isFullyInitialized = true;
|
|
|
|
_isFullyInitialized = true;
|
|
|
|
if (!_initializationCompleter!.isCompleted) {
|
|
|
|
if (!_initializationCompleter!.isCompleted) {
|
|
|
|
_initializationCompleter!.complete();
|
|
|
|
_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\nStack: $stackTrace', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
// Complete with error
|
|
|
|
|
|
|
|
if (_initializationCompleter != null && !_initializationCompleter!.isCompleted) {
|
|
|
|
if (_initializationCompleter != null && !_initializationCompleter!.isCompleted) {
|
|
|
|
_initializationCompleter!.completeError(e, stackTrace);
|
|
|
|
_initializationCompleter!.completeError(e, stackTrace);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_isFullyInitialized = false;
|
|
|
|
_isFullyInitialized = false;
|
|
|
|
|
|
|
|
|
|
|
|
// Clean up any partial initialization
|
|
|
|
|
|
|
|
await dispose();
|
|
|
|
await dispose();
|
|
|
|
rethrow;
|
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Initialize WebRTC for video call
|
|
|
|
/// Initialize WebRTC for video call
|
|
|
|
/// Initialize WebRTC for video call
|
|
|
|
Future<void> initializeForVideoCall() async {
|
|
|
|
Future<void> initializeForVideoCall() async {
|
|
|
|
try {
|
|
|
|
_initializationCompleter = Completer<void>();
|
|
|
|
if (kDebugMode) {
|
|
|
|
_isFullyInitialized = false;
|
|
|
|
log('🔧 [WebRTC] Initializing video call', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
_isVideoCall = true;
|
|
|
|
_isVideoCall = true;
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize video renderers
|
|
|
|
// Initialize video renderers
|
|
|
|
localRenderer = RTCVideoRenderer();
|
|
|
|
localRenderer = RTCVideoRenderer();
|
|
|
|
remoteRenderer = RTCVideoRenderer();
|
|
|
|
remoteRenderer = RTCVideoRenderer();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
await localRenderer!.initialize();
|
|
|
|
await localRenderer!.initialize();
|
|
|
|
await remoteRenderer!.initialize();
|
|
|
|
await remoteRenderer!.initialize();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Get camera + microphone stream first
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
_localStream = await navigator.mediaDevices.getUserMedia(_videoConstraints);
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
log('❌ [WebRTC] Failed to initialize renderers: $e', name: 'WebRTCService', error: e);
|
|
|
|
log(
|
|
|
|
throw Exception('Failed to initialize video renderers');
|
|
|
|
'❌ [WebRTC] Failed to get video stream: $e',
|
|
|
|
|
|
|
|
name: 'WebRTCService',
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
throw Exception('Camera or microphone access denied or unavailable');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create peer connection FIRST (before getting media)
|
|
|
|
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;
|
|
|
|
final config = _useTestMode ? _testConfiguration : _configuration;
|
|
|
|
|
|
|
|
|
|
|
|
_peerConnection = await createPeerConnection(config);
|
|
|
|
_peerConnection = await createPeerConnection(config);
|
|
|
|
|
|
|
|
|
|
|
|
if (_peerConnection == null) {
|
|
|
|
if (_peerConnection == null) {
|
|
|
|
throw Exception('Failed to create peer connection');
|
|
|
|
throw Exception('Failed to create peer connection');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Setup peer connection listeners immediately
|
|
|
|
// Setup listeners
|
|
|
|
_setupPeerConnectionListeners();
|
|
|
|
_setupPeerConnectionListeners();
|
|
|
|
|
|
|
|
|
|
|
|
// Get local audio + video stream
|
|
|
|
// Add local tracks
|
|
|
|
try {
|
|
|
|
final tracks = _localStream!.getTracks();
|
|
|
|
_localStream = await navigator.mediaDevices.getUserMedia(_videoConstraints);
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
if (tracks.isEmpty) {
|
|
|
|
log('❌ [WebRTC] Failed to get video stream: $e', name: 'WebRTCService', error: e);
|
|
|
|
throw Exception('No media tracks found');
|
|
|
|
throw Exception('Camera or microphone access denied or unavailable');
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Set local stream to renderer
|
|
|
|
for (final track in tracks) {
|
|
|
|
if (localRenderer != null) {
|
|
|
|
await _peerConnection!.addTrack(
|
|
|
|
localRenderer!.srcObject = _localStream;
|
|
|
|
track,
|
|
|
|
|
|
|
|
_localStream!,
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add local stream tracks to peer connection
|
|
|
|
_isFullyInitialized = true;
|
|
|
|
_localStream!.getTracks().forEach((track) {
|
|
|
|
|
|
|
|
_peerConnection!.addTrack(track, _localStream!);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (kDebugMode) {
|
|
|
|
if (!_initializationCompleter!.isCompleted) {
|
|
|
|
log('✅ [WebRTC] Video call initialized', name: 'WebRTCService');
|
|
|
|
_initializationCompleter!.complete();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log(
|
|
|
|
|
|
|
|
'✅ [WebRTC] Video initialization completed',
|
|
|
|
|
|
|
|
name: 'WebRTCService',
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
log('❌ [WebRTC] Video initialization error: $e', name: 'WebRTCService', error: e, stackTrace: stackTrace);
|
|
|
|
log(
|
|
|
|
// Clean up any partial initialization
|
|
|
|
'❌ [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();
|
|
|
|
await dispose();
|
|
|
|
|
|
|
|
|
|
|
|
rethrow;
|
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 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
|
|
|
|
|
|
|
|
_peerConnection!.onIceCandidate = (RTCIceCandidate candidate) {
|
|
|
|
_peerConnection!.onIceCandidate = (RTCIceCandidate candidate) {
|
|
|
|
log('🧊 [WebRTC] onIceCandidate triggered', name: 'WebRTCService');
|
|
|
|
onIceCandidate?.call(candidate);
|
|
|
|
if (onIceCandidate != null) {
|
|
|
|
|
|
|
|
onIceCandidate!(candidate);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
log('⚠️ [WebRTC] onIceCandidate callback is NULL!', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Handle ICE gathering state changes
|
|
|
|
|
|
|
|
_peerConnection!.onIceGatheringState = (RTCIceGatheringState state) {
|
|
|
|
_peerConnection!.onIceGatheringState = (RTCIceGatheringState state) {
|
|
|
|
log('📊 [WebRTC] ICE gathering state: ${state.toString()}', name: 'WebRTCService');
|
|
|
|
// Ice gathering state changed
|
|
|
|
if (state == RTCIceGatheringState.RTCIceGatheringStateComplete) {
|
|
|
|
|
|
|
|
log('✅ [WebRTC] ICE gathering completed', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Handle ICE connection state changes
|
|
|
|
|
|
|
|
_peerConnection!.onIceConnectionState = (RTCIceConnectionState state) {
|
|
|
|
_peerConnection!.onIceConnectionState = (RTCIceConnectionState state) {
|
|
|
|
log('═══════════════════════════════════════════', name: 'WebRTCService');
|
|
|
|
onIceConnectionStateChange?.call(state);
|
|
|
|
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');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Handle remote stream
|
|
|
|
|
|
|
|
_peerConnection!.onTrack = (RTCTrackEvent event) {
|
|
|
|
_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(' Streams count: ${event.streams.length}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (event.streams.isNotEmpty) {
|
|
|
|
if (event.streams.isNotEmpty) {
|
|
|
|
final stream = event.streams[0];
|
|
|
|
final stream = event.streams[0];
|
|
|
|
log(' Stream ID: ${stream.id}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
log(' Audio tracks: ${stream.getAudioTracks().length}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
log(' Video tracks: ${stream.getVideoTracks().length}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Log all tracks in the stream
|
|
|
|
|
|
|
|
final allTracks = stream.getTracks();
|
|
|
|
|
|
|
|
for (var i = 0; i < allTracks.length; i++) {
|
|
|
|
|
|
|
|
final track = allTracks[i];
|
|
|
|
|
|
|
|
log(' Track $i: ${track.kind} - enabled=${track.enabled}, muted=${track.muted}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// If this is the first remote stream or a different stream, update it
|
|
|
|
|
|
|
|
if (_remoteStream == null || _remoteStream!.id != stream.id) {
|
|
|
|
if (_remoteStream == null || _remoteStream!.id != stream.id) {
|
|
|
|
log('✅ [WebRTC] Setting new remote stream', name: 'WebRTCService');
|
|
|
|
|
|
|
|
_remoteStream = stream;
|
|
|
|
_remoteStream = stream;
|
|
|
|
|
|
|
|
|
|
|
|
// For video calls, assign stream to renderer
|
|
|
|
|
|
|
|
if (remoteRenderer != null && _isVideoCall) {
|
|
|
|
if (remoteRenderer != null && _isVideoCall) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
remoteRenderer!.srcObject = _remoteStream;
|
|
|
|
remoteRenderer!.srcObject = _remoteStream;
|
|
|
|
|
|
|
|
|
|
|
|
// Retry assignment after delays to ensure it sticks
|
|
|
|
|
|
|
|
Future.delayed(const Duration(milliseconds: 100), () {
|
|
|
|
Future.delayed(const Duration(milliseconds: 100), () {
|
|
|
|
if (remoteRenderer != null && remoteRenderer!.srcObject == null && _remoteStream != null) {
|
|
|
|
if (remoteRenderer != null && remoteRenderer!.srcObject == null && _remoteStream != null) {
|
|
|
|
remoteRenderer!.srcObject = _remoteStream;
|
|
|
|
remoteRenderer!.srcObject = _remoteStream;
|
|
|
|
@ -380,40 +331,27 @@ class WebRTCService {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
log('⚠️ [WebRTC] Failed to assign remote stream: $e', name: 'WebRTCService');
|
|
|
|
// Ignore errors
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Notify callback
|
|
|
|
onRemoteStream?.call(_remoteStream!);
|
|
|
|
if (onRemoteStream != null) {
|
|
|
|
|
|
|
|
onRemoteStream!(_remoteStream!);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log('✅ [WebRTC] Remote stream set and callback notified', name: 'WebRTCService');
|
|
|
|
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
log('ℹ️ [WebRTC] Additional track on existing stream', name: 'WebRTCService');
|
|
|
|
|
|
|
|
// Additional track on existing stream
|
|
|
|
|
|
|
|
if (remoteRenderer != null && _isVideoCall && remoteRenderer!.srcObject == null && _remoteStream != null) {
|
|
|
|
if (remoteRenderer != null && _isVideoCall && remoteRenderer!.srcObject == null && _remoteStream != null) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
remoteRenderer!.srcObject = _remoteStream;
|
|
|
|
remoteRenderer!.srcObject = _remoteStream;
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
log('⚠️ [WebRTC] Failed to assign stream (additional track): $e', name: 'WebRTCService');
|
|
|
|
// Ignore errors
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (onRemoteStream != null) {
|
|
|
|
onRemoteStream?.call(_remoteStream!);
|
|
|
|
onRemoteStream!(_remoteStream!);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
|
|
|
|
log('⚠️ [WebRTC] No streams in onTrack event!', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
log('═══════════════════════════════════════════', name: 'WebRTCService');
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Handle connection state changes
|
|
|
|
|
|
|
|
_peerConnection!.onConnectionState = (RTCPeerConnectionState state) {
|
|
|
|
_peerConnection!.onConnectionState = (RTCPeerConnectionState state) {
|
|
|
|
if (kDebugMode || state == RTCPeerConnectionState.RTCPeerConnectionStateFailed) {
|
|
|
|
if (state == RTCPeerConnectionState.RTCPeerConnectionStateFailed) {
|
|
|
|
log('🔌 [WebRTC] Connection state: ${state.toString()}', name: 'WebRTCService');
|
|
|
|
log('🔌 [WebRTC] Connection state: ${state.toString()}', name: 'WebRTCService');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@ -426,22 +364,6 @@ class WebRTCService {
|
|
|
|
throw Exception('Peer connection not initialized');
|
|
|
|
throw Exception('Peer connection not initialized');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log('🔧 [WebRTC] Creating SDP offer...', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Verify local tracks before creating offer
|
|
|
|
|
|
|
|
final senders = await _peerConnection!.getSenders();
|
|
|
|
|
|
|
|
log('📊 [WebRTC] Peer connection has ${senders.length} sender(s)', name: 'WebRTCService');
|
|
|
|
|
|
|
|
for (var i = 0; i < senders.length; i++) {
|
|
|
|
|
|
|
|
final sender = senders[i];
|
|
|
|
|
|
|
|
final track = sender.track;
|
|
|
|
|
|
|
|
if (track != null) {
|
|
|
|
|
|
|
|
log(' Sender $i: ${track.kind} track (ID: ${track.id})', name: 'WebRTCService');
|
|
|
|
|
|
|
|
log(' Sender $i: enabled=${track.enabled}, muted=${track.muted}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
log(' Sender $i: NO TRACK!', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final offer = await _peerConnection!.createOffer({
|
|
|
|
final offer = await _peerConnection!.createOffer({
|
|
|
|
'offerToReceiveAudio': true,
|
|
|
|
'offerToReceiveAudio': true,
|
|
|
|
'offerToReceiveVideo': _isVideoCall,
|
|
|
|
'offerToReceiveVideo': _isVideoCall,
|
|
|
|
@ -450,35 +372,9 @@ class WebRTCService {
|
|
|
|
|
|
|
|
|
|
|
|
await _peerConnection!.setLocalDescription(offer);
|
|
|
|
await _peerConnection!.setLocalDescription(offer);
|
|
|
|
|
|
|
|
|
|
|
|
if (kDebugMode) {
|
|
|
|
|
|
|
|
log('✅ [WebRTC] SDP offer created', name: 'WebRTCService');
|
|
|
|
|
|
|
|
log(' Offer type: ${offer.type}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
log(' Offer SDP length: ${offer.sdp?.length ?? 0}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Log SDP to verify audio track is included
|
|
|
|
|
|
|
|
if (offer.sdp != null) {
|
|
|
|
|
|
|
|
final sdpLines = offer.sdp!.split('\n');
|
|
|
|
|
|
|
|
final audioLines = sdpLines.where((line) =>
|
|
|
|
|
|
|
|
line.contains('m=audio') ||
|
|
|
|
|
|
|
|
line.contains('a=sendrecv') ||
|
|
|
|
|
|
|
|
line.contains('a=sendonly') ||
|
|
|
|
|
|
|
|
line.contains('a=recvonly')
|
|
|
|
|
|
|
|
).toList();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log('📋 [WebRTC] SDP Audio Configuration:', name: 'WebRTCService');
|
|
|
|
|
|
|
|
for (final line in audioLines) {
|
|
|
|
|
|
|
|
log(' $line', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!offer.sdp!.contains('m=audio')) {
|
|
|
|
|
|
|
|
log('❌ [WebRTC] WARNING: No audio media line in SDP offer!', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return offer;
|
|
|
|
return offer;
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
log('❌ [WebRTC] Error creating offer: $e', name: 'WebRTCService', error: e, stackTrace: stackTrace);
|
|
|
|
log('❌ [WebRTC] Error creating offer: $e\nStack: $stackTrace', name: 'WebRTCService');
|
|
|
|
rethrow;
|
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -490,55 +386,12 @@ class WebRTCService {
|
|
|
|
throw Exception('Peer connection not initialized');
|
|
|
|
throw Exception('Peer connection not initialized');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log('🔧 [WebRTC] Creating SDP answer...', name: 'WebRTCService');
|
|
|
|
|
|
|
|
log(' Received offer SDP length: ${offerSdp.length}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Log received offer to verify it has audio
|
|
|
|
|
|
|
|
if (kDebugMode) {
|
|
|
|
|
|
|
|
final offerLines = offerSdp.split('\n');
|
|
|
|
|
|
|
|
final audioLines = offerLines.where((line) =>
|
|
|
|
|
|
|
|
line.contains('m=audio') ||
|
|
|
|
|
|
|
|
line.contains('a=sendrecv') ||
|
|
|
|
|
|
|
|
line.contains('a=sendonly') ||
|
|
|
|
|
|
|
|
line.contains('a=recvonly')
|
|
|
|
|
|
|
|
).toList();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log('📋 [WebRTC] Received Offer Audio Configuration:', name: 'WebRTCService');
|
|
|
|
|
|
|
|
for (final line in audioLines) {
|
|
|
|
|
|
|
|
log(' $line', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!offerSdp.contains('m=audio')) {
|
|
|
|
|
|
|
|
log('❌ [WebRTC] WARNING: No audio media line in received offer!', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Verify local tracks before creating answer
|
|
|
|
|
|
|
|
final senders = await _peerConnection!.getSenders();
|
|
|
|
|
|
|
|
log('📊 [WebRTC] Peer connection has ${senders.length} sender(s)', name: 'WebRTCService');
|
|
|
|
|
|
|
|
for (var i = 0; i < senders.length; i++) {
|
|
|
|
|
|
|
|
final sender = senders[i];
|
|
|
|
|
|
|
|
final track = sender.track;
|
|
|
|
|
|
|
|
if (track != null) {
|
|
|
|
|
|
|
|
log(' Sender $i: ${track.kind} track (ID: ${track.id})', name: 'WebRTCService');
|
|
|
|
|
|
|
|
log(' Sender $i: enabled=${track.enabled}, muted=${track.muted}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
log(' Sender $i: NO TRACK!', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Set remote description (offer from caller)
|
|
|
|
|
|
|
|
final offer = RTCSessionDescription(offerSdp, 'offer');
|
|
|
|
final offer = RTCSessionDescription(offerSdp, 'offer');
|
|
|
|
log('🔧 [WebRTC] Setting remote description (offer)...', name: 'WebRTCService');
|
|
|
|
|
|
|
|
await _peerConnection!.setRemoteDescription(offer);
|
|
|
|
await _peerConnection!.setRemoteDescription(offer);
|
|
|
|
_remoteDescriptionSet = true;
|
|
|
|
_remoteDescriptionSet = true;
|
|
|
|
log('✅ [WebRTC] Remote description set', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Flush queued ICE candidates
|
|
|
|
|
|
|
|
await _flushIceCandidateQueue();
|
|
|
|
await _flushIceCandidateQueue();
|
|
|
|
|
|
|
|
|
|
|
|
// Create answer
|
|
|
|
|
|
|
|
log('🔧 [WebRTC] Creating answer...', name: 'WebRTCService');
|
|
|
|
|
|
|
|
final answer = await _peerConnection!.createAnswer({
|
|
|
|
final answer = await _peerConnection!.createAnswer({
|
|
|
|
'offerToReceiveAudio': true,
|
|
|
|
'offerToReceiveAudio': true,
|
|
|
|
'offerToReceiveVideo': _isVideoCall,
|
|
|
|
'offerToReceiveVideo': _isVideoCall,
|
|
|
|
@ -546,35 +399,9 @@ class WebRTCService {
|
|
|
|
|
|
|
|
|
|
|
|
await _peerConnection!.setLocalDescription(answer);
|
|
|
|
await _peerConnection!.setLocalDescription(answer);
|
|
|
|
|
|
|
|
|
|
|
|
if (kDebugMode) {
|
|
|
|
|
|
|
|
log('✅ [WebRTC] SDP answer created', name: 'WebRTCService');
|
|
|
|
|
|
|
|
log(' Answer type: ${answer.type}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
log(' Answer SDP length: ${answer.sdp?.length ?? 0}', name: 'WebRTCService');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Log SDP to verify audio track is included
|
|
|
|
|
|
|
|
if (answer.sdp != null) {
|
|
|
|
|
|
|
|
final sdpLines = answer.sdp!.split('\n');
|
|
|
|
|
|
|
|
final audioLines = sdpLines.where((line) =>
|
|
|
|
|
|
|
|
line.contains('m=audio') ||
|
|
|
|
|
|
|
|
line.contains('a=sendrecv') ||
|
|
|
|
|
|
|
|
line.contains('a=sendonly') ||
|
|
|
|
|
|
|
|
line.contains('a=recvonly')
|
|
|
|
|
|
|
|
).toList();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log('📋 [WebRTC] SDP Answer Audio Configuration:', name: 'WebRTCService');
|
|
|
|
|
|
|
|
for (final line in audioLines) {
|
|
|
|
|
|
|
|
log(' $line', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!answer.sdp!.contains('m=audio')) {
|
|
|
|
|
|
|
|
log('❌ [WebRTC] WARNING: No audio media line in SDP answer!', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return answer;
|
|
|
|
return answer;
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
log('❌ [WebRTC] Error creating answer: $e', name: 'WebRTCService', error: e, stackTrace: stackTrace);
|
|
|
|
log('❌ [WebRTC] Error creating answer: $e\nStack: $stackTrace', name: 'WebRTCService');
|
|
|
|
rethrow;
|
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -590,14 +417,10 @@ class WebRTCService {
|
|
|
|
await _peerConnection!.setRemoteDescription(answer);
|
|
|
|
await _peerConnection!.setRemoteDescription(answer);
|
|
|
|
_remoteDescriptionSet = true;
|
|
|
|
_remoteDescriptionSet = true;
|
|
|
|
|
|
|
|
|
|
|
|
// Flush queued ICE candidates
|
|
|
|
|
|
|
|
await _flushIceCandidateQueue();
|
|
|
|
await _flushIceCandidateQueue();
|
|
|
|
|
|
|
|
|
|
|
|
if (kDebugMode) {
|
|
|
|
|
|
|
|
log('✅ [WebRTC] Remote answer set', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
} catch (e, stackTrace) {
|
|
|
|
log('❌ [WebRTC] Error setting remote answer: $e', name: 'WebRTCService', error: e, stackTrace: stackTrace);
|
|
|
|
log('❌ [WebRTC] Error setting remote answer: $e\nStack: $stackTrace', name: 'WebRTCService');
|
|
|
|
rethrow;
|
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -638,10 +461,6 @@ class WebRTCService {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (kDebugMode) {
|
|
|
|
|
|
|
|
log('🔄 [WebRTC] Flushing ${_iceCandidateQueue.length} ICE candidates', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (final candidate in _iceCandidateQueue) {
|
|
|
|
for (final candidate in _iceCandidateQueue) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
await _peerConnection!.addCandidate(candidate);
|
|
|
|
await _peerConnection!.addCandidate(candidate);
|
|
|
|
@ -680,9 +499,6 @@ class WebRTCService {
|
|
|
|
if (videoTracks.isNotEmpty) {
|
|
|
|
if (videoTracks.isNotEmpty) {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
await Helper.switchCamera(videoTracks.first);
|
|
|
|
await Helper.switchCamera(videoTracks.first);
|
|
|
|
if (kDebugMode) {
|
|
|
|
|
|
|
|
log('✅ [WebRTC] Camera switched', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
log('❌ [WebRTC] Error switching camera: $e', name: 'WebRTCService');
|
|
|
|
log('❌ [WebRTC] Error switching camera: $e', name: 'WebRTCService');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -716,9 +532,6 @@ class WebRTCService {
|
|
|
|
if (_remoteStream != null && remoteRenderer != null && _isVideoCall) {
|
|
|
|
if (_remoteStream != null && remoteRenderer != null && _isVideoCall) {
|
|
|
|
if (remoteRenderer!.srcObject == null) {
|
|
|
|
if (remoteRenderer!.srcObject == null) {
|
|
|
|
remoteRenderer!.srcObject = _remoteStream;
|
|
|
|
remoteRenderer!.srcObject = _remoteStream;
|
|
|
|
if (kDebugMode) {
|
|
|
|
|
|
|
|
log('✅ [WebRTC] Remote renderer refreshed', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -738,10 +551,6 @@ class WebRTCService {
|
|
|
|
|
|
|
|
|
|
|
|
await _peerConnection!.setLocalDescription(offer);
|
|
|
|
await _peerConnection!.setLocalDescription(offer);
|
|
|
|
|
|
|
|
|
|
|
|
if (kDebugMode) {
|
|
|
|
|
|
|
|
log('✅ [WebRTC] ICE restart initiated', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return offer;
|
|
|
|
return offer;
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
log('❌ [WebRTC] ICE restart failed: $e', name: 'WebRTCService');
|
|
|
|
log('❌ [WebRTC] ICE restart failed: $e', name: 'WebRTCService');
|
|
|
|
@ -750,18 +559,6 @@ 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -771,13 +568,9 @@ class WebRTCService {
|
|
|
|
return _peerConnection?.iceConnectionState;
|
|
|
|
return _peerConnection?.iceConnectionState;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Check if peer connection is initialized
|
|
|
|
|
|
|
|
bool get isPeerConnectionInitialized => _peerConnection != null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Dispose and cleanup
|
|
|
|
/// Dispose and cleanup
|
|
|
|
Future<void> dispose() async {
|
|
|
|
Future<void> dispose() async {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
// Stop local stream tracks
|
|
|
|
|
|
|
|
if (_localStream != null) {
|
|
|
|
if (_localStream != null) {
|
|
|
|
_localStream!.getTracks().forEach((track) {
|
|
|
|
_localStream!.getTracks().forEach((track) {
|
|
|
|
track.stop();
|
|
|
|
track.stop();
|
|
|
|
@ -786,7 +579,6 @@ class WebRTCService {
|
|
|
|
_localStream = null;
|
|
|
|
_localStream = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Stop remote stream tracks
|
|
|
|
|
|
|
|
if (_remoteStream != null) {
|
|
|
|
if (_remoteStream != null) {
|
|
|
|
_remoteStream!.getTracks().forEach((track) {
|
|
|
|
_remoteStream!.getTracks().forEach((track) {
|
|
|
|
track.stop();
|
|
|
|
track.stop();
|
|
|
|
@ -795,7 +587,6 @@ class WebRTCService {
|
|
|
|
_remoteStream = null;
|
|
|
|
_remoteStream = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Dispose renderers
|
|
|
|
|
|
|
|
if (localRenderer != null) {
|
|
|
|
if (localRenderer != null) {
|
|
|
|
await localRenderer!.dispose();
|
|
|
|
await localRenderer!.dispose();
|
|
|
|
localRenderer = null;
|
|
|
|
localRenderer = null;
|
|
|
|
@ -806,20 +597,15 @@ class WebRTCService {
|
|
|
|
remoteRenderer = null;
|
|
|
|
remoteRenderer = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Close peer connection
|
|
|
|
|
|
|
|
if (_peerConnection != null) {
|
|
|
|
if (_peerConnection != null) {
|
|
|
|
await _peerConnection!.close();
|
|
|
|
await _peerConnection!.close();
|
|
|
|
await _peerConnection!.dispose();
|
|
|
|
await _peerConnection!.dispose();
|
|
|
|
_peerConnection = null;
|
|
|
|
_peerConnection = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Clear ICE candidate queue
|
|
|
|
|
|
|
|
_iceCandidateQueue.clear();
|
|
|
|
_iceCandidateQueue.clear();
|
|
|
|
_remoteDescriptionSet = false;
|
|
|
|
_remoteDescriptionSet = false;
|
|
|
|
|
|
|
|
|
|
|
|
if (kDebugMode) {
|
|
|
|
|
|
|
|
log('✅ [WebRTC] Service disposed', name: 'WebRTCService');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
log('⚠️ [WebRTC] Error during dispose: $e', name: 'WebRTCService');
|
|
|
|
log('⚠️ [WebRTC] Error during dispose: $e', name: 'WebRTCService');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|