LCOV - code coverage report
Current view: top level - lib/src/voip - voip.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 48.4 % 378 183
Test Date: 2025-01-14 13:39:53 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 2.0-1