LCOV - code coverage report
Current view: top level - lib/src/voip - voip.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 181 374 48.4 %
Date: 2024-09-28 12:47:43 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:async';
       2             : import 'dart:convert';
       3             : import 'dart:core';
       4             : 
       5             : import 'package:collection/collection.dart';
       6             : import 'package:sdp_transform/sdp_transform.dart' as sdp_transform;
       7             : import 'package:webrtc_interface/webrtc_interface.dart';
       8             : 
       9             : import 'package:matrix/matrix.dart';
      10             : import 'package:matrix/src/utils/cached_stream_controller.dart';
      11             : import 'package:matrix/src/utils/crypto/crypto.dart';
      12             : import 'package:matrix/src/voip/models/call_membership.dart';
      13             : import 'package:matrix/src/voip/models/call_options.dart';
      14             : import 'package:matrix/src/voip/models/voip_id.dart';
      15             : import 'package:matrix/src/voip/utils/stream_helper.dart';
      16             : 
      17             : /// The parent highlevel voip class, this trnslates matrix events to webrtc methods via
      18             : /// `CallSession` or `GroupCallSession` methods
      19             : class VoIP {
      20             :   // used only for internal tests, all txids for call events will be overwritten to this
      21             :   static String? customTxid;
      22             : 
      23             :   /// set to true if you want to use the ratcheting mechanism with your keyprovider
      24             :   /// remember to set the window size correctly on your keyprovider
      25             :   ///
      26             :   /// at client level because reinitializing a `GroupCallSession` and its `KeyProvider`
      27             :   /// everytime this changed would be a pain
      28             :   final bool enableSFUE2EEKeyRatcheting;
      29             : 
      30             :   /// cached turn creds
      31             :   TurnServerCredentials? _turnServerCredentials;
      32             : 
      33           4 :   Map<VoipId, CallSession> get calls => _calls;
      34             :   final Map<VoipId, CallSession> _calls = {};
      35             : 
      36           4 :   Map<VoipId, GroupCallSession> get groupCalls => _groupCalls;
      37             :   final Map<VoipId, GroupCallSession> _groupCalls = {};
      38             : 
      39             :   final CachedStreamController<CallSession> onIncomingCall =
      40             :       CachedStreamController();
      41             : 
      42             :   VoipId? currentCID;
      43             :   VoipId? currentGroupCID;
      44             : 
      45           4 :   String get localPartyId => currentSessionId;
      46             : 
      47             :   final Client client;
      48             :   final WebRTCDelegate delegate;
      49             :   final StreamController<GroupCallSession> onIncomingGroupCall =
      50             :       StreamController();
      51             : 
      52           6 :   CallParticipant? get localParticipant => client.isLogged()
      53           2 :       ? CallParticipant(
      54             :           this,
      55           4 :           userId: client.userID!,
      56           4 :           deviceId: client.deviceID,
      57             :         )
      58             :       : null;
      59             : 
      60             :   /// map of roomIds to the invites they are currently processing or in a call with
      61             :   /// used for handling glare in p2p calls
      62           4 :   Map<String, String> get incomingCallRoomId => _incomingCallRoomId;
      63             :   final Map<String, String> _incomingCallRoomId = {};
      64             : 
      65             :   /// the current instance of voip, changing this will drop any ongoing mesh calls
      66             :   /// with that sessionId
      67             :   late String currentSessionId;
      68           2 :   VoIP(
      69             :     this.client,
      70             :     this.delegate, {
      71             :     this.enableSFUE2EEKeyRatcheting = false,
      72           2 :   }) : super() {
      73           6 :     currentSessionId = base64Encode(secureRandomBytes(16));
      74           8 :     Logs().v('set currentSessionId to $currentSessionId');
      75             :     // to populate groupCalls with already present calls
      76           6 :     for (final room in client.rooms) {
      77           2 :       final memsList = room.getCallMembershipsFromRoom();
      78           2 :       for (final mems in memsList.values) {
      79           0 :         for (final mem in mems) {
      80           0 :           unawaited(createGroupCallFromRoomStateEvent(mem));
      81             :         }
      82             :       }
      83             :     }
      84             : 
      85             :     /// handles events todevice and matrix events for invite, candidates, hangup, etc.
      86          10 :     client.onCallEvents.stream.listen((events) async {
      87           2 :       await _handleCallEvents(events);
      88             :     });
      89             : 
      90             :     // handles the com.famedly.call events.
      91           8 :     client.onRoomState.stream.listen(
      92           2 :       (update) async {
      93             :         final event = update.state;
      94           2 :         if (event is! Event) return;
      95           6 :         if (event.room.membership != Membership.join) return;
      96           4 :         if (event.type != EventTypes.GroupCallMember) return;
      97             : 
      98           8 :         Logs().v('[VOIP] onRoomState: type ${event.toJson()}');
      99           4 :         final mems = event.room.getCallMembershipsFromEvent(event);
     100           4 :         for (final mem in mems) {
     101           4 :           unawaited(createGroupCallFromRoomStateEvent(mem));
     102             :         }
     103           6 :         for (final map in groupCalls.entries) {
     104          10 :           if (map.key.roomId == event.room.id) {
     105             :             // because we don't know which call got updated, just update all
     106             :             // group calls we have entered for that room
     107           4 :             await map.value.onMemberStateChanged();
     108             :           }
     109             :         }
     110             :       },
     111             :     );
     112             : 
     113           8 :     delegate.mediaDevices.ondevicechange = _onDeviceChange;
     114             :   }
     115             : 
     116           2 :   Future<void> _handleCallEvents(List<BasicEventWithSender> callEvents) async {
     117             :     // Call invites should be omitted for a call that is already answered,
     118             :     // has ended, is rejectd or replaced.
     119           2 :     final callEventsCopy = List<BasicEventWithSender>.from(callEvents);
     120           4 :     for (final callEvent in callEventsCopy) {
     121           4 :       final callId = callEvent.content.tryGet<String>('call_id');
     122             : 
     123           4 :       if (CallConstants.callEndedEventTypes.contains(callEvent.type)) {
     124           0 :         callEvents.removeWhere((event) {
     125           0 :           if (CallConstants.omitWhenCallEndedTypes.contains(event.type) &&
     126           0 :               event.content.tryGet<String>('call_id') == callId) {
     127           0 :             Logs().v(
     128           0 :                 'Ommit "${event.type}" event for an already terminated call');
     129             :             return true;
     130             :           }
     131             : 
     132             :           return false;
     133             :         });
     134             :       }
     135             : 
     136             :       // checks for ended events and removes invites for that call id.
     137           2 :       if (callEvent is Event) {
     138             :         // removes expired invites
     139           4 :         final age = callEvent.unsigned?.tryGet<int>('age') ??
     140           6 :             (DateTime.now().millisecondsSinceEpoch -
     141           4 :                 callEvent.originServerTs.millisecondsSinceEpoch);
     142             : 
     143           4 :         callEvents.removeWhere((element) {
     144           4 :           if (callEvent.type == EventTypes.CallInvite &&
     145           2 :               age >
     146           4 :                   (callEvent.content.tryGet<int>('lifetime') ??
     147           0 :                       CallTimeouts.callInviteLifetime.inMilliseconds)) {
     148           4 :             Logs().w(
     149           4 :                 '[VOIP] Ommiting invite event ${callEvent.eventId} as age was older than lifetime');
     150             :             return true;
     151             :           }
     152             :           return false;
     153             :         });
     154             :       }
     155             :     }
     156             : 
     157             :     // and finally call the respective methods on the clean callEvents list
     158           4 :     for (final callEvent in callEvents) {
     159           2 :       await _handleCallEvent(callEvent);
     160             :     }
     161             :   }
     162             : 
     163           2 :   Future<void> _handleCallEvent(BasicEventWithSender event) async {
     164             :     // member event updates handled in onRoomState for ease
     165           4 :     if (event.type == EventTypes.GroupCallMember) return;
     166             : 
     167             :     GroupCallSession? groupCallSession;
     168             :     Room? room;
     169           2 :     final remoteUserId = event.senderId;
     170             :     String? remoteDeviceId;
     171             : 
     172           2 :     if (event is Event) {
     173           2 :       room = event.room;
     174             : 
     175             :       /// this can also be sent in p2p calls when they want to call a specific device
     176           4 :       remoteDeviceId = event.content.tryGet<String>('invitee_device_id');
     177           0 :     } else if (event is ToDeviceEvent) {
     178           0 :       final roomId = event.content.tryGet<String>('room_id');
     179           0 :       final confId = event.content.tryGet<String>('conf_id');
     180             : 
     181             :       /// to-device events specifically, m.call.invite and encryption key sending and requesting
     182           0 :       remoteDeviceId = event.content.tryGet<String>('device_id');
     183             : 
     184             :       if (roomId != null && confId != null) {
     185           0 :         room = client.getRoomById(roomId);
     186           0 :         groupCallSession = groupCalls[VoipId(roomId: roomId, callId: confId)];
     187             :       } else {
     188           0 :         Logs().w(
     189           0 :             '[VOIP] Ignoring to_device event of type ${event.type} but did not find group call for id: $confId');
     190             :         return;
     191             :       }
     192             : 
     193           0 :       if (!event.type.startsWith(EventTypes.GroupCallMemberEncryptionKeys)) {
     194             :         // livekit calls have their own session deduplication logic so ignore sessionId deduplication for them
     195           0 :         final destSessionId = event.content.tryGet<String>('dest_session_id');
     196           0 :         if (destSessionId != currentSessionId) {
     197           0 :           Logs().w(
     198           0 :               '[VOIP] Ignoring to_device event of type ${event.type} did not match currentSessionId: $currentSessionId, dest_session_id was set to $destSessionId');
     199             :           return;
     200             :         }
     201             :       } else if (groupCallSession == null || remoteDeviceId == null) {
     202           0 :         Logs().w(
     203           0 :             '[VOIP] _handleCallEvent ${event.type} recieved but either groupCall ${groupCallSession?.groupCallId} or deviceId $remoteDeviceId was null, ignoring');
     204             :         return;
     205             :       }
     206             :     } else {
     207           0 :       Logs().w(
     208           0 :           '[VOIP] _handleCallEvent can only handle Event or ToDeviceEvent, it got ${event.runtimeType}');
     209             :       return;
     210             :     }
     211             : 
     212           2 :     final content = event.content;
     213             : 
     214             :     if (room == null) {
     215           0 :       Logs().w(
     216             :           '[VOIP] _handleCallEvent call event does not contain a room_id, ignoring');
     217             :       return;
     218           4 :     } else if (client.userID != null &&
     219           4 :         client.deviceID != null &&
     220           6 :         remoteUserId == client.userID &&
     221           0 :         remoteDeviceId == client.deviceID) {
     222           0 :       Logs().v(
     223           0 :           'Ignoring call event ${event.type} for room ${room.id} from our own device');
     224             :       return;
     225           2 :     } else if (!event.type
     226           2 :         .startsWith(EventTypes.GroupCallMemberEncryptionKeys)) {
     227             :       // skip webrtc event checks on encryption_keys
     228           2 :       final callId = content['call_id'] as String?;
     229           2 :       final partyId = content['party_id'] as String?;
     230           0 :       if (callId == null && event.type.startsWith('m.call')) {
     231           0 :         Logs().w('Ignoring call event ${event.type} because call_id was null');
     232             :         return;
     233             :       }
     234             :       if (callId != null) {
     235           8 :         final call = calls[VoipId(roomId: room.id, callId: callId)];
     236             :         if (call == null &&
     237           4 :             !{EventTypes.CallInvite, EventTypes.GroupCallMemberInvite}
     238           4 :                 .contains(event.type)) {
     239           0 :           Logs().w(
     240           0 :               'Ignoring call event ${event.type} for room ${room.id} because we do not have the call');
     241             :           return;
     242             :         } else if (call != null) {
     243             :           // multiple checks to make sure the events sent are from the the
     244             :           // expected party
     245           8 :           if (call.room.id != room.id) {
     246           0 :             Logs().w(
     247           0 :                 'Ignoring call event ${event.type} for room ${room.id} claiming to be for call in room ${call.room.id}');
     248             :             return;
     249             :           }
     250           6 :           if (call.remoteUserId != null && call.remoteUserId != remoteUserId) {
     251           0 :             Logs().w(
     252           0 :                 'Ignoring call event ${event.type} for room ${room.id} from sender $remoteUserId, expected sender: ${call.remoteUserId}');
     253             :             return;
     254             :           }
     255           6 :           if (call.remotePartyId != null && call.remotePartyId != partyId) {
     256           0 :             Logs().w(
     257           0 :                 'Ignoring call event ${event.type} for room ${room.id} from sender with a different party_id $partyId, expected party_id: ${call.remotePartyId}');
     258             :             return;
     259             :           }
     260           2 :           if ((call.remotePartyId != null &&
     261           6 :               call.remotePartyId == localPartyId)) {
     262           0 :             Logs().v(
     263           0 :                 'Ignoring call event ${event.type} for room ${room.id} from our own partyId');
     264             :             return;
     265             :           }
     266             :         }
     267             :       }
     268             :     }
     269           4 :     Logs().v(
     270           8 :         '[VOIP] Handling event of type: ${event.type}, content ${event.content} from sender ${event.senderId} rp: $remoteUserId:$remoteDeviceId');
     271             : 
     272           2 :     switch (event.type) {
     273           2 :       case EventTypes.CallInvite:
     274           2 :       case EventTypes.GroupCallMemberInvite:
     275           2 :         await onCallInvite(room, remoteUserId, remoteDeviceId, content);
     276             :         break;
     277           2 :       case EventTypes.CallAnswer:
     278           2 :       case EventTypes.GroupCallMemberAnswer:
     279           0 :         await onCallAnswer(room, remoteUserId, remoteDeviceId, content);
     280             :         break;
     281           2 :       case EventTypes.CallCandidates:
     282           2 :       case EventTypes.GroupCallMemberCandidates:
     283           2 :         await onCallCandidates(room, content);
     284             :         break;
     285           2 :       case EventTypes.CallHangup:
     286           2 :       case EventTypes.GroupCallMemberHangup:
     287           0 :         await onCallHangup(room, content);
     288             :         break;
     289           2 :       case EventTypes.CallReject:
     290           2 :       case EventTypes.GroupCallMemberReject:
     291           0 :         await onCallReject(room, content);
     292             :         break;
     293           2 :       case EventTypes.CallNegotiate:
     294           2 :       case EventTypes.GroupCallMemberNegotiate:
     295           0 :         await onCallNegotiate(room, content);
     296             :         break;
     297             :       // case EventTypes.CallReplaces:
     298             :       //   await onCallReplaces(room, content);
     299             :       //   break;
     300           2 :       case EventTypes.CallSelectAnswer:
     301           0 :       case EventTypes.GroupCallMemberSelectAnswer:
     302           2 :         await onCallSelectAnswer(room, content);
     303             :         break;
     304           0 :       case EventTypes.CallSDPStreamMetadataChanged:
     305           0 :       case EventTypes.CallSDPStreamMetadataChangedPrefix:
     306           0 :       case EventTypes.GroupCallMemberSDPStreamMetadataChanged:
     307           0 :         await onSDPStreamMetadataChangedReceived(room, content);
     308             :         break;
     309           0 :       case EventTypes.CallAssertedIdentity:
     310           0 :       case EventTypes.CallAssertedIdentityPrefix:
     311           0 :       case EventTypes.GroupCallMemberAssertedIdentity:
     312           0 :         await onAssertedIdentityReceived(room, content);
     313             :         break;
     314           0 :       case EventTypes.GroupCallMemberEncryptionKeys:
     315           0 :         await groupCallSession!.backend.onCallEncryption(
     316             :             groupCallSession, remoteUserId, remoteDeviceId!, content);
     317             :         break;
     318           0 :       case EventTypes.GroupCallMemberEncryptionKeysRequest:
     319           0 :         await groupCallSession!.backend.onCallEncryptionKeyRequest(
     320             :             groupCallSession, remoteUserId, remoteDeviceId!, content);
     321             :         break;
     322             :     }
     323             :   }
     324             : 
     325           0 :   Future<void> _onDeviceChange(dynamic _) async {
     326           0 :     Logs().v('[VOIP] _onDeviceChange');
     327           0 :     for (final call in calls.values) {
     328           0 :       if (call.state == CallState.kConnected && !call.isGroupCall) {
     329           0 :         await call.updateMediaDeviceForCall();
     330             :       }
     331             :     }
     332           0 :     for (final groupCall in groupCalls.values) {
     333           0 :       if (groupCall.state == GroupCallState.entered) {
     334           0 :         await groupCall.backend.updateMediaDeviceForCalls();
     335             :       }
     336             :     }
     337             :   }
     338             : 
     339           2 :   Future<void> onCallInvite(Room room, String remoteUserId,
     340             :       String? remoteDeviceId, Map<String, dynamic> content) async {
     341           4 :     Logs().v(
     342          12 :         '[VOIP] onCallInvite $remoteUserId:$remoteDeviceId => ${client.userID}:${client.deviceID}, \ncontent => ${content.toString()}');
     343             : 
     344           2 :     final String callId = content['call_id'];
     345           2 :     final int lifetime = content['lifetime'];
     346           2 :     final String? confId = content['conf_id'];
     347             : 
     348           8 :     final call = calls[VoipId(roomId: room.id, callId: callId)];
     349             : 
     350           4 :     Logs().d(
     351          10 :         '[glare] got new call ${content.tryGet('call_id')} and currently room id is mapped to ${incomingCallRoomId.tryGet(room.id)}');
     352             : 
     353           0 :     if (call != null && call.state == CallState.kEnded) {
     354             :       // Session already exist.
     355           0 :       Logs().v('[VOIP] onCallInvite: Session [$callId] already exist.');
     356             :       return;
     357             :     }
     358             : 
     359           2 :     final inviteeUserId = content['invitee'];
     360           0 :     if (inviteeUserId != null && inviteeUserId != localParticipant?.userId) {
     361           0 :       Logs().w('[VOIP] Ignoring call, meant for user $inviteeUserId');
     362             :       return; // This invite was meant for another user in the room
     363             :     }
     364           2 :     final inviteeDeviceId = content['invitee_device_id'];
     365             :     if (inviteeDeviceId != null &&
     366           0 :         inviteeDeviceId != localParticipant?.deviceId) {
     367           0 :       Logs().w('[VOIP] Ignoring call, meant for device $inviteeDeviceId');
     368             :       return; // This invite was meant for another device in the room
     369             :     }
     370             : 
     371           2 :     if (content['capabilities'] != null) {
     372           0 :       final capabilities = CallCapabilities.fromJson(content['capabilities']);
     373           0 :       Logs().v(
     374           0 :           '[VOIP] CallCapabilities: dtmf => ${capabilities.dtmf}, transferee => ${capabilities.transferee}');
     375             :     }
     376             : 
     377             :     var callType = CallType.kVoice;
     378             :     SDPStreamMetadata? sdpStreamMetadata;
     379           2 :     if (content[sdpStreamMetadataKey] != null) {
     380             :       sdpStreamMetadata =
     381           0 :           SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]);
     382           0 :       sdpStreamMetadata.sdpStreamMetadatas
     383           0 :           .forEach((streamId, SDPStreamPurpose purpose) {
     384           0 :         Logs().v(
     385           0 :             '[VOIP] [$streamId] => purpose: ${purpose.purpose}, audioMuted: ${purpose.audio_muted}, videoMuted:  ${purpose.video_muted}');
     386             : 
     387           0 :         if (!purpose.video_muted) {
     388             :           callType = CallType.kVideo;
     389             :         }
     390             :       });
     391             :     } else {
     392           6 :       callType = getCallType(content['offer']['sdp']);
     393             :     }
     394             : 
     395           2 :     final opts = CallOptions(
     396             :       voip: this,
     397             :       callId: callId,
     398             :       groupCallId: confId,
     399             :       dir: CallDirection.kIncoming,
     400             :       type: callType,
     401             :       room: room,
     402           2 :       localPartyId: localPartyId,
     403           2 :       iceServers: await getIceServers(),
     404             :     );
     405             : 
     406           2 :     final newCall = createNewCall(opts);
     407             : 
     408             :     /// both invitee userId and deviceId are set here because there can be
     409             :     /// multiple devices from same user in a call, so we specifiy who the
     410             :     /// invite is for
     411           2 :     newCall.remoteUserId = remoteUserId;
     412           2 :     newCall.remoteDeviceId = remoteDeviceId;
     413           4 :     newCall.remotePartyId = content['party_id'];
     414           4 :     newCall.remoteSessionId = content['sender_session_id'];
     415             : 
     416             :     // newCall.remoteSessionId = remoteParticipant.sessionId;
     417             : 
     418           4 :     if (!delegate.canHandleNewCall &&
     419             :         (confId == null ||
     420           0 :             currentGroupCID != VoipId(roomId: room.id, callId: confId))) {
     421           0 :       Logs().v(
     422             :           '[VOIP] onCallInvite: Unable to handle new calls, maybe user is busy.');
     423             :       // no need to emit here because handleNewCall was never triggered yet
     424           0 :       await newCall.reject(reason: CallErrorCode.userBusy, shouldEmit: false);
     425           0 :       await delegate.handleMissedCall(newCall);
     426             :       return;
     427             :     }
     428             : 
     429           2 :     final offer = RTCSessionDescription(
     430           4 :       content['offer']['sdp'],
     431           4 :       content['offer']['type'],
     432             :     );
     433             : 
     434             :     /// play ringtone. We decided to play the ringtone before adding the call to
     435             :     /// the incoming call stream because getUserMedia from initWithInvite fails
     436             :     /// on firefox unless the tab is in focus. We should atleast be able to notify
     437             :     /// the user about an incoming call
     438             :     ///
     439             :     /// Autoplay on firefox still needs interaction, without which all notifications
     440             :     /// could be blocked.
     441             :     if (confId == null) {
     442           4 :       await delegate.playRingtone();
     443             :     }
     444             : 
     445             :     // When getUserMedia throws an exception, we handle it by terminating the call,
     446             :     // and all this happens inside initWithInvite. If we set currentCID after
     447             :     // initWithInvite, we might set it to callId even after it was reset to null
     448             :     // by terminate.
     449           6 :     currentCID = VoipId(roomId: room.id, callId: callId);
     450             : 
     451           2 :     await newCall.initWithInvite(
     452             :         callType, offer, sdpStreamMetadata, lifetime, confId != null);
     453             : 
     454             :     // Popup CallingPage for incoming call.
     455           2 :     if (confId == null && !newCall.callHasEnded) {
     456           4 :       await delegate.handleNewCall(newCall);
     457             :     }
     458             : 
     459             :     if (confId != null) {
     460             :       // the stream is used to monitor incoming peer calls in a mesh call
     461           0 :       onIncomingCall.add(newCall);
     462             :     }
     463             :   }
     464             : 
     465           0 :   Future<void> onCallAnswer(Room room, String remoteUserId,
     466             :       String? remoteDeviceId, Map<String, dynamic> content) async {
     467           0 :     Logs().v('[VOIP] onCallAnswer => ${content.toString()}');
     468           0 :     final String callId = content['call_id'];
     469             : 
     470           0 :     final call = calls[VoipId(roomId: room.id, callId: callId)];
     471             :     if (call != null) {
     472           0 :       if (!call.answeredByUs) {
     473           0 :         await delegate.stopRingtone();
     474             :       }
     475           0 :       if (call.state == CallState.kRinging) {
     476           0 :         await call.onAnsweredElsewhere();
     477             :       }
     478             : 
     479           0 :       if (call.room.id != room.id) {
     480           0 :         Logs().w(
     481           0 :             'Ignoring call answer for room ${room.id} claiming to be for call in room ${call.room.id}');
     482             :         return;
     483             :       }
     484             : 
     485           0 :       if (call.remoteUserId == null) {
     486           0 :         Logs().i(
     487             :             '[VOIP] you probably called the room without setting a userId in invite, setting the calls remote user id to what I get from m.call.answer now');
     488           0 :         call.remoteUserId = remoteUserId;
     489             :       }
     490             : 
     491           0 :       if (call.remoteDeviceId == null) {
     492           0 :         Logs().i(
     493             :             '[VOIP] you probably called the room without setting a userId in invite, setting the calls remote user id to what I get from m.call.answer now');
     494           0 :         call.remoteDeviceId = remoteDeviceId;
     495             :       }
     496           0 :       if (call.remotePartyId != null) {
     497           0 :         Logs().d(
     498           0 :             'Ignoring call answer from party ${content['party_id']}, we are already with ${call.remotePartyId}');
     499             :         return;
     500             :       } else {
     501           0 :         call.remotePartyId = content['party_id'];
     502             :       }
     503             : 
     504           0 :       final answer = RTCSessionDescription(
     505           0 :           content['answer']['sdp'], content['answer']['type']);
     506             : 
     507             :       SDPStreamMetadata? metadata;
     508           0 :       if (content[sdpStreamMetadataKey] != null) {
     509           0 :         metadata = SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]);
     510             :       }
     511           0 :       await call.onAnswerReceived(answer, metadata);
     512             :     } else {
     513           0 :       Logs().v('[VOIP] onCallAnswer: Session [$callId] not found!');
     514             :     }
     515             :   }
     516             : 
     517           2 :   Future<void> onCallCandidates(Room room, Map<String, dynamic> content) async {
     518           8 :     Logs().v('[VOIP] onCallCandidates => ${content.toString()}');
     519           2 :     final String callId = content['call_id'];
     520           8 :     final call = calls[VoipId(roomId: room.id, callId: callId)];
     521             :     if (call != null) {
     522           4 :       await call.onCandidatesReceived(content['candidates']);
     523             :     } else {
     524           0 :       Logs().v('[VOIP] onCallCandidates: Session [$callId] not found!');
     525             :     }
     526             :   }
     527             : 
     528           0 :   Future<void> onCallHangup(Room room, Map<String, dynamic> content) async {
     529             :     // stop play ringtone, if this is an incoming call
     530           0 :     await delegate.stopRingtone();
     531           0 :     Logs().v('[VOIP] onCallHangup => ${content.toString()}');
     532           0 :     final String callId = content['call_id'];
     533             : 
     534           0 :     final call = calls[VoipId(roomId: room.id, callId: callId)];
     535             :     if (call != null) {
     536             :       // hangup in any case, either if the other party hung up or we did on another device
     537           0 :       await call.terminate(
     538             :           CallParty.kRemote,
     539           0 :           CallErrorCode.values.firstWhereOrNull(
     540           0 :                   (element) => element.reason == content['reason']) ??
     541             :               CallErrorCode.userHangup,
     542             :           true);
     543             :     } else {
     544           0 :       Logs().v('[VOIP] onCallHangup: Session [$callId] not found!');
     545             :     }
     546           0 :     if (callId == currentCID?.callId) {
     547           0 :       currentCID = null;
     548             :     }
     549             :   }
     550             : 
     551           0 :   Future<void> onCallReject(Room room, Map<String, dynamic> content) async {
     552           0 :     final String callId = content['call_id'];
     553           0 :     Logs().d('Reject received for call ID $callId');
     554             : 
     555           0 :     final call = calls[VoipId(roomId: room.id, callId: callId)];
     556             :     if (call != null) {
     557           0 :       await call.onRejectReceived(
     558           0 :         CallErrorCode.values.firstWhereOrNull(
     559           0 :                 (element) => element.reason == content['reason']) ??
     560             :             CallErrorCode.userHangup,
     561             :       );
     562             :     } else {
     563           0 :       Logs().v('[VOIP] onCallReject: Session [$callId] not found!');
     564             :     }
     565             :   }
     566             : 
     567           2 :   Future<void> onCallSelectAnswer(
     568             :       Room room, Map<String, dynamic> content) async {
     569           2 :     final String callId = content['call_id'];
     570           6 :     Logs().d('SelectAnswer received for call ID $callId');
     571           2 :     final String selectedPartyId = content['selected_party_id'];
     572             : 
     573           8 :     final call = calls[VoipId(roomId: room.id, callId: callId)];
     574             :     if (call != null) {
     575           8 :       if (call.room.id != room.id) {
     576           0 :         Logs().w(
     577           0 :             'Ignoring call select answer for room ${room.id} claiming to be for call in room ${call.room.id}');
     578             :         return;
     579             :       }
     580           2 :       await call.onSelectAnswerReceived(selectedPartyId);
     581             :     }
     582             :   }
     583             : 
     584           0 :   Future<void> onSDPStreamMetadataChangedReceived(
     585             :       Room room, Map<String, dynamic> content) async {
     586           0 :     final String callId = content['call_id'];
     587           0 :     Logs().d('SDP Stream metadata received for call ID $callId');
     588             : 
     589           0 :     final call = calls[VoipId(roomId: room.id, callId: callId)];
     590             :     if (call != null) {
     591           0 :       if (content[sdpStreamMetadataKey] == null) {
     592           0 :         Logs().d('SDP Stream metadata is null');
     593             :         return;
     594             :       }
     595           0 :       await call.onSDPStreamMetadataReceived(
     596           0 :           SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]));
     597             :     }
     598             :   }
     599             : 
     600           0 :   Future<void> onAssertedIdentityReceived(
     601             :       Room room, Map<String, dynamic> content) async {
     602           0 :     final String callId = content['call_id'];
     603           0 :     Logs().d('Asserted identity received for call ID $callId');
     604             : 
     605           0 :     final call = calls[VoipId(roomId: room.id, callId: callId)];
     606             :     if (call != null) {
     607           0 :       if (content['asserted_identity'] == null) {
     608           0 :         Logs().d('asserted_identity is null ');
     609             :         return;
     610             :       }
     611           0 :       call.onAssertedIdentityReceived(
     612           0 :           AssertedIdentity.fromJson(content['asserted_identity']));
     613             :     }
     614             :   }
     615             : 
     616           0 :   Future<void> onCallNegotiate(Room room, Map<String, dynamic> content) async {
     617           0 :     final String callId = content['call_id'];
     618           0 :     Logs().d('Negotiate received for call ID $callId');
     619             : 
     620           0 :     final call = calls[VoipId(roomId: room.id, callId: callId)];
     621             :     if (call != null) {
     622             :       // ideally you also check the lifetime here and discard negotiation events
     623             :       // if age of the event was older than the lifetime but as to device events
     624             :       // do not have a unsigned age nor a origin_server_ts there's no easy way to
     625             :       // override this one function atm
     626             : 
     627           0 :       final description = content['description'];
     628             :       try {
     629             :         SDPStreamMetadata? metadata;
     630           0 :         if (content[sdpStreamMetadataKey] != null) {
     631           0 :           metadata = SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]);
     632             :         }
     633           0 :         await call.onNegotiateReceived(metadata,
     634           0 :             RTCSessionDescription(description['sdp'], description['type']));
     635             :       } catch (e, s) {
     636           0 :         Logs().e('[VOIP] Failed to complete negotiation', e, s);
     637             :       }
     638             :     }
     639             :   }
     640             : 
     641           2 :   CallType getCallType(String sdp) {
     642             :     try {
     643           2 :       final session = sdp_transform.parse(sdp);
     644           8 :       if (session['media'].indexWhere((e) => e['type'] == 'video') != -1) {
     645             :         return CallType.kVideo;
     646             :       }
     647             :     } catch (e, s) {
     648           0 :       Logs().e('[VOIP] Failed to getCallType', e, s);
     649             :     }
     650             : 
     651             :     return CallType.kVoice;
     652             :   }
     653             : 
     654           2 :   Future<List<Map<String, dynamic>>> getIceServers() async {
     655           2 :     if (_turnServerCredentials == null) {
     656             :       try {
     657           6 :         _turnServerCredentials = await client.getTurnServer();
     658             :       } catch (e) {
     659           0 :         Logs().v('[VOIP] getTurnServerCredentials error => ${e.toString()}');
     660             :       }
     661             :     }
     662             : 
     663           2 :     if (_turnServerCredentials == null) {
     664           0 :       return [];
     665             :     }
     666             : 
     667           2 :     return [
     668           2 :       {
     669           4 :         'username': _turnServerCredentials!.username,
     670           4 :         'credential': _turnServerCredentials!.password,
     671           4 :         'urls': _turnServerCredentials!.uris
     672             :       }
     673             :     ];
     674             :   }
     675             : 
     676             :   /// Make a P2P call to room
     677             :   ///
     678             :   /// Pretty important to set the userId, or all the users in the room get a call.
     679             :   /// Including your own other devices, so just set it to directChatMatrixId
     680             :   ///
     681             :   /// Setting the deviceId would make all other devices for that userId ignore the call
     682             :   /// Ideally only group calls would need setting both userId and deviceId to allow
     683             :   /// having 2 devices from the same user in a group call
     684             :   ///
     685             :   /// For p2p call, you want to have all the devices of the specified `userId` ring
     686           2 :   Future<CallSession> inviteToCall(
     687             :     Room room,
     688             :     CallType type, {
     689             :     String? userId,
     690             :     String? deviceId,
     691             :   }) async {
     692           2 :     final roomId = room.id;
     693           2 :     final callId = genCallID();
     694           2 :     if (currentGroupCID == null) {
     695           4 :       incomingCallRoomId[roomId] = callId;
     696             :     }
     697           2 :     final opts = CallOptions(
     698             :       callId: callId,
     699             :       type: type,
     700             :       dir: CallDirection.kOutgoing,
     701             :       room: room,
     702             :       voip: this,
     703           2 :       localPartyId: localPartyId,
     704           2 :       iceServers: await getIceServers(),
     705             :     );
     706           2 :     final newCall = createNewCall(opts);
     707             : 
     708           2 :     newCall.remoteUserId = userId;
     709           2 :     newCall.remoteDeviceId = deviceId;
     710             : 
     711           4 :     currentCID = VoipId(roomId: roomId, callId: callId);
     712           6 :     await newCall.initOutboundCall(type).then((_) {
     713           4 :       delegate.handleNewCall(newCall);
     714             :     });
     715             :     return newCall;
     716             :   }
     717             : 
     718           2 :   CallSession createNewCall(CallOptions opts) {
     719           2 :     final call = CallSession(opts);
     720          12 :     calls[VoipId(roomId: opts.room.id, callId: opts.callId)] = call;
     721             :     return call;
     722             :   }
     723             : 
     724             :   /// Create a new group call in an existing room.
     725             :   ///
     726             :   /// [groupCallId] The room id to call
     727             :   ///
     728             :   /// [application] normal group call, thrirdroom, etc
     729             :   ///
     730             :   /// [scope] room, between specifc users, etc.
     731           0 :   Future<GroupCallSession> _newGroupCall(
     732             :     String groupCallId,
     733             :     Room room,
     734             :     CallBackend backend,
     735             :     String? application,
     736             :     String? scope,
     737             :   ) async {
     738           0 :     if (getGroupCallById(room.id, groupCallId) != null) {
     739           0 :       Logs().v('[VOIP] [$groupCallId] already exists.');
     740           0 :       return getGroupCallById(room.id, groupCallId)!;
     741             :     }
     742             : 
     743           0 :     final groupCall = GroupCallSession(
     744             :       groupCallId: groupCallId,
     745           0 :       client: client,
     746             :       room: room,
     747             :       voip: this,
     748             :       backend: backend,
     749             :       application: application,
     750             :       scope: scope,
     751             :     );
     752             : 
     753           0 :     setGroupCallById(groupCall);
     754             : 
     755             :     return groupCall;
     756             :   }
     757             : 
     758             :   /// Create a new group call in an existing room.
     759             :   ///
     760             :   /// [groupCallId] The room id to call
     761             :   ///
     762             :   /// [application] normal group call, thrirdroom, etc
     763             :   ///
     764             :   /// [scope] room, between specifc users, etc.
     765             :   ///
     766             :   /// [preShareKey] for livekit calls it creates and shares a key with other
     767             :   /// participants in the call without entering, useful on onboarding screens.
     768             :   /// does not do anything in mesh calls
     769             : 
     770           0 :   Future<GroupCallSession> fetchOrCreateGroupCall(
     771             :     String groupCallId,
     772             :     Room room,
     773             :     CallBackend backend,
     774             :     String? application,
     775             :     String? scope, {
     776             :     bool preShareKey = true,
     777             :   }) async {
     778             :     // somehow user were mising their powerlevels events and got stuck
     779             :     // with the exception below, this part just makes sure importantStateEvents
     780             :     // does not cause it.
     781           0 :     await room.postLoad();
     782             : 
     783           0 :     if (!room.groupCallsEnabledForEveryone) {
     784           0 :       await room.enableGroupCalls();
     785             :     }
     786             : 
     787           0 :     if (!room.canJoinGroupCall) {
     788           0 :       throw MatrixSDKVoipException(
     789             :         '''
     790           0 :         User ${client.userID}:${client.deviceID} is not allowed to join famedly calls in room ${room.id}, 
     791           0 :         canJoinGroupCall: ${room.canJoinGroupCall}, 
     792           0 :         groupCallsEnabledForEveryone: ${room.groupCallsEnabledForEveryone}, 
     793           0 :         needed: ${room.powerForChangingStateEvent(EventTypes.GroupCallMember)}, 
     794           0 :         own: ${room.ownPowerLevel}}
     795           0 :         plMap: ${room.getState(EventTypes.RoomPowerLevels)?.content}
     796           0 :         ''',
     797             :       );
     798             :     }
     799             : 
     800           0 :     GroupCallSession? groupCall = getGroupCallById(room.id, groupCallId);
     801             : 
     802           0 :     groupCall ??= await _newGroupCall(
     803             :       groupCallId,
     804             :       room,
     805             :       backend,
     806             :       application,
     807             :       scope,
     808             :     );
     809             : 
     810             :     if (preShareKey) {
     811           0 :       await groupCall.backend.preShareKey(groupCall);
     812             :     }
     813             : 
     814             :     return groupCall;
     815             :   }
     816             : 
     817           0 :   GroupCallSession? getGroupCallById(String roomId, String groupCallId) {
     818           0 :     return groupCalls[VoipId(roomId: roomId, callId: groupCallId)];
     819             :   }
     820             : 
     821           2 :   void setGroupCallById(GroupCallSession groupCallSession) {
     822           6 :     groupCalls[VoipId(
     823           4 :       roomId: groupCallSession.room.id,
     824           2 :       callId: groupCallSession.groupCallId,
     825             :     )] = groupCallSession;
     826             :   }
     827             : 
     828             :   /// Create a new group call from a room state event.
     829           2 :   Future<void> createGroupCallFromRoomStateEvent(
     830             :     CallMembership membership, {
     831             :     bool emitHandleNewGroupCall = true,
     832             :   }) async {
     833           2 :     if (membership.isExpired) {
     834           4 :       Logs().d(
     835           4 :           'Ignoring expired membership in passive groupCall creator. ${membership.toJson()}');
     836             :       return;
     837             :     }
     838             : 
     839           6 :     final room = client.getRoomById(membership.roomId);
     840             : 
     841             :     if (room == null) {
     842           0 :       Logs().w('Couldn\'t find room ${membership.roomId} for GroupCallSession');
     843             :       return;
     844             :     }
     845             : 
     846           4 :     if (membership.application != 'm.call' && membership.scope != 'm.room') {
     847           0 :       Logs().w('Received invalid group call application or scope.');
     848             :       return;
     849             :     }
     850             : 
     851           2 :     final groupCall = GroupCallSession(
     852           2 :       client: client,
     853             :       voip: this,
     854             :       room: room,
     855           2 :       backend: membership.backend,
     856           2 :       groupCallId: membership.callId,
     857           2 :       application: membership.application,
     858           2 :       scope: membership.scope,
     859             :     );
     860             : 
     861           4 :     if (groupCalls.containsKey(
     862           6 :         VoipId(roomId: membership.roomId, callId: membership.callId))) {
     863             :       return;
     864             :     }
     865             : 
     866           2 :     setGroupCallById(groupCall);
     867             : 
     868           4 :     onIncomingGroupCall.add(groupCall);
     869             :     if (emitHandleNewGroupCall) {
     870           4 :       await delegate.handleNewGroupCall(groupCall);
     871             :     }
     872             :   }
     873             : 
     874           0 :   @Deprecated('Call `hasActiveGroupCall` on the room directly instead')
     875           0 :   bool hasActiveCall(Room room) => room.hasActiveGroupCall;
     876             : }

Generated by: LCOV version 1.14