Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ coverage/
coverage_badge.svg
coverage.xml
TEST-report.*
coverage_dir/*

# IntelliJ related
*.iml
Expand Down
47 changes: 42 additions & 5 deletions lib/src/voip/call_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,6 @@
await addLocalStream(stream, SDPStreamMetadataPurpose.Screenshare);
return true;
} catch (err) {
fireCallEvent(CallStateChange.kError);
return false;
}
} else {
Expand Down Expand Up @@ -1218,7 +1217,9 @@
}
};
} catch (e) {
Logs().v('[VOIP] prepareMediaStream error => ${e.toString()}');
Logs().v('[VOIP] preparePeerConnection error => ${e.toString()}');
await _createPeerConnectionFailed(e);
rethrow;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this rethrow here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, forgot to remove it

}
}

Expand Down Expand Up @@ -1313,16 +1314,16 @@
return await voip.delegate.mediaDevices.getUserMedia(mediaConstraints);
} catch (e) {
await _getUserMediaFailed(e);
rethrow;
}
return null;
}

Future<MediaStream?> _getDisplayMedia() async {
try {
return await voip.delegate.mediaDevices
.getDisplayMedia(UserMediaConstraints.screenMediaConstraints);
} catch (e) {
await _getUserMediaFailed(e);
await _getDisplayMediaFailed(e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm this is a bit difficult to read, I don't think _getDisplayMediaFailed is reused, can we maybe remove the *failed methods and move the implementations in this block? it saves us the code jumping + might also remove the return null

}
return null;
}
Expand Down Expand Up @@ -1454,17 +1455,53 @@
}
}

Future<void> _createPeerConnectionFailed(dynamic err) async {
Logs().e('Failed to create peer connection object ${err.toString()}');
fireCallEvent(CallStateChange.kError);
await terminate(
CallParty.kLocal,
CallErrorCode.createPeerConnectionFailed,
true,
);
throw CallError(
CallErrorCode.createPeerConnectionFailed,
'Failed to create peer connection object ',
err,
);
}

Future<void> _getLocalOfferFailed(dynamic err) async {
Logs().e('Failed to get local offer ${err.toString()}');
fireCallEvent(CallStateChange.kError);

await terminate(CallParty.kLocal, CallErrorCode.localOfferFailed, true);
throw CallError(

Check warning on line 1477 in lib/src/voip/call_session.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/voip/call_session.dart#L1477

Added line #L1477 was not covered by tests
CallErrorCode.localOfferFailed,
'Failed to get local offer',
err,
);
}

Future<void> _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);
throw CallError(

Check warning on line 1488 in lib/src/voip/call_session.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/voip/call_session.dart#L1488

Added line #L1488 was not covered by tests
CallErrorCode.userMediaFailed,
'Failed to get user media',
err,
);
}

Future<void> _getDisplayMediaFailed(dynamic err) async {
Logs().w('Failed to get display media - ending call ${err.toString()}');
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',
err,
);
}

Future<void> onSelectAnswerReceived(String? selectedPartyId) async {
Expand Down
8 changes: 8 additions & 0 deletions lib/src/voip/utils/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,21 @@ 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'),

/// An error code when there is no local mic/camera to use. This may be because
/// 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'),
Expand Down
46 changes: 46 additions & 0 deletions test/calls_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,52 @@ 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<CallError>());
expect((e as CallError).code, CallErrorCode.createPeerConnectionFailed);
expect(voip.currentCID, null);
}
});

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(
Expand Down
25 changes: 20 additions & 5 deletions test/webrtc_stub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@ import 'package:webrtc_interface/webrtc_interface.dart';
import 'package:matrix/matrix.dart';

class MockWebRTCDelegate implements WebRTCDelegate {
bool throwOnCreatePeerConnection = false;

@override
bool get canHandleNewCall => true;

@override
Future<RTCPeerConnection> createPeerConnection(
Map<String, dynamic> configuration, [
Map<String, dynamic> constraints = const {},
]) async =>
MockRTCPeerConnection();
]) async {
if (throwOnCreatePeerConnection) {
throw Exception('mock exception while creating peer connection');
}
return MockRTCPeerConnection();
}

@override
Future<void> registerListeners(CallSession session) async {
Expand Down Expand Up @@ -48,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<void> playRingtone() async {
Expand Down Expand Up @@ -87,6 +95,8 @@ class MockEncryptionKeyProvider implements EncryptionKeyProvider {
}

class MockMediaDevices implements MediaDevices {
bool throwOnGetDisplayMedia = false;

@override
Function(dynamic event)? ondevicechange;

Expand All @@ -96,8 +106,13 @@ class MockMediaDevices implements MediaDevices {
}

@override
Future<MediaStream> getDisplayMedia(Map<String, dynamic> mediaConstraints) {
throw UnimplementedError();
Future<MediaStream> getDisplayMedia(
Map<String, dynamic> mediaConstraints,
) async {
if (throwOnGetDisplayMedia) {
throw Exception('mock exception while getting display media');
}
return MockMediaStream('', '');
}

@override
Expand Down