From 4f7d73bdebad3db15e330bb14181db911f5e3ec8 Mon Sep 17 00:00:00 2001 From: Karthikeyan S Date: Mon, 5 Jan 2026 22:14:42 +0530 Subject: [PATCH 1/2] fix: handle failed to create RTCPeerConnection object error in a call --- .gitignore | 1 + lib/src/voip/call_session.dart | 19 +++++++++++++++++-- lib/src/voip/utils/types.dart | 3 +++ test/calls_test.dart | 20 ++++++++++++++++++++ test/webrtc_stub.dart | 10 ++++++++-- 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 5419541bd..f6a011698 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ coverage_badge.svg coverage.xml TEST-report.* rust +coverage_dir/* # IntelliJ related *.iml diff --git a/lib/src/voip/call_session.dart b/lib/src/voip/call_session.dart index 6aa332db2..d318454d2 100644 --- a/lib/src/voip/call_session.dart +++ b/lib/src/voip/call_session.dart @@ -1217,8 +1217,23 @@ class CallSession { } } }; - } catch (e) { - Logs().v('[VOIP] prepareMediaStream error => ${e.toString()}'); + } catch (e, s) { + Logs().e( + '[VOIP] preparePeerConnection => Failed to create peer connection object', + e, + s, + ); + fireCallEvent(CallStateChange.kError); + await terminate( + CallParty.kLocal, + CallErrorCode.createPeerConnectionFailed, + true, + ); + throw CallError( + CallErrorCode.createPeerConnectionFailed, + 'Failed to create peer connection object ', + e, + ); } } diff --git a/lib/src/voip/utils/types.dart b/lib/src/voip/utils/types.dart index 29531fdd3..2f33bbf32 100644 --- a/lib/src/voip/utils/types.dart +++ b/lib/src/voip/utils/types.dart @@ -40,6 +40,9 @@ enum CallErrorCode { /// The user chose to end the call userHangup('user_hangup'), + /// An error code when creating peer connection object fails locally. + createPeerConnectionFailed('create_peer_connection_failed'), + /// An error code when the local client failed to create an offer. localOfferFailed('local_offer_failed'), diff --git a/test/calls_test.dart b/test/calls_test.dart index c65661ea0..3ff0476ab 100644 --- a/test/calls_test.dart +++ b/test/calls_test.dart @@ -614,6 +614,26 @@ void main() { ); }); + test('Call fails when peer connection creation fails', () async { + final mockDelegate = MockWebRTCDelegate() + ..throwOnCreatePeerConnection = true; + voip = VoIP(matrix, mockDelegate); + VoIP.customTxid = '1234'; + + try { + await voip.inviteToCall( + room, + CallType.kVoice, + userId: '@alice:testing.com', + ); + fail('Expected call to fail'); + } catch (e) { + expect(e, isA()); + expect((e as CallError).code, CallErrorCode.createPeerConnectionFailed); + expect(voip.currentCID, null); + } + }); + test('getFamedlyCallEvents sort order', () { room.setState( Event( diff --git a/test/webrtc_stub.dart b/test/webrtc_stub.dart index 0744d6147..8d9ec1b20 100644 --- a/test/webrtc_stub.dart +++ b/test/webrtc_stub.dart @@ -5,6 +5,8 @@ import 'package:webrtc_interface/webrtc_interface.dart'; import 'package:matrix/matrix.dart'; class MockWebRTCDelegate implements WebRTCDelegate { + bool throwOnCreatePeerConnection = false; + @override bool get canHandleNewCall => true; @@ -12,8 +14,12 @@ class MockWebRTCDelegate implements WebRTCDelegate { Future createPeerConnection( Map configuration, [ Map constraints = const {}, - ]) async => - MockRTCPeerConnection(); + ]) async { + if (throwOnCreatePeerConnection) { + throw Exception('mock exception while creating peer connection'); + } + return MockRTCPeerConnection(); + } @override Future registerListeners(CallSession session) async { From 11322449a15fafae2d9c11471b7e662ea910759a Mon Sep 17 00:00:00 2001 From: Karthikeyan S Date: Mon, 5 Jan 2026 22:15:13 +0530 Subject: [PATCH 2/2] fix: handle screenshare failure gracefully --- lib/src/voip/call_session.dart | 62 ++++++++++++++++++++-------------- lib/src/voip/utils/types.dart | 5 +++ test/calls_test.dart | 26 ++++++++++++++ test/webrtc_stub.dart | 9 ++++- 4 files changed, 75 insertions(+), 27 deletions(-) diff --git a/lib/src/voip/call_session.dart b/lib/src/voip/call_session.dart index d318454d2..c25c7bfde 100644 --- a/lib/src/voip/call_session.dart +++ b/lib/src/voip/call_session.dart @@ -465,9 +465,14 @@ class CallSession { await pc!.setLocalDescription(answer); } } catch (e, s) { - Logs().e('[VOIP] onNegotiateReceived => ', e, s); - await _getLocalOfferFailed(e); - return; + Logs().e('[VOIP] onNegotiateReceived => Failed to get local offer', e, s); + fireCallEvent(CallStateChange.kError); + await terminate(CallParty.kLocal, CallErrorCode.localOfferFailed, true); + throw CallError( + CallErrorCode.localOfferFailed, + 'Failed to get local offer', + e, + ); } final newLocalOnHold = await isLocalOnHold(); @@ -592,7 +597,6 @@ class CallSession { await addLocalStream(stream, SDPStreamMetadataPurpose.Screenshare); return true; } catch (err) { - fireCallEvent(CallStateChange.kError); return false; } } else { @@ -1147,9 +1151,15 @@ class CallSession { await Future.delayed(voip.timeouts!.delayBeforeOffer); final offer = await pc!.createOffer({}); await _gotLocalOffer(offer); - } catch (e) { - await _getLocalOfferFailed(e); - return; + } catch (e, s) { + Logs().e('[VOIP] onNegotiationNeeded => Failed to get local offer', e, s); + fireCallEvent(CallStateChange.kError); + await terminate(CallParty.kLocal, CallErrorCode.localOfferFailed, true); + throw CallError( + CallErrorCode.localOfferFailed, + 'Failed to get local offer', + e, + ); } finally { _makingOffer = false; } @@ -1326,9 +1336,15 @@ class CallSession { }; try { return await voip.delegate.mediaDevices.getUserMedia(mediaConstraints); - } catch (e) { - await _getUserMediaFailed(e); - rethrow; + } catch (e, s) { + Logs().w('Failed to get user media - ending call', e, s); + fireCallEvent(CallStateChange.kError); + await terminate(CallParty.kLocal, CallErrorCode.userMediaFailed, true); + throw CallError( + CallErrorCode.userMediaFailed, + 'Failed to get user media', + e, + ); } } @@ -1336,10 +1352,17 @@ class CallSession { try { return await voip.delegate.mediaDevices .getDisplayMedia(UserMediaConstraints.screenMediaConstraints); - } catch (e) { - await _getUserMediaFailed(e); + } catch (e, s) { + Logs().w('Failed to get display media', e, s); + fireCallEvent(CallStateChange.kError); + // We don't terminate the call here because the user might still want to stay + // on the call and try again later. + throw CallError( + CallErrorCode.displayMediaFailed, + 'Failed to get display media', + e, + ); } - return null; } Future _createPeerConnection() async { @@ -1469,19 +1492,6 @@ class CallSession { } } - Future _getLocalOfferFailed(dynamic err) async { - Logs().e('Failed to get local offer ${err.toString()}'); - fireCallEvent(CallStateChange.kError); - - await terminate(CallParty.kLocal, CallErrorCode.localOfferFailed, true); - } - - Future _getUserMediaFailed(dynamic err) async { - Logs().w('Failed to get user media - ending call ${err.toString()}'); - fireCallEvent(CallStateChange.kError); - await terminate(CallParty.kLocal, CallErrorCode.userMediaFailed, true); - } - Future onSelectAnswerReceived(String? selectedPartyId) async { if (direction != CallDirection.kIncoming) { Logs().w('Got select_answer for an outbound call: ignoring'); diff --git a/lib/src/voip/utils/types.dart b/lib/src/voip/utils/types.dart index 2f33bbf32..abce342ab 100644 --- a/lib/src/voip/utils/types.dart +++ b/lib/src/voip/utils/types.dart @@ -50,6 +50,11 @@ enum CallErrorCode { /// the hardware isn't plugged in, or the user has explicitly denied access. userMediaFailed('user_media_failed'), + /// An error code when there is no local display to screenshare. This may be + /// because the hardware isn't plugged in, or the user has explicitly denied + /// access. + displayMediaFailed('display_media_failed'), + /// Error code used when a call event failed to send /// because unknown devices were present in the room unknownDevice('unknown_device'), diff --git a/test/calls_test.dart b/test/calls_test.dart index 3ff0476ab..09f59003d 100644 --- a/test/calls_test.dart +++ b/test/calls_test.dart @@ -634,6 +634,32 @@ void main() { } }); + test('Call continues when getDisplayMedia fails', () async { + final mockDelegate = MockWebRTCDelegate(); + mockDelegate.mediaDevices.throwOnGetDisplayMedia = true; + voip = VoIP(matrix, mockDelegate); + VoIP.customTxid = '1234'; + + final call = await voip.inviteToCall( + room, + CallType.kVoice, + userId: '@alice:testing.com', + ); + + // Attempt to share screen - should not throw or terminate call + try { + await call.setScreensharingEnabled(true); + } catch (e) { + fail('Screen sharing failure should be handled internally'); + } + + expect(call.onCallEventChanged.value, CallStateChange.kError); + expect(call.state, isNot(CallState.kEnded)); + expect(voip.currentCID, isNotNull); + + await call.hangup(reason: CallErrorCode.userHangup); + }); + test('getFamedlyCallEvents sort order', () { room.setState( Event( diff --git a/test/webrtc_stub.dart b/test/webrtc_stub.dart index 8d9ec1b20..7644df07a 100644 --- a/test/webrtc_stub.dart +++ b/test/webrtc_stub.dart @@ -54,8 +54,10 @@ class MockWebRTCDelegate implements WebRTCDelegate { @override bool get isWeb => false; + final _mockMediaDevices = MockMediaDevices(); + @override - MediaDevices get mediaDevices => MockMediaDevices(); + MockMediaDevices get mediaDevices => _mockMediaDevices; @override Future playRingtone() async { @@ -111,6 +113,8 @@ class MockMediaDeviceInfo implements MediaDeviceInfo { } class MockMediaDevices implements MediaDevices { + bool throwOnGetDisplayMedia = false; + @override Function(dynamic event)? ondevicechange; @@ -134,6 +138,9 @@ class MockMediaDevices implements MediaDevices { Future getDisplayMedia( Map mediaConstraints, ) async { + if (throwOnGetDisplayMedia) { + throw Exception('mock exception while getting display media'); + } return MockMediaStream('', ''); }