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

Generated by: LCOV version 1.14