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

Generated by: LCOV version 1.14