Skip to content

Commit 5237c68

Browse files
fix: handle screenshare failure gracefully
1 parent eb32812 commit 5237c68

File tree

4 files changed

+75
-27
lines changed

4 files changed

+75
-27
lines changed

lib/src/voip/call_session.dart

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -465,9 +465,14 @@ class CallSession {
465465
await pc!.setLocalDescription(answer);
466466
}
467467
} catch (e, s) {
468-
Logs().e('[VOIP] onNegotiateReceived => ', e, s);
469-
await _getLocalOfferFailed(e);
470-
return;
468+
Logs().e('[VOIP] onNegotiateReceived => Failed to get local offer', e, s);
469+
fireCallEvent(CallStateChange.kError);
470+
await terminate(CallParty.kLocal, CallErrorCode.localOfferFailed, true);
471+
throw CallError(
472+
CallErrorCode.localOfferFailed,
473+
'Failed to get local offer',
474+
e,
475+
);
471476
}
472477

473478
final newLocalOnHold = await isLocalOnHold();
@@ -592,7 +597,6 @@ class CallSession {
592597
await addLocalStream(stream, SDPStreamMetadataPurpose.Screenshare);
593598
return true;
594599
} catch (err) {
595-
fireCallEvent(CallStateChange.kError);
596600
return false;
597601
}
598602
} else {
@@ -1147,9 +1151,15 @@ class CallSession {
11471151
await Future.delayed(voip.timeouts!.delayBeforeOffer);
11481152
final offer = await pc!.createOffer({});
11491153
await _gotLocalOffer(offer);
1150-
} catch (e) {
1151-
await _getLocalOfferFailed(e);
1152-
return;
1154+
} catch (e, s) {
1155+
Logs().e('[VOIP] onNegotiationNeeded => Failed to get local offer', e, s);
1156+
fireCallEvent(CallStateChange.kError);
1157+
await terminate(CallParty.kLocal, CallErrorCode.localOfferFailed, true);
1158+
throw CallError(
1159+
CallErrorCode.localOfferFailed,
1160+
'Failed to get local offer',
1161+
e,
1162+
);
11531163
} finally {
11541164
_makingOffer = false;
11551165
}
@@ -1326,20 +1336,33 @@ class CallSession {
13261336
};
13271337
try {
13281338
return await voip.delegate.mediaDevices.getUserMedia(mediaConstraints);
1329-
} catch (e) {
1330-
await _getUserMediaFailed(e);
1331-
rethrow;
1339+
} catch (e, s) {
1340+
Logs().w('Failed to get user media - ending call', e, s);
1341+
fireCallEvent(CallStateChange.kError);
1342+
await terminate(CallParty.kLocal, CallErrorCode.userMediaFailed, true);
1343+
throw CallError(
1344+
CallErrorCode.userMediaFailed,
1345+
'Failed to get user media',
1346+
e,
1347+
);
13321348
}
13331349
}
13341350

13351351
Future<MediaStream?> _getDisplayMedia() async {
13361352
try {
13371353
return await voip.delegate.mediaDevices
13381354
.getDisplayMedia(UserMediaConstraints.screenMediaConstraints);
1339-
} catch (e) {
1340-
await _getUserMediaFailed(e);
1355+
} catch (e, s) {
1356+
Logs().w('Failed to get display media', e, s);
1357+
fireCallEvent(CallStateChange.kError);
1358+
// We don't terminate the call here because the user might still want to stay
1359+
// on the call and try again later.
1360+
throw CallError(
1361+
CallErrorCode.displayMediaFailed,
1362+
'Failed to get display media',
1363+
e,
1364+
);
13411365
}
1342-
return null;
13431366
}
13441367

13451368
Future<RTCPeerConnection> _createPeerConnection() async {
@@ -1469,19 +1492,6 @@ class CallSession {
14691492
}
14701493
}
14711494

1472-
Future<void> _getLocalOfferFailed(dynamic err) async {
1473-
Logs().e('Failed to get local offer ${err.toString()}');
1474-
fireCallEvent(CallStateChange.kError);
1475-
1476-
await terminate(CallParty.kLocal, CallErrorCode.localOfferFailed, true);
1477-
}
1478-
1479-
Future<void> _getUserMediaFailed(dynamic err) async {
1480-
Logs().w('Failed to get user media - ending call ${err.toString()}');
1481-
fireCallEvent(CallStateChange.kError);
1482-
await terminate(CallParty.kLocal, CallErrorCode.userMediaFailed, true);
1483-
}
1484-
14851495
Future<void> onSelectAnswerReceived(String? selectedPartyId) async {
14861496
if (direction != CallDirection.kIncoming) {
14871497
Logs().w('Got select_answer for an outbound call: ignoring');

lib/src/voip/utils/types.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ enum CallErrorCode {
5050
/// the hardware isn't plugged in, or the user has explicitly denied access.
5151
userMediaFailed('user_media_failed'),
5252

53+
/// An error code when there is no local display to screenshare. This may be
54+
/// because the hardware isn't plugged in, or the user has explicitly denied
55+
/// access.
56+
displayMediaFailed('display_media_failed'),
57+
5358
/// Error code used when a call event failed to send
5459
/// because unknown devices were present in the room
5560
unknownDevice('unknown_device'),

test/calls_test.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,32 @@ void main() {
634634
}
635635
});
636636

637+
test('Call continues when getDisplayMedia fails', () async {
638+
final mockDelegate = MockWebRTCDelegate();
639+
mockDelegate.mediaDevices.throwOnGetDisplayMedia = true;
640+
voip = VoIP(matrix, mockDelegate);
641+
VoIP.customTxid = '1234';
642+
643+
final call = await voip.inviteToCall(
644+
room,
645+
CallType.kVoice,
646+
userId: '@alice:testing.com',
647+
);
648+
649+
// Attempt to share screen - should not throw or terminate call
650+
try {
651+
await call.setScreensharingEnabled(true);
652+
} catch (e) {
653+
fail('Screen sharing failure should be handled internally');
654+
}
655+
656+
expect(call.onCallEventChanged.value, CallStateChange.kError);
657+
expect(call.state, isNot(CallState.kEnded));
658+
expect(voip.currentCID, isNotNull);
659+
660+
await call.hangup(reason: CallErrorCode.userHangup);
661+
});
662+
637663
test('getFamedlyCallEvents sort order', () {
638664
room.setState(
639665
Event(

test/webrtc_stub.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ class MockWebRTCDelegate implements WebRTCDelegate {
5454
@override
5555
bool get isWeb => false;
5656

57+
final _mockMediaDevices = MockMediaDevices();
58+
5759
@override
58-
MediaDevices get mediaDevices => MockMediaDevices();
60+
MockMediaDevices get mediaDevices => _mockMediaDevices;
5961

6062
@override
6163
Future<void> playRingtone() async {
@@ -111,6 +113,8 @@ class MockMediaDeviceInfo implements MediaDeviceInfo {
111113
}
112114

113115
class MockMediaDevices implements MediaDevices {
116+
bool throwOnGetDisplayMedia = false;
117+
114118
@override
115119
Function(dynamic event)? ondevicechange;
116120

@@ -134,6 +138,9 @@ class MockMediaDevices implements MediaDevices {
134138
Future<MediaStream> getDisplayMedia(
135139
Map<String, dynamic> mediaConstraints,
136140
) async {
141+
if (throwOnGetDisplayMedia) {
142+
throw Exception('mock exception while getting display media');
143+
}
137144
return MockMediaStream('', '');
138145
}
139146

0 commit comments

Comments
 (0)