diff --git a/lib/src/voip/call_session.dart b/lib/src/voip/call_session.dart index 9a96c6880..0395e8510 100644 --- a/lib/src/voip/call_session.dart +++ b/lib/src/voip/call_session.dart @@ -44,6 +44,7 @@ class CallSession { String? get groupCallId => opts.groupCallId; String get callId => opts.callId; String get localPartyId => opts.localPartyId; + VoipId get voipId => VoipId(roomId: room.id, callId: callId); CallDirection get direction => opts.dir; @@ -216,8 +217,9 @@ class CallSession { final prevCallId = voip.incomingCallRoomId[room.id]; if (prevCallId != null) { // This is probably an outbound call, but we already have a incoming invite, so let's terminate it. - final prevCall = - voip.calls[VoipId(roomId: room.id, callId: prevCallId)]; + final prevCall = voip.calls.singleWhereOrNull((element) => + element.room.id == room.id && element.callId == prevCallId); + if (prevCall != null) { if (prevCall._inviteOrAnswerSent) { Logs().d('[glare] invite or answer sent, lex compare now'); @@ -975,7 +977,7 @@ class CallSession { voip.incomingCallRoomId.removeWhere((key, value) => value == callId); } - voip.calls.removeWhere((key, value) => key.callId == callId); + voip.calls.remove(this); await cleanUp(); if (shouldEmit) { diff --git a/lib/src/voip/group_call_session.dart b/lib/src/voip/group_call_session.dart index 3535a06f2..16d7a552b 100644 --- a/lib/src/voip/group_call_session.dart +++ b/lib/src/voip/group_call_session.dart @@ -32,6 +32,13 @@ class GroupCallSession { final Client client; final VoIP voip; final Room room; + VoipId get voipId => VoipId( + roomId: room.id, + callId: groupCallId, + callBackendType: backend.type, + application: application, + scope: scope, + ); /// is a list of backend to allow passing multiple backend in the future /// we use the first backend everywhere as of now @@ -131,7 +138,13 @@ class GroupCallSession { await backend.setupP2PCallsWithExistingMembers(this); - voip.currentGroupCID = VoipId(roomId: room.id, callId: groupCallId); + voip.currentGroupCID = VoipId( + roomId: room.id, + callId: groupCallId, + callBackendType: backend.type, + application: application, + scope: scope, + ); await voip.delegate.handleNewGroupCall(this); } @@ -142,7 +155,7 @@ class GroupCallSession { setState(GroupCallState.localCallFeedUninitialized); voip.currentGroupCID = null; _participants.clear(); - voip.groupCalls.remove(VoipId(roomId: room.id, callId: groupCallId)); + voip.groupCalls.remove(this); await voip.delegate.handleGroupCallEnded(this); _resendMemberStateEventTimer?.cancel(); setState(GroupCallState.ended); diff --git a/lib/src/voip/models/voip_id.dart b/lib/src/voip/models/voip_id.dart index 19523b4b5..b0a1241db 100644 --- a/lib/src/voip/models/voip_id.dart +++ b/lib/src/voip/models/voip_id.dart @@ -1,24 +1,33 @@ class VoipId { final String roomId; final String callId; + final String? callBackendType; + final String? application; + final String? scope; - String get id => '$roomId:$callId'; - - factory VoipId.fromId(String id) { - final int lastIndex = id.lastIndexOf(':'); - return VoipId( - roomId: id.substring(0, lastIndex), - callId: id.substring(lastIndex + 1), - ); - } - - VoipId({required this.roomId, required this.callId}); + VoipId({ + required this.roomId, + required this.callId, + this.callBackendType, + this.application, + this.scope, + }); @override bool operator ==(Object other) => identical(this, other) || - other is VoipId && roomId == other.roomId && callId == other.callId; + other is VoipId && + roomId == other.roomId && + callId == other.callId && + callBackendType == other.callBackendType && + application == other.application && + scope == other.scope; @override - int get hashCode => roomId.hashCode ^ callId.hashCode; + int get hashCode => + roomId.hashCode ^ + callId.hashCode ^ + callBackendType.hashCode ^ + application.hashCode ^ + scope.hashCode; } diff --git a/lib/src/voip/utils/famedly_call_extension.dart b/lib/src/voip/utils/famedly_call_extension.dart index 0f7b6d4a5..591a29e84 100644 --- a/lib/src/voip/utils/famedly_call_extension.dart +++ b/lib/src/voip/utils/famedly_call_extension.dart @@ -40,6 +40,17 @@ extension FamedlyCallMemberEventsExtension on Room { return mem ?? []; } + /// returns a list of memberships in the room for `callParticipant` + List getCallMembershipsForUserWithDeviceId( + CallParticipant callParticipant, + ) { + final userMems = getCallMembershipsForUser(callParticipant.userId); + final mem = userMems + .where((element) => element.deviceId == callParticipant.deviceId) + .toList(); + return mem; + } + /// returns the user count (not sessions, yet) for the group call with id: `groupCallId`. /// returns 0 if group call not found int groupCallParticipantCount(String groupCallId) { diff --git a/lib/src/voip/voip.dart b/lib/src/voip/voip.dart index 6d381a7a4..1c0839c89 100644 --- a/lib/src/voip/voip.dart +++ b/lib/src/voip/voip.dart @@ -30,11 +30,11 @@ class VoIP { /// cached turn creds TurnServerCredentials? _turnServerCredentials; - Map get calls => _calls; - final Map _calls = {}; + List get calls => _calls; + final List _calls = []; - Map get groupCalls => _groupCalls; - final Map _groupCalls = {}; + List get groupCalls => _groupCalls; + final List _groupCalls = []; final CachedStreamController onIncomingCall = CachedStreamController(); @@ -100,11 +100,11 @@ class VoIP { for (final mem in mems) { unawaited(createGroupCallFromRoomStateEvent(mem)); } - for (final map in groupCalls.entries) { - if (map.key.roomId == event.room.id) { + for (final gc in groupCalls) { + if (gc.room.id == event.room.id) { // because we don't know which call got updated, just update all // group calls we have entered for that room - await map.value.onMemberStateChanged(); + await gc.onMemberStateChanged(); } } }, @@ -164,6 +164,7 @@ class VoIP { // member event updates handled in onRoomState for ease if (event.type == EventTypes.GroupCallMember) return; + CallSession? callSession; GroupCallSession? groupCallSession; Room? room; final remoteUserId = event.senderId; @@ -177,13 +178,24 @@ class VoIP { } else if (event is ToDeviceEvent) { final roomId = event.content.tryGet('room_id'); final confId = event.content.tryGet('conf_id'); + final isLivekitToDeviceEvent = + event.type.startsWith(EventTypes.GroupCallMemberEncryptionKeys); /// to-device events specifically, m.call.invite and encryption key sending and requesting remoteDeviceId = event.content.tryGet('device_id'); if (roomId != null && confId != null) { room = client.getRoomById(roomId); - groupCallSession = groupCalls[VoipId(roomId: roomId, callId: confId)]; + groupCallSession = groupCalls.singleWhereOrNull( + (element) => + element.room.id == roomId && + element.groupCallId == confId && + element.backend.type == + (isLivekitToDeviceEvent ? 'livekit' : 'mesh'), + // ideally you would want to check application and scope using + // currentGroupCid here, but td is not sure if that will be + // set before the first todevice event like m.call.invite + ); } else { Logs().w( '[VOIP] Ignoring to_device event of type ${event.type} but did not find group call for id: $confId'); @@ -225,33 +237,37 @@ class VoIP { return; } if (callId != null) { - final call = calls[VoipId(roomId: room.id, callId: callId)]; - if (call == null && + callSession = calls.singleWhereOrNull((element) => + element.room.id == room!.id && element.callId == callId); + + if (callSession == null && !{EventTypes.CallInvite, EventTypes.GroupCallMemberInvite} .contains(event.type)) { Logs().w( 'Ignoring call event ${event.type} because we do not have the call'); return; - } else if (call != null) { + } else if (callSession != null) { // multiple checks to make sure the events sent are from the the // expected party - if (call.room.id != room.id) { + if (callSession.room.id != room.id) { Logs().w( - 'Ignoring call event ${event.type} for room ${room.id} claiming to be for call in room ${call.room.id}'); + 'Ignoring call event ${event.type} for room ${room.id} claiming to be for call in room ${callSession.room.id}'); return; } - if (call.remoteUserId != null && call.remoteUserId != remoteUserId) { + if (callSession.remoteUserId != null && + callSession.remoteUserId != remoteUserId) { Logs().w( - 'Ignoring call event ${event.type} from sender $remoteUserId, expected sender: ${call.remoteUserId}'); + 'Ignoring call event ${event.type} from sender $remoteUserId, expected sender: ${callSession.remoteUserId}'); return; } - if (call.remotePartyId != null && call.remotePartyId != partyId) { + if (callSession.remotePartyId != null && + callSession.remotePartyId != partyId) { Logs().w( - 'Ignoring call event ${event.type} from sender with a different party_id $partyId, expected party_id: ${call.remotePartyId}'); + 'Ignoring call event ${event.type} from sender with a different party_id $partyId, expected party_id: ${callSession.remotePartyId}'); return; } - if ((call.remotePartyId != null && - call.remotePartyId == localPartyId) || + if ((callSession.remotePartyId != null && + callSession.remotePartyId == localPartyId) || (remoteUserId == client.userID && remoteDeviceId == client.deviceID!)) { Logs().w('Ignoring call event ${event.type} from ourself'); @@ -266,46 +282,85 @@ class VoIP { switch (event.type) { case EventTypes.CallInvite: case EventTypes.GroupCallMemberInvite: - await onCallInvite(room, remoteUserId, remoteDeviceId, content); + await onCallInvite( + room, + remoteUserId, + remoteDeviceId, + content, + callSession, + ); break; case EventTypes.CallAnswer: case EventTypes.GroupCallMemberAnswer: - await onCallAnswer(room, remoteUserId, remoteDeviceId, content); + await onCallAnswer( + room, + remoteUserId, + remoteDeviceId, + content, + callSession, + ); break; case EventTypes.CallCandidates: case EventTypes.GroupCallMemberCandidates: - await onCallCandidates(room, content); + await onCallCandidates( + room, + content, + callSession, + ); break; case EventTypes.CallHangup: case EventTypes.GroupCallMemberHangup: - await onCallHangup(room, content); + await onCallHangup( + room, + content, + callSession, + ); break; case EventTypes.CallReject: case EventTypes.GroupCallMemberReject: - await onCallReject(room, content); + await onCallReject( + room, + content, + callSession, + ); break; case EventTypes.CallNegotiate: case EventTypes.GroupCallMemberNegotiate: - await onCallNegotiate(room, content); + await onCallNegotiate( + room, + content, + callSession, + ); break; - // case EventTypes.CallReplaces: - // await onCallReplaces(room, content); - // break; + case EventTypes.CallSelectAnswer: case EventTypes.GroupCallMemberSelectAnswer: - await onCallSelectAnswer(room, content); + await onCallSelectAnswer( + room, + content, + callSession, + ); break; case EventTypes.CallSDPStreamMetadataChanged: case EventTypes.CallSDPStreamMetadataChangedPrefix: case EventTypes.GroupCallMemberSDPStreamMetadataChanged: - await onSDPStreamMetadataChangedReceived(room, content); + await onSDPStreamMetadataChangedReceived( + room, + content, + callSession, + ); break; case EventTypes.CallAssertedIdentity: case EventTypes.CallAssertedIdentityPrefix: case EventTypes.GroupCallMemberAssertedIdentity: - await onAssertedIdentityReceived(room, content); + await onAssertedIdentityReceived( + room, + content, + callSession, + ); break; case EventTypes.GroupCallMemberEncryptionKeys: + // we already did a groupCallSession null check on toDeviceEvent above await groupCallSession!.backend.onCallEncryption( groupCallSession, remoteUserId, remoteDeviceId!, content); break; @@ -318,20 +373,25 @@ class VoIP { Future _onDeviceChange(dynamic _) async { Logs().v('[VOIP] _onDeviceChange'); - for (final call in calls.values) { + for (final call in calls) { if (call.state == CallState.kConnected && !call.isGroupCall) { await call.updateMediaDeviceForCall(); } } - for (final groupCall in groupCalls.values) { + for (final groupCall in groupCalls) { if (groupCall.state == GroupCallState.entered) { await groupCall.backend.updateMediaDeviceForCalls(); } } } - Future onCallInvite(Room room, String remoteUserId, - String? remoteDeviceId, Map content) async { + Future onCallInvite( + Room room, + String remoteUserId, + String? remoteDeviceId, + Map content, + CallSession? call, + ) async { Logs().v( '[VOIP] onCallInvite $remoteUserId:$remoteDeviceId => ${client.userID}:${client.deviceID}, \ncontent => ${content.toString()}'); @@ -339,8 +399,6 @@ class VoIP { final int lifetime = content['lifetime']; final String? confId = content['conf_id']; - final call = calls[VoipId(roomId: room.id, callId: callId)]; - Logs().d( '[glare] got new call ${content.tryGet('call_id')} and currently room id is mapped to ${incomingCallRoomId.tryGet(room.id)}'); @@ -407,11 +465,14 @@ class VoIP { newCall.remotePartyId = content['party_id']; newCall.remoteSessionId = content['sender_session_id']; - // newCall.remoteSessionId = remoteParticipant.sessionId; - - if (!delegate.canHandleNewCall && - (confId == null || - currentGroupCID != VoipId(roomId: room.id, callId: confId))) { + if (!delegate + .canHandleNewCall && // trigger missed call if you cannot handle new calls + (confId == null || // except when it's a group call + (currentGroupCID != null && + ( // confirms you are in a group call + currentGroupCID!.roomId != + room.id || // but the ids don't match so we ignore this + currentGroupCID!.callId != confId)))) { Logs().v( '[VOIP] onCallInvite: Unable to handle new calls, maybe user is busy.'); // no need to emit here because handleNewCall was never triggered yet @@ -456,12 +517,16 @@ class VoIP { } } - Future onCallAnswer(Room room, String remoteUserId, - String? remoteDeviceId, Map content) async { + Future onCallAnswer( + Room room, + String remoteUserId, + String? remoteDeviceId, + Map content, + CallSession? call, + ) async { Logs().v('[VOIP] onCallAnswer => ${content.toString()}'); final String callId = content['call_id']; - final call = calls[VoipId(roomId: room.id, callId: callId)]; if (call != null) { if (!call.answeredByUs) { await delegate.stopRingtone(); @@ -508,10 +573,14 @@ class VoIP { } } - Future onCallCandidates(Room room, Map content) async { + Future onCallCandidates( + Room room, + Map content, + CallSession? call, + ) async { Logs().v('[VOIP] onCallCandidates => ${content.toString()}'); final String callId = content['call_id']; - final call = calls[VoipId(roomId: room.id, callId: callId)]; + if (call != null) { await call.onCandidatesReceived(content['candidates']); } else { @@ -519,13 +588,16 @@ class VoIP { } } - Future onCallHangup(Room room, Map content) async { + Future onCallHangup( + Room room, + Map content, + CallSession? call, + ) async { // stop play ringtone, if this is an incoming call await delegate.stopRingtone(); Logs().v('[VOIP] onCallHangup => ${content.toString()}'); final String callId = content['call_id']; - final call = calls[VoipId(roomId: room.id, callId: callId)]; if (call != null) { // hangup in any case, either if the other party hung up or we did on another device await call.terminate( @@ -542,11 +614,14 @@ class VoIP { } } - Future onCallReject(Room room, Map content) async { + Future onCallReject( + Room room, + Map content, + CallSession? call, + ) async { final String callId = content['call_id']; Logs().d('Reject received for call ID $callId'); - final call = calls[VoipId(roomId: room.id, callId: callId)]; if (call != null) { await call.onRejectReceived( CallErrorCode.values.firstWhereOrNull( @@ -559,12 +634,14 @@ class VoIP { } Future onCallSelectAnswer( - Room room, Map content) async { + Room room, + Map content, + CallSession? call, + ) async { final String callId = content['call_id']; Logs().d('SelectAnswer received for call ID $callId'); final String selectedPartyId = content['selected_party_id']; - final call = calls[VoipId(roomId: room.id, callId: callId)]; if (call != null) { if (call.room.id != room.id) { Logs().w( @@ -576,11 +653,13 @@ class VoIP { } Future onSDPStreamMetadataChangedReceived( - Room room, Map content) async { + Room room, + Map content, + CallSession? call, + ) async { final String callId = content['call_id']; Logs().d('SDP Stream metadata received for call ID $callId'); - final call = calls[VoipId(roomId: room.id, callId: callId)]; if (call != null) { if (content[sdpStreamMetadataKey] == null) { Logs().d('SDP Stream metadata is null'); @@ -592,11 +671,13 @@ class VoIP { } Future onAssertedIdentityReceived( - Room room, Map content) async { + Room room, + Map content, + CallSession? call, + ) async { final String callId = content['call_id']; Logs().d('Asserted identity received for call ID $callId'); - final call = calls[VoipId(roomId: room.id, callId: callId)]; if (call != null) { if (content['asserted_identity'] == null) { Logs().d('asserted_identity is null '); @@ -607,11 +688,14 @@ class VoIP { } } - Future onCallNegotiate(Room room, Map content) async { + Future onCallNegotiate( + Room room, + Map content, + CallSession? call, + ) async { final String callId = content['call_id']; Logs().d('Negotiate received for call ID $callId'); - final call = calls[VoipId(roomId: room.id, callId: callId)]; if (call != null) { // ideally you also check the lifetime here and discard negotiation events // if age of the event was older than the lifetime but as to device events @@ -711,7 +795,7 @@ class VoIP { CallSession createNewCall(CallOptions opts) { final call = CallSession(opts); - calls[VoipId(roomId: opts.room.id, callId: opts.callId)] = call; + calls.add(call); return call; } @@ -729,11 +813,6 @@ class VoIP { String? application, String? scope, ) async { - if (getGroupCallById(room.id, groupCallId) != null) { - Logs().v('[VOIP] [$groupCallId] already exists.'); - return getGroupCallById(room.id, groupCallId)!; - } - final groupCall = GroupCallSession( groupCallId: groupCallId, client: client, @@ -744,7 +823,7 @@ class VoIP { scope: scope, ); - setGroupCallById(groupCall); + groupCalls.add(groupCall); return groupCall; } @@ -764,14 +843,22 @@ class VoIP { String? application, String? scope, ) async { - final groupCall = getGroupCallById(room.id, groupCallId); + final existingGroupCall = groupCalls.singleWhereOrNull( + (element) => + element.room.id == room.id && + element.groupCallId == groupCallId && + element.backend.type == backend.type && + element.application == application && + element.scope == scope, + ); - if (groupCall != null) { + if (existingGroupCall != null) { if (!room.canJoinGroupCall) { throw Exception( 'User is not allowed to join famedly calls in the room'); } - return groupCall; + Logs().v('[VOIP] [$groupCallId] already exists.'); + return existingGroupCall; } if (!room.groupCallsEnabledForEveryone) { @@ -788,17 +875,6 @@ class VoIP { ); } - GroupCallSession? getGroupCallById(String roomId, String groupCallId) { - return groupCalls[VoipId(roomId: roomId, callId: groupCallId)]; - } - - void setGroupCallById(GroupCallSession groupCallSession) { - groupCalls[VoipId( - roomId: groupCallSession.room.id, - callId: groupCallSession.groupCallId, - )] = groupCallSession; - } - /// Create a new group call from a room state event. Future createGroupCallFromRoomStateEvent( CallMembership membership, { @@ -822,6 +898,17 @@ class VoIP { return; } + final existingGroupCall = groupCalls.singleWhereOrNull( + (element) => + element.room.id == membership.roomId && + element.groupCallId == membership.callId && + element.backend.type == membership.backend.type && + element.application == membership.application && + element.scope == membership.scope, + ); + + if (existingGroupCall != null) return; + final groupCall = GroupCallSession( client: client, voip: this, @@ -832,12 +919,7 @@ class VoIP { scope: membership.scope, ); - if (groupCalls.containsKey( - VoipId(roomId: membership.roomId, callId: membership.callId))) { - return; - } - - setGroupCallById(groupCall); + groupCalls.add(groupCall); onIncomingGroupCall.add(groupCall); if (emitHandleNewGroupCall) { diff --git a/test/calls_test.dart b/test/calls_test.dart index 7ecbace70..de6f7131a 100644 --- a/test/calls_test.dart +++ b/test/calls_test.dart @@ -173,7 +173,8 @@ void main() { } expect(voip.currentCID, VoipId(roomId: room.id, callId: 'originTsValidCall')); - final call = voip.calls[voip.currentCID]!; + final call = voip.calls + .singleWhere((element) => element.voipId == voip.currentCID); expect(call.state, CallState.kRinging); await call.answer(txid: '1234'); @@ -300,7 +301,8 @@ void main() { } expect( voip.currentCID, VoipId(roomId: room.id, callId: 'answer_elseWhere')); - final call = voip.calls[voip.currentCID]!; + final call = voip.calls + .singleWhere((element) => element.voipId == voip.currentCID); expect(call.state, CallState.kRinging); // caller sends select answer @@ -392,7 +394,8 @@ void main() { Logs().d('Waiting for currentCID to update'); } expect(voip.currentCID, VoipId(roomId: room.id, callId: 'reject_call')); - final call = voip.calls[voip.currentCID]!; + final call = voip.calls + .singleWhere((element) => element.voipId == voip.currentCID); expect(call.state, CallState.kRinging); await call.reject();