LCOV - code coverage report
Current view: top level - lib/src/voip - call_session.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 45.9 % 844 387
Test Date: 2025-03-17 05:40:22 Functions: - 0 0

            Line data    Source code
       1              : /*
       2              :  *   Famedly Matrix SDK
       3              :  *   Copyright (C) 2021 Famedly GmbH
       4              :  *
       5              :  *   This program is free software: you can redistribute it and/or modify
       6              :  *   it under the terms of the GNU Affero General Public License as
       7              :  *   published by the Free Software Foundation, either version 3 of the
       8              :  *   License, or (at your option) any later version.
       9              :  *
      10              :  *   This program is distributed in the hope that it will be useful,
      11              :  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
      12              :  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      13              :  *   GNU Affero General Public License for more details.
      14              :  *
      15              :  *   You should have received a copy of the GNU Affero General Public License
      16              :  *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
      17              :  */
      18              : 
      19              : import 'dart:async';
      20              : import 'dart:core';
      21              : import 'dart:math';
      22              : 
      23              : import 'package:collection/collection.dart';
      24              : import 'package:webrtc_interface/webrtc_interface.dart';
      25              : 
      26              : import 'package:matrix/matrix.dart';
      27              : import 'package:matrix/src/utils/cached_stream_controller.dart';
      28              : import 'package:matrix/src/voip/models/call_options.dart';
      29              : import 'package:matrix/src/voip/models/voip_id.dart';
      30              : import 'package:matrix/src/voip/utils/stream_helper.dart';
      31              : import 'package:matrix/src/voip/utils/user_media_constraints.dart';
      32              : 
      33              : /// Parses incoming matrix events to the apropriate webrtc layer underneath using
      34              : /// a `WebRTCDelegate`. This class is also responsible for sending any outgoing
      35              : /// matrix events if required (f.ex m.call.answer).
      36              : ///
      37              : /// Handles p2p calls as well individual mesh group call peer connections.
      38              : class CallSession {
      39            2 :   CallSession(this.opts);
      40              :   CallOptions opts;
      41            6 :   CallType get type => opts.type;
      42            6 :   Room get room => opts.room;
      43            6 :   VoIP get voip => opts.voip;
      44            6 :   String? get groupCallId => opts.groupCallId;
      45            6 :   String get callId => opts.callId;
      46            6 :   String get localPartyId => opts.localPartyId;
      47              : 
      48            6 :   CallDirection get direction => opts.dir;
      49              : 
      50            4 :   CallState get state => _state;
      51              :   CallState _state = CallState.kFledgling;
      52              : 
      53            0 :   bool get isOutgoing => direction == CallDirection.kOutgoing;
      54              : 
      55            0 :   bool get isRinging => state == CallState.kRinging;
      56              : 
      57              :   RTCPeerConnection? pc;
      58              : 
      59              :   final _remoteCandidates = <RTCIceCandidate>[];
      60              :   final _localCandidates = <RTCIceCandidate>[];
      61              : 
      62            0 :   AssertedIdentity? get remoteAssertedIdentity => _remoteAssertedIdentity;
      63              :   AssertedIdentity? _remoteAssertedIdentity;
      64              : 
      65            6 :   bool get callHasEnded => state == CallState.kEnded;
      66              : 
      67              :   bool _iceGatheringFinished = false;
      68              : 
      69              :   bool _inviteOrAnswerSent = false;
      70              : 
      71            0 :   bool get localHold => _localHold;
      72              :   bool _localHold = false;
      73              : 
      74            0 :   bool get remoteOnHold => _remoteOnHold;
      75              :   bool _remoteOnHold = false;
      76              : 
      77              :   bool _answeredByUs = false;
      78              : 
      79              :   bool _speakerOn = false;
      80              : 
      81              :   bool _makingOffer = false;
      82              : 
      83              :   bool _ignoreOffer = false;
      84              : 
      85            0 :   bool get answeredByUs => _answeredByUs;
      86              : 
      87            8 :   Client get client => opts.room.client;
      88              : 
      89              :   /// The local participant in the call, with id userId + deviceId
      90            6 :   CallParticipant? get localParticipant => voip.localParticipant;
      91              : 
      92              :   /// The ID of the user being called. If omitted, any user in the room can answer.
      93              :   String? remoteUserId;
      94              : 
      95            0 :   User? get remoteUser => remoteUserId != null
      96            0 :       ? room.unsafeGetUserFromMemoryOrFallback(remoteUserId!)
      97              :       : null;
      98              : 
      99              :   /// The ID of the device being called. If omitted, any device for the remoteUserId in the room can answer.
     100              :   String? remoteDeviceId;
     101              :   String? remoteSessionId; // same
     102              :   String? remotePartyId; // random string
     103              : 
     104              :   CallErrorCode? hangupReason;
     105              :   CallSession? _successor;
     106              :   int _toDeviceSeq = 0;
     107              :   int _candidateSendTries = 0;
     108            4 :   bool get isGroupCall => groupCallId != null;
     109              :   bool _missedCall = true;
     110              : 
     111              :   final CachedStreamController<CallSession> onCallStreamsChanged =
     112              :       CachedStreamController();
     113              : 
     114              :   final CachedStreamController<CallSession> onCallReplaced =
     115              :       CachedStreamController();
     116              : 
     117              :   final CachedStreamController<CallSession> onCallHangupNotifierForGroupCalls =
     118              :       CachedStreamController();
     119              : 
     120              :   final CachedStreamController<CallState> onCallStateChanged =
     121              :       CachedStreamController();
     122              : 
     123              :   final CachedStreamController<CallStateChange> onCallEventChanged =
     124              :       CachedStreamController();
     125              : 
     126              :   final CachedStreamController<WrappedMediaStream> onStreamAdd =
     127              :       CachedStreamController();
     128              : 
     129              :   final CachedStreamController<WrappedMediaStream> onStreamRemoved =
     130              :       CachedStreamController();
     131              : 
     132              :   SDPStreamMetadata? _remoteSDPStreamMetadata;
     133              :   final List<RTCRtpSender> _usermediaSenders = [];
     134              :   final List<RTCRtpSender> _screensharingSenders = [];
     135              :   final List<WrappedMediaStream> _streams = <WrappedMediaStream>[];
     136              : 
     137            2 :   List<WrappedMediaStream> get getLocalStreams =>
     138           10 :       _streams.where((element) => element.isLocal()).toList();
     139            0 :   List<WrappedMediaStream> get getRemoteStreams =>
     140            0 :       _streams.where((element) => !element.isLocal()).toList();
     141              : 
     142            0 :   bool get isLocalVideoMuted => localUserMediaStream?.isVideoMuted() ?? false;
     143              : 
     144            0 :   bool get isMicrophoneMuted => localUserMediaStream?.isAudioMuted() ?? false;
     145              : 
     146            0 :   bool get screensharingEnabled => localScreenSharingStream != null;
     147              : 
     148            2 :   WrappedMediaStream? get localUserMediaStream {
     149            4 :     final stream = getLocalStreams.where(
     150            6 :       (element) => element.purpose == SDPStreamMetadataPurpose.Usermedia,
     151              :     );
     152            2 :     if (stream.isNotEmpty) {
     153            2 :       return stream.first;
     154              :     }
     155              :     return null;
     156              :   }
     157              : 
     158            2 :   WrappedMediaStream? get localScreenSharingStream {
     159            4 :     final stream = getLocalStreams.where(
     160            6 :       (element) => element.purpose == SDPStreamMetadataPurpose.Screenshare,
     161              :     );
     162            2 :     if (stream.isNotEmpty) {
     163            0 :       return stream.first;
     164              :     }
     165              :     return null;
     166              :   }
     167              : 
     168            0 :   WrappedMediaStream? get remoteUserMediaStream {
     169            0 :     final stream = getRemoteStreams.where(
     170            0 :       (element) => element.purpose == SDPStreamMetadataPurpose.Usermedia,
     171              :     );
     172            0 :     if (stream.isNotEmpty) {
     173            0 :       return stream.first;
     174              :     }
     175              :     return null;
     176              :   }
     177              : 
     178            0 :   WrappedMediaStream? get remoteScreenSharingStream {
     179            0 :     final stream = getRemoteStreams.where(
     180            0 :       (element) => element.purpose == SDPStreamMetadataPurpose.Screenshare,
     181              :     );
     182            0 :     if (stream.isNotEmpty) {
     183            0 :       return stream.first;
     184              :     }
     185              :     return null;
     186              :   }
     187              : 
     188              :   /// returns whether a 1:1 call sender has video tracks
     189            0 :   Future<bool> hasVideoToSend() async {
     190            0 :     final transceivers = await pc!.getTransceivers();
     191            0 :     final localUserMediaVideoTrack = localUserMediaStream?.stream
     192            0 :         ?.getTracks()
     193            0 :         .singleWhereOrNull((track) => track.kind == 'video');
     194              : 
     195              :     // check if we have a video track locally and have transceivers setup correctly.
     196              :     return localUserMediaVideoTrack != null &&
     197            0 :         transceivers.singleWhereOrNull(
     198            0 :               (transceiver) =>
     199            0 :                   transceiver.sender.track?.id == localUserMediaVideoTrack.id,
     200              :             ) !=
     201              :             null;
     202              :   }
     203              : 
     204              :   Timer? _inviteTimer;
     205              :   Timer? _ringingTimer;
     206              : 
     207              :   // outgoing call
     208            2 :   Future<void> initOutboundCall(CallType type) async {
     209            2 :     await _preparePeerConnection();
     210            2 :     setCallState(CallState.kCreateOffer);
     211            2 :     final stream = await _getUserMedia(type);
     212              :     if (stream != null) {
     213            2 :       await addLocalStream(stream, SDPStreamMetadataPurpose.Usermedia);
     214              :     }
     215              :   }
     216              : 
     217              :   // incoming call
     218            2 :   Future<void> initWithInvite(
     219              :     CallType type,
     220              :     RTCSessionDescription offer,
     221              :     SDPStreamMetadata? metadata,
     222              :     int lifetime,
     223              :     bool isGroupCall,
     224              :   ) async {
     225              :     if (!isGroupCall) {
     226              :       // glare fixes
     227           10 :       final prevCallId = voip.incomingCallRoomId[room.id];
     228              :       if (prevCallId != null) {
     229              :         // This is probably an outbound call, but we already have a incoming invite, so let's terminate it.
     230              :         final prevCall =
     231           12 :             voip.calls[VoipId(roomId: room.id, callId: prevCallId)];
     232              :         if (prevCall != null) {
     233            2 :           if (prevCall._inviteOrAnswerSent) {
     234            4 :             Logs().d('[glare] invite or answer sent, lex compare now');
     235            8 :             if (callId.compareTo(prevCall.callId) > 0) {
     236            4 :               Logs().d(
     237            6 :                 '[glare] new call $callId needs to be canceled because the older one ${prevCall.callId} has a smaller lex',
     238              :               );
     239            2 :               await hangup(reason: CallErrorCode.unknownError);
     240            4 :               voip.currentCID =
     241            8 :                   VoipId(roomId: room.id, callId: prevCall.callId);
     242              :             } else {
     243            0 :               Logs().d(
     244            0 :                 '[glare] nice, lex of newer call $callId is smaller auto accept this here',
     245              :               );
     246              : 
     247              :               /// These fixes do not work all the time because sometimes the code
     248              :               /// is at an unrecoverable stage (invite already sent when we were
     249              :               /// checking if we want to send a invite), so commented out answering
     250              :               /// automatically to prevent unknown cases
     251              :               // await answer();
     252              :               // return;
     253              :             }
     254              :           } else {
     255            4 :             Logs().d(
     256            4 :               '[glare] ${prevCall.callId} was still preparing prev call, nvm now cancel it',
     257              :             );
     258            2 :             await prevCall.hangup(reason: CallErrorCode.unknownError);
     259              :           }
     260              :         }
     261              :       }
     262              :     }
     263              : 
     264            2 :     await _preparePeerConnection();
     265              :     if (metadata != null) {
     266            0 :       _updateRemoteSDPStreamMetadata(metadata);
     267              :     }
     268            4 :     await pc!.setRemoteDescription(offer);
     269              : 
     270              :     /// only add local stream if it is not a group call.
     271              :     if (!isGroupCall) {
     272            2 :       final stream = await _getUserMedia(type);
     273              :       if (stream != null) {
     274            2 :         await addLocalStream(stream, SDPStreamMetadataPurpose.Usermedia);
     275              :       } else {
     276              :         // we don't have a localstream, call probably crashed
     277              :         // for sanity
     278            0 :         if (state == CallState.kEnded) {
     279              :           return;
     280              :         }
     281              :       }
     282              :     }
     283              : 
     284            2 :     setCallState(CallState.kRinging);
     285              : 
     286            4 :     _ringingTimer = Timer(CallTimeouts.callInviteLifetime, () {
     287            0 :       if (state == CallState.kRinging) {
     288            0 :         Logs().v('[VOIP] Call invite has expired. Hanging up.');
     289              : 
     290            0 :         fireCallEvent(CallStateChange.kHangup);
     291            0 :         hangup(reason: CallErrorCode.inviteTimeout);
     292              :       }
     293            0 :       _ringingTimer?.cancel();
     294            0 :       _ringingTimer = null;
     295              :     });
     296              :   }
     297              : 
     298            0 :   Future<void> answerWithStreams(List<WrappedMediaStream> callFeeds) async {
     299            0 :     if (_inviteOrAnswerSent) return;
     300            0 :     Logs().d('answering call $callId');
     301            0 :     await gotCallFeedsForAnswer(callFeeds);
     302              :   }
     303              : 
     304            0 :   Future<void> replacedBy(CallSession newCall) async {
     305            0 :     if (state == CallState.kWaitLocalMedia) {
     306            0 :       Logs().v('Telling new call to wait for local media');
     307            0 :     } else if (state == CallState.kCreateOffer ||
     308            0 :         state == CallState.kInviteSent) {
     309            0 :       Logs().v('Handing local stream to new call');
     310            0 :       await newCall.gotCallFeedsForAnswer(getLocalStreams);
     311              :     }
     312            0 :     _successor = newCall;
     313            0 :     onCallReplaced.add(newCall);
     314              :     // ignore: unawaited_futures
     315            0 :     hangup(reason: CallErrorCode.replaced);
     316              :   }
     317              : 
     318            0 :   Future<void> sendAnswer(RTCSessionDescription answer) async {
     319            0 :     final callCapabilities = CallCapabilities()
     320            0 :       ..dtmf = false
     321            0 :       ..transferee = false;
     322              : 
     323            0 :     final metadata = SDPStreamMetadata({
     324            0 :       localUserMediaStream!.stream!.id: SDPStreamPurpose(
     325              :         purpose: SDPStreamMetadataPurpose.Usermedia,
     326            0 :         audio_muted: localUserMediaStream!.stream!.getAudioTracks().isEmpty,
     327            0 :         video_muted: localUserMediaStream!.stream!.getVideoTracks().isEmpty,
     328              :       ),
     329              :     });
     330              : 
     331            0 :     final res = await sendAnswerCall(
     332            0 :       room,
     333            0 :       callId,
     334            0 :       answer.sdp!,
     335            0 :       localPartyId,
     336            0 :       type: answer.type!,
     337              :       capabilities: callCapabilities,
     338              :       metadata: metadata,
     339              :     );
     340            0 :     Logs().v('[VOIP] answer res => $res');
     341              :   }
     342              : 
     343            0 :   Future<void> gotCallFeedsForAnswer(List<WrappedMediaStream> callFeeds) async {
     344            0 :     if (state == CallState.kEnded) return;
     345              : 
     346            0 :     for (final element in callFeeds) {
     347            0 :       await addLocalStream(await element.stream!.clone(), element.purpose);
     348              :     }
     349              : 
     350            0 :     await answer();
     351              :   }
     352              : 
     353            0 :   Future<void> placeCallWithStreams(
     354              :     List<WrappedMediaStream> callFeeds, {
     355              :     bool requestScreenSharing = false,
     356              :   }) async {
     357              :     // create the peer connection now so it can be gathering candidates while we get user
     358              :     // media (assuming a candidate pool size is configured)
     359            0 :     await _preparePeerConnection();
     360            0 :     await gotCallFeedsForInvite(
     361              :       callFeeds,
     362              :       requestScreenSharing: requestScreenSharing,
     363              :     );
     364              :   }
     365              : 
     366            0 :   Future<void> gotCallFeedsForInvite(
     367              :     List<WrappedMediaStream> callFeeds, {
     368              :     bool requestScreenSharing = false,
     369              :   }) async {
     370            0 :     if (_successor != null) {
     371            0 :       await _successor!.gotCallFeedsForAnswer(callFeeds);
     372              :       return;
     373              :     }
     374            0 :     if (state == CallState.kEnded) {
     375            0 :       await cleanUp();
     376              :       return;
     377              :     }
     378              : 
     379            0 :     for (final element in callFeeds) {
     380            0 :       await addLocalStream(await element.stream!.clone(), element.purpose);
     381              :     }
     382              : 
     383              :     if (requestScreenSharing) {
     384            0 :       await pc!.addTransceiver(
     385              :         kind: RTCRtpMediaType.RTCRtpMediaTypeVideo,
     386            0 :         init: RTCRtpTransceiverInit(direction: TransceiverDirection.RecvOnly),
     387              :       );
     388              :     }
     389              : 
     390            0 :     setCallState(CallState.kCreateOffer);
     391              : 
     392            0 :     Logs().d('gotUserMediaForInvite');
     393              :     // Now we wait for the negotiationneeded event
     394              :   }
     395              : 
     396            0 :   Future<void> onAnswerReceived(
     397              :     RTCSessionDescription answer,
     398              :     SDPStreamMetadata? metadata,
     399              :   ) async {
     400              :     if (metadata != null) {
     401            0 :       _updateRemoteSDPStreamMetadata(metadata);
     402              :     }
     403              : 
     404            0 :     if (direction == CallDirection.kOutgoing) {
     405            0 :       setCallState(CallState.kConnecting);
     406            0 :       await pc!.setRemoteDescription(answer);
     407            0 :       for (final candidate in _remoteCandidates) {
     408            0 :         await pc!.addCandidate(candidate);
     409              :       }
     410              :     }
     411            0 :     if (remotePartyId != null) {
     412              :       /// Send select_answer event.
     413            0 :       await sendSelectCallAnswer(
     414            0 :         opts.room,
     415            0 :         callId,
     416            0 :         localPartyId,
     417            0 :         remotePartyId!,
     418              :       );
     419              :     }
     420              :   }
     421              : 
     422            0 :   Future<void> onNegotiateReceived(
     423              :     SDPStreamMetadata? metadata,
     424              :     RTCSessionDescription description,
     425              :   ) async {
     426            0 :     final polite = direction == CallDirection.kIncoming;
     427              : 
     428              :     // Here we follow the perfect negotiation logic from
     429              :     // https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation
     430            0 :     final offerCollision = ((description.type == 'offer') &&
     431            0 :         (_makingOffer ||
     432            0 :             pc!.signalingState != RTCSignalingState.RTCSignalingStateStable));
     433              : 
     434            0 :     _ignoreOffer = !polite && offerCollision;
     435            0 :     if (_ignoreOffer) {
     436            0 :       Logs().i('Ignoring colliding negotiate event because we\'re impolite');
     437              :       return;
     438              :     }
     439              : 
     440            0 :     final prevLocalOnHold = await isLocalOnHold();
     441              : 
     442              :     if (metadata != null) {
     443            0 :       _updateRemoteSDPStreamMetadata(metadata);
     444              :     }
     445              : 
     446              :     try {
     447            0 :       await pc!.setRemoteDescription(description);
     448              :       RTCSessionDescription? answer;
     449            0 :       if (description.type == 'offer') {
     450              :         try {
     451            0 :           answer = await pc!.createAnswer({});
     452              :         } catch (e) {
     453            0 :           await terminate(CallParty.kLocal, CallErrorCode.createAnswer, true);
     454              :           rethrow;
     455              :         }
     456              : 
     457            0 :         await sendCallNegotiate(
     458            0 :           room,
     459            0 :           callId,
     460            0 :           CallTimeouts.defaultCallEventLifetime.inMilliseconds,
     461            0 :           localPartyId,
     462            0 :           answer.sdp!,
     463            0 :           type: answer.type!,
     464              :         );
     465            0 :         await pc!.setLocalDescription(answer);
     466              :       }
     467              :     } catch (e, s) {
     468            0 :       Logs().e('[VOIP] onNegotiateReceived => ', e, s);
     469            0 :       await _getLocalOfferFailed(e);
     470              :       return;
     471              :     }
     472              : 
     473            0 :     final newLocalOnHold = await isLocalOnHold();
     474            0 :     if (prevLocalOnHold != newLocalOnHold) {
     475            0 :       _localHold = newLocalOnHold;
     476            0 :       fireCallEvent(CallStateChange.kLocalHoldUnhold);
     477              :     }
     478              :   }
     479              : 
     480            0 :   Future<void> updateMediaDeviceForCall() async {
     481            0 :     await updateMediaDevice(
     482            0 :       voip.delegate,
     483              :       MediaKind.audio,
     484            0 :       _usermediaSenders,
     485              :     );
     486            0 :     await updateMediaDevice(
     487            0 :       voip.delegate,
     488              :       MediaKind.video,
     489            0 :       _usermediaSenders,
     490              :     );
     491              :   }
     492              : 
     493            0 :   void _updateRemoteSDPStreamMetadata(SDPStreamMetadata metadata) {
     494            0 :     _remoteSDPStreamMetadata = metadata;
     495            0 :     _remoteSDPStreamMetadata?.sdpStreamMetadatas
     496            0 :         .forEach((streamId, sdpStreamMetadata) {
     497            0 :       Logs().i(
     498            0 :         'Stream purpose update: \nid = "$streamId", \npurpose = "${sdpStreamMetadata.purpose}",  \naudio_muted = ${sdpStreamMetadata.audio_muted}, \nvideo_muted = ${sdpStreamMetadata.video_muted}',
     499              :       );
     500              :     });
     501            0 :     for (final wpstream in getRemoteStreams) {
     502            0 :       final streamId = wpstream.stream!.id;
     503            0 :       final purpose = metadata.sdpStreamMetadatas[streamId];
     504              :       if (purpose != null) {
     505              :         wpstream
     506            0 :             .setAudioMuted(metadata.sdpStreamMetadatas[streamId]!.audio_muted);
     507              :         wpstream
     508            0 :             .setVideoMuted(metadata.sdpStreamMetadatas[streamId]!.video_muted);
     509            0 :         wpstream.purpose = metadata.sdpStreamMetadatas[streamId]!.purpose;
     510              :       } else {
     511            0 :         Logs().i('Not found purpose for remote stream $streamId, remove it?');
     512            0 :         wpstream.stopped = true;
     513            0 :         fireCallEvent(CallStateChange.kFeedsChanged);
     514              :       }
     515              :     }
     516              :   }
     517              : 
     518            0 :   Future<void> onSDPStreamMetadataReceived(SDPStreamMetadata metadata) async {
     519            0 :     _updateRemoteSDPStreamMetadata(metadata);
     520            0 :     fireCallEvent(CallStateChange.kFeedsChanged);
     521              :   }
     522              : 
     523            2 :   Future<void> onCandidatesReceived(List<dynamic> candidates) async {
     524            4 :     for (final json in candidates) {
     525            2 :       final candidate = RTCIceCandidate(
     526            2 :         json['candidate'],
     527            2 :         json['sdpMid'] ?? '',
     528            4 :         json['sdpMLineIndex']?.round() ?? 0,
     529              :       );
     530              : 
     531            2 :       if (!candidate.isValid) {
     532            0 :         Logs().w(
     533            0 :           '[VOIP] onCandidatesReceived => skip invalid candidate ${candidate.toMap()}',
     534              :         );
     535              :         continue;
     536              :       }
     537              : 
     538            4 :       if (direction == CallDirection.kOutgoing &&
     539            0 :           pc != null &&
     540            0 :           await pc!.getRemoteDescription() == null) {
     541            0 :         _remoteCandidates.add(candidate);
     542              :         continue;
     543              :       }
     544              : 
     545            4 :       if (pc != null && _inviteOrAnswerSent) {
     546              :         try {
     547            0 :           await pc!.addCandidate(candidate);
     548              :         } catch (e, s) {
     549            0 :           Logs().e('[VOIP] onCandidatesReceived => ', e, s);
     550              :         }
     551              :       } else {
     552            4 :         _remoteCandidates.add(candidate);
     553              :       }
     554              :     }
     555              :   }
     556              : 
     557            0 :   void onAssertedIdentityReceived(AssertedIdentity identity) {
     558            0 :     _remoteAssertedIdentity = identity;
     559            0 :     fireCallEvent(CallStateChange.kAssertedIdentityChanged);
     560              :   }
     561              : 
     562            2 :   Future<bool> setScreensharingEnabled(bool enabled) async {
     563              :     // Skip if there is nothing to do
     564            2 :     if (enabled && localScreenSharingStream != null) {
     565            0 :       Logs().w(
     566              :         'There is already a screensharing stream - there is nothing to do!',
     567              :       );
     568              :       return true;
     569            0 :     } else if (!enabled && localScreenSharingStream == null) {
     570            0 :       Logs().w(
     571              :         'There already isn\'t a screensharing stream - there is nothing to do!',
     572              :       );
     573              :       return false;
     574              :     }
     575              : 
     576            6 :     Logs().d('Set screensharing enabled? $enabled');
     577              : 
     578              :     if (enabled) {
     579              :       try {
     580            2 :         final stream = await _getDisplayMedia();
     581              :         if (stream == null) {
     582              :           return false;
     583              :         }
     584            0 :         for (final track in stream.getTracks()) {
     585              :           // screen sharing should only have 1 video track anyway, so this only
     586              :           // fires once
     587            0 :           track.onEnded = () async {
     588            0 :             await setScreensharingEnabled(false);
     589              :           };
     590              :         }
     591              : 
     592            0 :         await addLocalStream(stream, SDPStreamMetadataPurpose.Screenshare);
     593              :         return true;
     594              :       } catch (err) {
     595              :         return false;
     596              :       }
     597              :     } else {
     598              :       try {
     599            0 :         for (final sender in _screensharingSenders) {
     600            0 :           await pc!.removeTrack(sender);
     601              :         }
     602            0 :         for (final track in localScreenSharingStream!.stream!.getTracks()) {
     603            0 :           await track.stop();
     604              :         }
     605            0 :         localScreenSharingStream!.stopped = true;
     606            0 :         await _removeStream(localScreenSharingStream!.stream!);
     607            0 :         fireCallEvent(CallStateChange.kFeedsChanged);
     608              :         return false;
     609              :       } catch (e, s) {
     610            0 :         Logs().e('[VOIP] stopping screen sharing track failed', e, s);
     611              :         return false;
     612              :       }
     613              :     }
     614              :   }
     615              : 
     616            2 :   Future<void> addLocalStream(
     617              :     MediaStream stream,
     618              :     String purpose, {
     619              :     bool addToPeerConnection = true,
     620              :   }) async {
     621              :     final existingStream =
     622            4 :         getLocalStreams.where((element) => element.purpose == purpose);
     623            2 :     if (existingStream.isNotEmpty) {
     624            0 :       existingStream.first.setNewStream(stream);
     625              :     } else {
     626            2 :       final newStream = WrappedMediaStream(
     627            2 :         participant: localParticipant!,
     628            4 :         room: opts.room,
     629              :         stream: stream,
     630              :         purpose: purpose,
     631            2 :         client: client,
     632            4 :         audioMuted: stream.getAudioTracks().isEmpty,
     633            4 :         videoMuted: stream.getVideoTracks().isEmpty,
     634            2 :         isGroupCall: groupCallId != null,
     635            2 :         pc: pc,
     636            2 :         voip: voip,
     637              :       );
     638            4 :       _streams.add(newStream);
     639            4 :       onStreamAdd.add(newStream);
     640              :     }
     641              : 
     642              :     if (addToPeerConnection) {
     643            2 :       if (purpose == SDPStreamMetadataPurpose.Screenshare) {
     644            0 :         _screensharingSenders.clear();
     645            0 :         for (final track in stream.getTracks()) {
     646            0 :           _screensharingSenders.add(await pc!.addTrack(track, stream));
     647              :         }
     648            2 :       } else if (purpose == SDPStreamMetadataPurpose.Usermedia) {
     649            4 :         _usermediaSenders.clear();
     650            2 :         for (final track in stream.getTracks()) {
     651            0 :           _usermediaSenders.add(await pc!.addTrack(track, stream));
     652              :         }
     653              :       }
     654              :     }
     655              : 
     656            2 :     if (purpose == SDPStreamMetadataPurpose.Usermedia) {
     657            6 :       _speakerOn = type == CallType.kVideo;
     658           10 :       if (!voip.delegate.isWeb && stream.getAudioTracks().isNotEmpty) {
     659            0 :         final audioTrack = stream.getAudioTracks()[0];
     660            0 :         audioTrack.enableSpeakerphone(_speakerOn);
     661              :       }
     662              :     }
     663              : 
     664            2 :     fireCallEvent(CallStateChange.kFeedsChanged);
     665              :   }
     666              : 
     667            0 :   Future<void> _addRemoteStream(MediaStream stream) async {
     668              :     //final userId = remoteUser.id;
     669            0 :     final metadata = _remoteSDPStreamMetadata?.sdpStreamMetadatas[stream.id];
     670              :     if (metadata == null) {
     671            0 :       Logs().i(
     672            0 :         'Ignoring stream with id ${stream.id} because we didn\'t get any metadata about it',
     673              :       );
     674              :       return;
     675              :     }
     676              : 
     677            0 :     final purpose = metadata.purpose;
     678            0 :     final audioMuted = metadata.audio_muted;
     679            0 :     final videoMuted = metadata.video_muted;
     680              : 
     681              :     // Try to find a feed with the same purpose as the new stream,
     682              :     // if we find it replace the old stream with the new one
     683              :     final existingStream =
     684            0 :         getRemoteStreams.where((element) => element.purpose == purpose);
     685            0 :     if (existingStream.isNotEmpty) {
     686            0 :       existingStream.first.setNewStream(stream);
     687              :     } else {
     688            0 :       final newStream = WrappedMediaStream(
     689            0 :         participant: CallParticipant(
     690            0 :           voip,
     691            0 :           userId: remoteUserId!,
     692            0 :           deviceId: remoteDeviceId,
     693              :         ),
     694            0 :         room: opts.room,
     695              :         stream: stream,
     696              :         purpose: purpose,
     697            0 :         client: client,
     698              :         audioMuted: audioMuted,
     699              :         videoMuted: videoMuted,
     700            0 :         isGroupCall: groupCallId != null,
     701            0 :         pc: pc,
     702            0 :         voip: voip,
     703              :       );
     704            0 :       _streams.add(newStream);
     705            0 :       onStreamAdd.add(newStream);
     706              :     }
     707            0 :     fireCallEvent(CallStateChange.kFeedsChanged);
     708            0 :     Logs().i('Pushed remote stream (id="${stream.id}", purpose=$purpose)');
     709              :   }
     710              : 
     711            0 :   Future<void> deleteAllStreams() async {
     712            0 :     for (final stream in _streams) {
     713            0 :       if (stream.isLocal() || groupCallId == null) {
     714            0 :         await stream.dispose();
     715              :       }
     716              :     }
     717            0 :     _streams.clear();
     718            0 :     fireCallEvent(CallStateChange.kFeedsChanged);
     719              :   }
     720              : 
     721            0 :   Future<void> deleteFeedByStream(MediaStream stream) async {
     722              :     final index =
     723            0 :         _streams.indexWhere((element) => element.stream!.id == stream.id);
     724            0 :     if (index == -1) {
     725            0 :       Logs().w('Didn\'t find the feed with stream id ${stream.id} to delete');
     726              :       return;
     727              :     }
     728            0 :     final wstream = _streams.elementAt(index);
     729            0 :     onStreamRemoved.add(wstream);
     730            0 :     await deleteStream(wstream);
     731              :   }
     732              : 
     733            0 :   Future<void> deleteStream(WrappedMediaStream stream) async {
     734            0 :     await stream.dispose();
     735            0 :     _streams.removeAt(_streams.indexOf(stream));
     736            0 :     fireCallEvent(CallStateChange.kFeedsChanged);
     737              :   }
     738              : 
     739            0 :   Future<void> removeLocalStream(WrappedMediaStream callFeed) async {
     740            0 :     final senderArray = callFeed.purpose == SDPStreamMetadataPurpose.Usermedia
     741            0 :         ? _usermediaSenders
     742            0 :         : _screensharingSenders;
     743              : 
     744            0 :     for (final element in senderArray) {
     745            0 :       await pc!.removeTrack(element);
     746              :     }
     747              : 
     748            0 :     if (callFeed.purpose == SDPStreamMetadataPurpose.Screenshare) {
     749            0 :       await stopMediaStream(callFeed.stream);
     750              :     }
     751              : 
     752              :     // Empty the array
     753            0 :     senderArray.removeRange(0, senderArray.length);
     754            0 :     onStreamRemoved.add(callFeed);
     755            0 :     await deleteStream(callFeed);
     756              :   }
     757              : 
     758            2 :   void setCallState(CallState newState) {
     759            2 :     _state = newState;
     760            4 :     onCallStateChanged.add(newState);
     761            2 :     fireCallEvent(CallStateChange.kState);
     762              :   }
     763              : 
     764            0 :   Future<void> setLocalVideoMuted(bool muted) async {
     765              :     if (!muted) {
     766            0 :       final videoToSend = await hasVideoToSend();
     767              :       if (!videoToSend) {
     768            0 :         if (_remoteSDPStreamMetadata == null) return;
     769            0 :         await insertVideoTrackToAudioOnlyStream();
     770              :       }
     771              :     }
     772            0 :     localUserMediaStream?.setVideoMuted(muted);
     773            0 :     await updateMuteStatus();
     774              :   }
     775              : 
     776              :   // used for upgrading 1:1 calls
     777            0 :   Future<void> insertVideoTrackToAudioOnlyStream() async {
     778            0 :     if (localUserMediaStream != null && localUserMediaStream!.stream != null) {
     779            0 :       final stream = await _getUserMedia(CallType.kVideo);
     780              :       if (stream != null) {
     781            0 :         Logs().d('[VOIP] running replaceTracks() on stream: ${stream.id}');
     782            0 :         _setTracksEnabled(stream.getVideoTracks(), true);
     783              :         // replace local tracks
     784            0 :         for (final track in localUserMediaStream!.stream!.getTracks()) {
     785              :           try {
     786            0 :             await localUserMediaStream!.stream!.removeTrack(track);
     787            0 :             await track.stop();
     788              :           } catch (e) {
     789            0 :             Logs().w('failed to stop track');
     790              :           }
     791              :         }
     792            0 :         final streamTracks = stream.getTracks();
     793            0 :         for (final newTrack in streamTracks) {
     794            0 :           await localUserMediaStream!.stream!.addTrack(newTrack);
     795              :         }
     796              : 
     797              :         // remove any screen sharing or remote transceivers, these don't need
     798              :         // to be replaced anyway.
     799            0 :         final transceivers = await pc!.getTransceivers();
     800            0 :         transceivers.removeWhere(
     801            0 :           (transceiver) =>
     802            0 :               transceiver.sender.track == null ||
     803            0 :               (localScreenSharingStream != null &&
     804            0 :                   localScreenSharingStream!.stream != null &&
     805            0 :                   localScreenSharingStream!.stream!
     806            0 :                       .getTracks()
     807            0 :                       .map((e) => e.id)
     808            0 :                       .contains(transceiver.sender.track?.id)),
     809              :         );
     810              : 
     811              :         // in an ideal case the following should happen
     812              :         // - audio track gets replaced
     813              :         // - new video track gets added
     814            0 :         for (final newTrack in streamTracks) {
     815            0 :           final transceiver = transceivers.singleWhereOrNull(
     816            0 :             (transceiver) => transceiver.sender.track!.kind == newTrack.kind,
     817              :           );
     818              :           if (transceiver != null) {
     819            0 :             Logs().d(
     820            0 :               '[VOIP] replacing ${transceiver.sender.track} in transceiver',
     821              :             );
     822            0 :             final oldSender = transceiver.sender;
     823            0 :             await oldSender.replaceTrack(newTrack);
     824            0 :             await transceiver.setDirection(
     825            0 :               await transceiver.getDirection() ==
     826              :                       TransceiverDirection.Inactive // upgrade, send now
     827              :                   ? TransceiverDirection.SendOnly
     828              :                   : TransceiverDirection.SendRecv,
     829              :             );
     830              :           } else {
     831              :             // adding transceiver
     832            0 :             Logs().d('[VOIP] adding track $newTrack to pc');
     833            0 :             await pc!.addTrack(newTrack, localUserMediaStream!.stream!);
     834              :           }
     835              :         }
     836              :         // for renderer to be able to show new video track
     837            0 :         localUserMediaStream?.onStreamChanged
     838            0 :             .add(localUserMediaStream!.stream!);
     839              :       }
     840              :     }
     841              :   }
     842              : 
     843            0 :   Future<void> setMicrophoneMuted(bool muted) async {
     844            0 :     localUserMediaStream?.setAudioMuted(muted);
     845            0 :     await updateMuteStatus();
     846              :   }
     847              : 
     848            0 :   Future<void> setRemoteOnHold(bool onHold) async {
     849            0 :     if (remoteOnHold == onHold) return;
     850            0 :     _remoteOnHold = onHold;
     851            0 :     final transceivers = await pc!.getTransceivers();
     852            0 :     for (final transceiver in transceivers) {
     853            0 :       await transceiver.setDirection(
     854              :         onHold ? TransceiverDirection.SendOnly : TransceiverDirection.SendRecv,
     855              :       );
     856              :     }
     857            0 :     await updateMuteStatus();
     858            0 :     fireCallEvent(CallStateChange.kRemoteHoldUnhold);
     859              :   }
     860              : 
     861            0 :   Future<bool> isLocalOnHold() async {
     862            0 :     if (state != CallState.kConnected) return false;
     863              :     var callOnHold = true;
     864              :     // We consider a call to be on hold only if *all* the tracks are on hold
     865              :     // (is this the right thing to do?)
     866            0 :     final transceivers = await pc!.getTransceivers();
     867            0 :     for (final transceiver in transceivers) {
     868            0 :       final currentDirection = await transceiver.getCurrentDirection();
     869            0 :       final trackOnHold = (currentDirection == TransceiverDirection.Inactive ||
     870            0 :           currentDirection == TransceiverDirection.RecvOnly);
     871              :       if (!trackOnHold) {
     872              :         callOnHold = false;
     873              :       }
     874              :     }
     875              :     return callOnHold;
     876              :   }
     877              : 
     878            2 :   Future<void> answer({String? txid}) async {
     879            2 :     if (_inviteOrAnswerSent) {
     880              :       return;
     881              :     }
     882              :     // stop play ringtone
     883            6 :     await voip.delegate.stopRingtone();
     884              : 
     885            4 :     if (direction == CallDirection.kIncoming) {
     886            2 :       setCallState(CallState.kCreateAnswer);
     887              : 
     888            6 :       final answer = await pc!.createAnswer({});
     889            4 :       for (final candidate in _remoteCandidates) {
     890            4 :         await pc!.addCandidate(candidate);
     891              :       }
     892              : 
     893            2 :       final callCapabilities = CallCapabilities()
     894            2 :         ..dtmf = false
     895            2 :         ..transferee = false;
     896              : 
     897            4 :       final metadata = SDPStreamMetadata({
     898            2 :         if (localUserMediaStream != null)
     899           10 :           localUserMediaStream!.stream!.id: SDPStreamPurpose(
     900              :             purpose: SDPStreamMetadataPurpose.Usermedia,
     901            4 :             audio_muted: localUserMediaStream!.audioMuted,
     902            4 :             video_muted: localUserMediaStream!.videoMuted,
     903              :           ),
     904            2 :         if (localScreenSharingStream != null)
     905            0 :           localScreenSharingStream!.stream!.id: SDPStreamPurpose(
     906              :             purpose: SDPStreamMetadataPurpose.Screenshare,
     907            0 :             audio_muted: localScreenSharingStream!.audioMuted,
     908            0 :             video_muted: localScreenSharingStream!.videoMuted,
     909              :           ),
     910              :       });
     911              : 
     912            4 :       await pc!.setLocalDescription(answer);
     913            2 :       setCallState(CallState.kConnecting);
     914              : 
     915              :       // Allow a short time for initial candidates to be gathered
     916            4 :       await Future.delayed(Duration(milliseconds: 200));
     917              : 
     918            2 :       final res = await sendAnswerCall(
     919            2 :         room,
     920            2 :         callId,
     921            2 :         answer.sdp!,
     922            2 :         localPartyId,
     923            2 :         type: answer.type!,
     924              :         capabilities: callCapabilities,
     925              :         metadata: metadata,
     926              :         txid: txid,
     927              :       );
     928            6 :       Logs().v('[VOIP] answer res => $res');
     929              : 
     930            2 :       _inviteOrAnswerSent = true;
     931            2 :       _answeredByUs = true;
     932              :     }
     933              :   }
     934              : 
     935              :   /// Reject a call
     936              :   /// This used to be done by calling hangup, but is a separate method and protocol
     937              :   /// event as of MSC2746.
     938            2 :   Future<void> reject({CallErrorCode? reason, bool shouldEmit = true}) async {
     939            4 :     if (state != CallState.kRinging && state != CallState.kFledgling) {
     940            0 :       Logs().e(
     941            0 :         '[VOIP] Call must be in \'ringing|fledgling\' state to reject! (current state was: ${state.toString()}) Calling hangup instead',
     942              :       );
     943            0 :       await hangup(reason: CallErrorCode.userHangup, shouldEmit: shouldEmit);
     944              :       return;
     945              :     }
     946            8 :     Logs().d('[VOIP] Rejecting call: $callId');
     947            2 :     setCallState(CallState.kEnding);
     948            2 :     await terminate(CallParty.kLocal, CallErrorCode.userHangup, shouldEmit);
     949              :     if (shouldEmit) {
     950            8 :       await sendCallReject(room, callId, localPartyId);
     951              :     }
     952              :   }
     953              : 
     954            2 :   Future<void> hangup({
     955              :     required CallErrorCode reason,
     956              :     bool shouldEmit = true,
     957              :   }) async {
     958            2 :     setCallState(CallState.kEnding);
     959            2 :     await terminate(CallParty.kLocal, reason, shouldEmit);
     960              :     try {
     961              :       final res =
     962            8 :           await sendHangupCall(room, callId, localPartyId, 'userHangup');
     963            6 :       Logs().v('[VOIP] hangup res => $res');
     964              :     } catch (e) {
     965            0 :       Logs().v('[VOIP] hangup error => ${e.toString()}');
     966              :     }
     967              :   }
     968              : 
     969            0 :   Future<void> sendDTMF(String tones) async {
     970            0 :     final senders = await pc!.getSenders();
     971            0 :     for (final sender in senders) {
     972            0 :       if (sender.track != null && sender.track!.kind == 'audio') {
     973            0 :         await sender.dtmfSender.insertDTMF(tones);
     974              :         return;
     975              :       } else {
     976            0 :         Logs().w('[VOIP] Unable to find a track to send DTMF on');
     977              :       }
     978              :     }
     979              :   }
     980              : 
     981            2 :   Future<void> terminate(
     982              :     CallParty party,
     983              :     CallErrorCode reason,
     984              :     bool shouldEmit,
     985              :   ) async {
     986            4 :     if (state == CallState.kConnected) {
     987            0 :       await hangup(
     988              :         reason: CallErrorCode.userHangup,
     989              :         shouldEmit: true,
     990              :       );
     991              :       return;
     992              :     }
     993              : 
     994            4 :     Logs().d('[VOIP] terminating call');
     995            4 :     _inviteTimer?.cancel();
     996            2 :     _inviteTimer = null;
     997              : 
     998            4 :     _ringingTimer?.cancel();
     999            2 :     _ringingTimer = null;
    1000              : 
    1001              :     try {
    1002            6 :       await voip.delegate.stopRingtone();
    1003              :     } catch (e) {
    1004              :       // maybe rigntone never started (group calls) or has been stopped already
    1005            0 :       Logs().d('stopping ringtone failed ', e);
    1006              :     }
    1007              : 
    1008            2 :     hangupReason = reason;
    1009              : 
    1010              :     // don't see any reason to wrap this with shouldEmit atm,
    1011              :     // looks like a local state change only
    1012            2 :     setCallState(CallState.kEnded);
    1013              : 
    1014            2 :     if (!isGroupCall) {
    1015              :       // when a call crash and this call is already terminated the currentCId is null.
    1016              :       // So don't return bc the hangup or reject will not proceed anymore.
    1017            4 :       if (voip.currentCID != null &&
    1018           14 :           voip.currentCID != VoipId(roomId: room.id, callId: callId)) {
    1019              :         return;
    1020              :       }
    1021            4 :       voip.currentCID = null;
    1022           12 :       voip.incomingCallRoomId.removeWhere((key, value) => value == callId);
    1023              :     }
    1024              : 
    1025           14 :     voip.calls.removeWhere((key, value) => key.callId == callId);
    1026              : 
    1027            2 :     await cleanUp();
    1028              :     if (shouldEmit) {
    1029            4 :       onCallHangupNotifierForGroupCalls.add(this);
    1030            6 :       await voip.delegate.handleCallEnded(this);
    1031            2 :       fireCallEvent(CallStateChange.kHangup);
    1032            2 :       if ((party == CallParty.kRemote &&
    1033            2 :           _missedCall &&
    1034            2 :           reason != CallErrorCode.answeredElsewhere)) {
    1035            0 :         await voip.delegate.handleMissedCall(this);
    1036              :       }
    1037              :     }
    1038              :   }
    1039              : 
    1040            0 :   Future<void> onRejectReceived(CallErrorCode? reason) async {
    1041            0 :     Logs().v('[VOIP] Reject received for call ID $callId');
    1042              :     // No need to check party_id for reject because if we'd received either
    1043              :     // an answer or reject, we wouldn't be in state InviteSent
    1044            0 :     final shouldTerminate = (state == CallState.kFledgling &&
    1045            0 :             direction == CallDirection.kIncoming) ||
    1046            0 :         CallState.kInviteSent == state ||
    1047            0 :         CallState.kRinging == state;
    1048              : 
    1049              :     if (shouldTerminate) {
    1050            0 :       await terminate(
    1051              :         CallParty.kRemote,
    1052              :         reason ?? CallErrorCode.userHangup,
    1053              :         true,
    1054              :       );
    1055              :     } else {
    1056            0 :       Logs().e('[VOIP] Call is in state: ${state.toString()}: ignoring reject');
    1057              :     }
    1058              :   }
    1059              : 
    1060            2 :   Future<void> _gotLocalOffer(RTCSessionDescription offer) async {
    1061            2 :     if (callHasEnded) {
    1062            0 :       Logs().d(
    1063            0 :         'Ignoring newly created offer on call ID ${opts.callId} because the call has ended',
    1064              :       );
    1065              :       return;
    1066              :     }
    1067              : 
    1068              :     try {
    1069            4 :       await pc!.setLocalDescription(offer);
    1070              :     } catch (err) {
    1071            0 :       Logs().d('Error setting local description! ${err.toString()}');
    1072            0 :       await terminate(
    1073              :         CallParty.kLocal,
    1074              :         CallErrorCode.setLocalDescription,
    1075              :         true,
    1076              :       );
    1077              :       return;
    1078              :     }
    1079              : 
    1080            6 :     if (pc!.iceGatheringState ==
    1081              :         RTCIceGatheringState.RTCIceGatheringStateGathering) {
    1082              :       // Allow a short time for initial candidates to be gathered
    1083            0 :       await Future.delayed(CallTimeouts.iceGatheringDelay);
    1084              :     }
    1085              : 
    1086            2 :     if (callHasEnded) return;
    1087              : 
    1088            2 :     final callCapabilities = CallCapabilities()
    1089            2 :       ..dtmf = false
    1090            2 :       ..transferee = false;
    1091            2 :     final metadata = _getLocalSDPStreamMetadata();
    1092            4 :     if (state == CallState.kCreateOffer) {
    1093            2 :       await sendInviteToCall(
    1094            2 :         room,
    1095            2 :         callId,
    1096            2 :         CallTimeouts.callInviteLifetime.inMilliseconds,
    1097            2 :         localPartyId,
    1098            2 :         offer.sdp!,
    1099              :         capabilities: callCapabilities,
    1100              :         metadata: metadata,
    1101              :       );
    1102              :       // just incase we ended the call but already sent the invite
    1103              :       // raraley happens during glares
    1104            4 :       if (state == CallState.kEnded) {
    1105            0 :         await hangup(reason: CallErrorCode.replaced);
    1106              :         return;
    1107              :       }
    1108            2 :       _inviteOrAnswerSent = true;
    1109              : 
    1110            2 :       if (!isGroupCall) {
    1111            4 :         Logs().d('[glare] set callid because new invite sent');
    1112           12 :         voip.incomingCallRoomId[room.id] = callId;
    1113              :       }
    1114              : 
    1115            2 :       setCallState(CallState.kInviteSent);
    1116              : 
    1117            4 :       _inviteTimer = Timer(CallTimeouts.callInviteLifetime, () {
    1118            0 :         if (state == CallState.kInviteSent) {
    1119            0 :           hangup(reason: CallErrorCode.inviteTimeout);
    1120              :         }
    1121            0 :         _inviteTimer?.cancel();
    1122            0 :         _inviteTimer = null;
    1123              :       });
    1124              :     } else {
    1125            0 :       await sendCallNegotiate(
    1126            0 :         room,
    1127            0 :         callId,
    1128            0 :         CallTimeouts.defaultCallEventLifetime.inMilliseconds,
    1129            0 :         localPartyId,
    1130            0 :         offer.sdp!,
    1131            0 :         type: offer.type!,
    1132              :         capabilities: callCapabilities,
    1133              :         metadata: metadata,
    1134              :       );
    1135              :     }
    1136              :   }
    1137              : 
    1138            2 :   Future<void> onNegotiationNeeded() async {
    1139            4 :     Logs().d('Negotiation is needed!');
    1140            2 :     _makingOffer = true;
    1141              :     try {
    1142              :       // The first addTrack(audio track) on iOS will trigger
    1143              :       // onNegotiationNeeded, which causes creatOffer to only include
    1144              :       // audio m-line, add delay and wait for video track to be added,
    1145              :       // then createOffer can get audio/video m-line correctly.
    1146            2 :       await Future.delayed(CallTimeouts.delayBeforeOffer);
    1147            6 :       final offer = await pc!.createOffer({});
    1148            2 :       await _gotLocalOffer(offer);
    1149              :     } catch (e) {
    1150            0 :       await _getLocalOfferFailed(e);
    1151              :       return;
    1152              :     } finally {
    1153            2 :       _makingOffer = false;
    1154              :     }
    1155              :   }
    1156              : 
    1157            2 :   Future<void> _preparePeerConnection() async {
    1158              :     int iceRestartedCount = 0;
    1159              : 
    1160              :     try {
    1161            4 :       pc = await _createPeerConnection();
    1162            6 :       pc!.onRenegotiationNeeded = onNegotiationNeeded;
    1163              : 
    1164            4 :       pc!.onIceCandidate = (RTCIceCandidate candidate) async {
    1165            0 :         if (callHasEnded) return;
    1166            0 :         _localCandidates.add(candidate);
    1167              : 
    1168            0 :         if (state == CallState.kRinging || !_inviteOrAnswerSent) return;
    1169              : 
    1170              :         // MSC2746 recommends these values (can be quite long when calling because the
    1171              :         // callee will need a while to answer the call)
    1172            0 :         final delay = direction == CallDirection.kIncoming ? 500 : 2000;
    1173            0 :         if (_candidateSendTries == 0) {
    1174            0 :           Timer(Duration(milliseconds: delay), () {
    1175            0 :             _sendCandidateQueue();
    1176              :           });
    1177              :         }
    1178              :       };
    1179              : 
    1180            6 :       pc!.onIceGatheringState = (RTCIceGatheringState state) async {
    1181            8 :         Logs().v('[VOIP] IceGatheringState => ${state.toString()}');
    1182            2 :         if (state == RTCIceGatheringState.RTCIceGatheringStateGathering) {
    1183            0 :           Timer(Duration(seconds: 3), () async {
    1184            0 :             if (!_iceGatheringFinished) {
    1185            0 :               _iceGatheringFinished = true;
    1186            0 :               await _sendCandidateQueue();
    1187              :             }
    1188              :           });
    1189              :         }
    1190            2 :         if (state == RTCIceGatheringState.RTCIceGatheringStateComplete) {
    1191            2 :           if (!_iceGatheringFinished) {
    1192            2 :             _iceGatheringFinished = true;
    1193            2 :             await _sendCandidateQueue();
    1194              :           }
    1195              :         }
    1196              :       };
    1197            6 :       pc!.onIceConnectionState = (RTCIceConnectionState state) async {
    1198            8 :         Logs().v('[VOIP] RTCIceConnectionState => ${state.toString()}');
    1199            2 :         if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) {
    1200            4 :           _localCandidates.clear();
    1201            4 :           _remoteCandidates.clear();
    1202              :           iceRestartedCount = 0;
    1203            2 :           setCallState(CallState.kConnected);
    1204              :           // fix any state/race issues we had with sdp packets and cloned streams
    1205            2 :           await updateMuteStatus();
    1206            2 :           _missedCall = false;
    1207              :         } else if ({
    1208            2 :           RTCIceConnectionState.RTCIceConnectionStateFailed,
    1209            2 :           RTCIceConnectionState.RTCIceConnectionStateDisconnected,
    1210            2 :         }.contains(state)) {
    1211            0 :           if (iceRestartedCount < 3) {
    1212            0 :             await restartIce();
    1213            0 :             iceRestartedCount++;
    1214              :           } else {
    1215            0 :             await hangup(reason: CallErrorCode.iceFailed);
    1216              :           }
    1217              :         }
    1218              :       };
    1219              :     } catch (e) {
    1220            8 :       Logs().v('[VOIP] preparePeerConnection error => ${e.toString()}');
    1221            2 :       await _createPeerConnectionFailed(e);
    1222              :       rethrow;
    1223              :     }
    1224              :   }
    1225              : 
    1226            0 :   Future<void> onAnsweredElsewhere() async {
    1227            0 :     Logs().d('Call ID $callId answered elsewhere');
    1228            0 :     await terminate(CallParty.kRemote, CallErrorCode.answeredElsewhere, true);
    1229              :   }
    1230              : 
    1231            2 :   Future<void> cleanUp() async {
    1232              :     try {
    1233            4 :       for (final stream in _streams) {
    1234            2 :         await stream.dispose();
    1235              :       }
    1236            4 :       _streams.clear();
    1237              :     } catch (e, s) {
    1238            0 :       Logs().e('[VOIP] cleaning up streams failed', e, s);
    1239              :     }
    1240              : 
    1241              :     try {
    1242            2 :       if (pc != null) {
    1243            4 :         await pc!.close();
    1244            4 :         await pc!.dispose();
    1245              :       }
    1246              :     } catch (e, s) {
    1247            0 :       Logs().e('[VOIP] removing pc failed', e, s);
    1248              :     }
    1249              :   }
    1250              : 
    1251            2 :   Future<void> updateMuteStatus() async {
    1252            2 :     final micShouldBeMuted = (localUserMediaStream != null &&
    1253            0 :             localUserMediaStream!.isAudioMuted()) ||
    1254            2 :         _remoteOnHold;
    1255            2 :     final vidShouldBeMuted = (localUserMediaStream != null &&
    1256            0 :             localUserMediaStream!.isVideoMuted()) ||
    1257            2 :         _remoteOnHold;
    1258              : 
    1259            2 :     _setTracksEnabled(
    1260            4 :       localUserMediaStream?.stream?.getAudioTracks() ?? [],
    1261              :       !micShouldBeMuted,
    1262              :     );
    1263            2 :     _setTracksEnabled(
    1264            4 :       localUserMediaStream?.stream?.getVideoTracks() ?? [],
    1265              :       !vidShouldBeMuted,
    1266              :     );
    1267              : 
    1268            2 :     await sendSDPStreamMetadataChanged(
    1269            2 :       room,
    1270            2 :       callId,
    1271            2 :       localPartyId,
    1272            2 :       _getLocalSDPStreamMetadata(),
    1273              :     );
    1274              :   }
    1275              : 
    1276            2 :   void _setTracksEnabled(List<MediaStreamTrack> tracks, bool enabled) {
    1277            2 :     for (final track in tracks) {
    1278            0 :       track.enabled = enabled;
    1279              :     }
    1280              :   }
    1281              : 
    1282            2 :   SDPStreamMetadata _getLocalSDPStreamMetadata() {
    1283            2 :     final sdpStreamMetadatas = <String, SDPStreamPurpose>{};
    1284            4 :     for (final wpstream in getLocalStreams) {
    1285            2 :       if (wpstream.stream != null) {
    1286            8 :         sdpStreamMetadatas[wpstream.stream!.id] = SDPStreamPurpose(
    1287            2 :           purpose: wpstream.purpose,
    1288            2 :           audio_muted: wpstream.audioMuted,
    1289            2 :           video_muted: wpstream.videoMuted,
    1290              :         );
    1291              :       }
    1292              :     }
    1293            2 :     final metadata = SDPStreamMetadata(sdpStreamMetadatas);
    1294           10 :     Logs().v('Got local SDPStreamMetadata ${metadata.toJson().toString()}');
    1295              :     return metadata;
    1296              :   }
    1297              : 
    1298            0 :   Future<void> restartIce() async {
    1299            0 :     Logs().v('[VOIP] iceRestart.');
    1300              :     // Needs restart ice on session.pc and renegotiation.
    1301            0 :     _iceGatheringFinished = false;
    1302            0 :     _localCandidates.clear();
    1303            0 :     await pc!.restartIce();
    1304              :   }
    1305              : 
    1306            2 :   Future<MediaStream?> _getUserMedia(CallType type) async {
    1307            2 :     final mediaConstraints = {
    1308              :       'audio': UserMediaConstraints.micMediaConstraints,
    1309            2 :       'video': type == CallType.kVideo
    1310              :           ? UserMediaConstraints.camMediaConstraints
    1311              :           : false,
    1312              :     };
    1313              :     try {
    1314            8 :       return await voip.delegate.mediaDevices.getUserMedia(mediaConstraints);
    1315              :     } catch (e) {
    1316            0 :       await _getUserMediaFailed(e);
    1317              :     }
    1318              :     return null;
    1319              :   }
    1320              : 
    1321            2 :   Future<MediaStream?> _getDisplayMedia() async {
    1322              :     try {
    1323            6 :       return await voip.delegate.mediaDevices
    1324            2 :           .getDisplayMedia(UserMediaConstraints.screenMediaConstraints);
    1325              :     } catch (e) {
    1326            2 :       await _getDisplayMediaFailed(e);
    1327              :     }
    1328              :     return null;
    1329              :   }
    1330              : 
    1331            2 :   Future<RTCPeerConnection> _createPeerConnection() async {
    1332            2 :     final configuration = <String, dynamic>{
    1333            4 :       'iceServers': opts.iceServers,
    1334              :       'sdpSemantics': 'unified-plan',
    1335              :     };
    1336            6 :     final pc = await voip.delegate.createPeerConnection(configuration);
    1337            2 :     pc.onTrack = (RTCTrackEvent event) async {
    1338            0 :       for (final stream in event.streams) {
    1339            0 :         await _addRemoteStream(stream);
    1340            0 :         for (final track in stream.getTracks()) {
    1341            0 :           track.onEnded = () async {
    1342            0 :             if (stream.getTracks().isEmpty) {
    1343            0 :               Logs().d('[VOIP] detected a empty stream, removing it');
    1344            0 :               await _removeStream(stream);
    1345              :             }
    1346              :           };
    1347              :         }
    1348              :       }
    1349              :     };
    1350              :     return pc;
    1351              :   }
    1352              : 
    1353            0 :   Future<void> createDataChannel(
    1354              :     String label,
    1355              :     RTCDataChannelInit dataChannelDict,
    1356              :   ) async {
    1357            0 :     await pc?.createDataChannel(label, dataChannelDict);
    1358              :   }
    1359              : 
    1360            0 :   Future<void> tryRemoveStopedStreams() async {
    1361            0 :     final removedStreams = <String, WrappedMediaStream>{};
    1362            0 :     for (final stream in _streams) {
    1363            0 :       if (stream.stopped) {
    1364            0 :         removedStreams[stream.stream!.id] = stream;
    1365              :       }
    1366              :     }
    1367            0 :     _streams
    1368            0 :         .removeWhere((stream) => removedStreams.containsKey(stream.stream!.id));
    1369            0 :     for (final element in removedStreams.entries) {
    1370            0 :       await _removeStream(element.value.stream!);
    1371              :     }
    1372              :   }
    1373              : 
    1374            0 :   Future<void> _removeStream(MediaStream stream) async {
    1375            0 :     Logs().v('Removing feed with stream id ${stream.id}');
    1376              : 
    1377            0 :     final it = _streams.where((element) => element.stream!.id == stream.id);
    1378            0 :     if (it.isEmpty) {
    1379            0 :       Logs().v('Didn\'t find the feed with stream id ${stream.id} to delete');
    1380              :       return;
    1381              :     }
    1382            0 :     final wpstream = it.first;
    1383            0 :     _streams.removeWhere((element) => element.stream!.id == stream.id);
    1384            0 :     onStreamRemoved.add(wpstream);
    1385            0 :     fireCallEvent(CallStateChange.kFeedsChanged);
    1386            0 :     await wpstream.dispose();
    1387              :   }
    1388              : 
    1389            2 :   Future<void> _sendCandidateQueue() async {
    1390            2 :     if (callHasEnded) return;
    1391              :     /*
    1392              :     Currently, trickle-ice is not supported, so it will take a
    1393              :     long time to wait to collect all the canidates, set the
    1394              :     timeout for collection canidates to speed up the connection.
    1395              :     */
    1396            2 :     final candidatesQueue = _localCandidates;
    1397              :     try {
    1398            2 :       if (candidatesQueue.isNotEmpty) {
    1399            0 :         final candidates = <Map<String, dynamic>>[];
    1400            0 :         for (final element in candidatesQueue) {
    1401            0 :           candidates.add(element.toMap());
    1402              :         }
    1403            0 :         _localCandidates.clear();
    1404            0 :         final res = await sendCallCandidates(
    1405            0 :           opts.room,
    1406            0 :           callId,
    1407            0 :           localPartyId,
    1408              :           candidates,
    1409              :         );
    1410            0 :         Logs().v('[VOIP] sendCallCandidates res => $res');
    1411              :       }
    1412              :     } catch (e) {
    1413            0 :       Logs().v('[VOIP] sendCallCandidates e => ${e.toString()}');
    1414            0 :       _candidateSendTries++;
    1415            0 :       _localCandidates.clear();
    1416            0 :       _localCandidates.addAll(candidatesQueue);
    1417              : 
    1418            0 :       if (_candidateSendTries > 5) {
    1419            0 :         Logs().d(
    1420            0 :           'Failed to send candidates on attempt $_candidateSendTries Giving up on this call.',
    1421              :         );
    1422            0 :         await hangup(reason: CallErrorCode.iceTimeout);
    1423              :         return;
    1424              :       }
    1425              : 
    1426            0 :       final delay = 500 * pow(2, _candidateSendTries);
    1427            0 :       Timer(Duration(milliseconds: delay as int), () {
    1428            0 :         _sendCandidateQueue();
    1429              :       });
    1430              :     }
    1431              :   }
    1432              : 
    1433            2 :   void fireCallEvent(CallStateChange event) {
    1434            4 :     onCallEventChanged.add(event);
    1435            8 :     Logs().i('CallStateChange: ${event.toString()}');
    1436              :     switch (event) {
    1437            2 :       case CallStateChange.kFeedsChanged:
    1438            4 :         onCallStreamsChanged.add(this);
    1439              :         break;
    1440            2 :       case CallStateChange.kState:
    1441           10 :         Logs().i('CallState: ${state.toString()}');
    1442              :         break;
    1443            2 :       case CallStateChange.kError:
    1444              :         break;
    1445            2 :       case CallStateChange.kHangup:
    1446              :         break;
    1447            0 :       case CallStateChange.kReplaced:
    1448              :         break;
    1449            0 :       case CallStateChange.kLocalHoldUnhold:
    1450              :         break;
    1451            0 :       case CallStateChange.kRemoteHoldUnhold:
    1452              :         break;
    1453            0 :       case CallStateChange.kAssertedIdentityChanged:
    1454              :         break;
    1455              :     }
    1456              :   }
    1457              : 
    1458            2 :   Future<void> _createPeerConnectionFailed(dynamic err) async {
    1459            8 :     Logs().e('Failed to create peer connection object ${err.toString()}');
    1460            2 :     fireCallEvent(CallStateChange.kError);
    1461            2 :     await terminate(
    1462              :       CallParty.kLocal,
    1463              :       CallErrorCode.createPeerConnectionFailed,
    1464              :       true,
    1465              :     );
    1466            2 :     throw CallError(
    1467              :       CallErrorCode.createPeerConnectionFailed,
    1468              :       'Failed to create peer connection object ',
    1469              :       err,
    1470              :     );
    1471              :   }
    1472              : 
    1473            0 :   Future<void> _getLocalOfferFailed(dynamic err) async {
    1474            0 :     Logs().e('Failed to get local offer ${err.toString()}');
    1475            0 :     fireCallEvent(CallStateChange.kError);
    1476            0 :     await terminate(CallParty.kLocal, CallErrorCode.localOfferFailed, true);
    1477            0 :     throw CallError(
    1478              :       CallErrorCode.localOfferFailed,
    1479              :       'Failed to get local offer',
    1480              :       err,
    1481              :     );
    1482              :   }
    1483              : 
    1484            0 :   Future<void> _getUserMediaFailed(dynamic err) async {
    1485            0 :     Logs().w('Failed to get user media - ending call ${err.toString()}');
    1486            0 :     fireCallEvent(CallStateChange.kError);
    1487            0 :     await terminate(CallParty.kLocal, CallErrorCode.userMediaFailed, true);
    1488            0 :     throw CallError(
    1489              :       CallErrorCode.userMediaFailed,
    1490              :       'Failed to get user media',
    1491              :       err,
    1492              :     );
    1493              :   }
    1494              : 
    1495            2 :   Future<void> _getDisplayMediaFailed(dynamic err) async {
    1496            8 :     Logs().w('Failed to get display media - ending call ${err.toString()}');
    1497            2 :     fireCallEvent(CallStateChange.kError);
    1498              :     // We don't terminate the call here because the user might still want to stay
    1499              :     // on the call and try again later.
    1500            2 :     throw CallError(
    1501              :       CallErrorCode.displayMediaFailed,
    1502              :       'Failed to get display media',
    1503              :       err,
    1504              :     );
    1505              :   }
    1506              : 
    1507            2 :   Future<void> onSelectAnswerReceived(String? selectedPartyId) async {
    1508            4 :     if (direction != CallDirection.kIncoming) {
    1509            0 :       Logs().w('Got select_answer for an outbound call: ignoring');
    1510              :       return;
    1511              :     }
    1512              :     if (selectedPartyId == null) {
    1513            0 :       Logs().w(
    1514              :         'Got nonsensical select_answer with null/undefined selected_party_id: ignoring',
    1515              :       );
    1516              :       return;
    1517              :     }
    1518              : 
    1519            4 :     if (selectedPartyId != localPartyId) {
    1520            4 :       Logs().w(
    1521            4 :         'Got select_answer for party ID $selectedPartyId: we are party ID $localPartyId.',
    1522              :       );
    1523              :       // The other party has picked somebody else's answer
    1524            2 :       await terminate(CallParty.kRemote, CallErrorCode.answeredElsewhere, true);
    1525              :     }
    1526              :   }
    1527              : 
    1528              :   /// This is sent by the caller when they wish to establish a call.
    1529              :   /// [callId] is a unique identifier for the call.
    1530              :   /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
    1531              :   /// [lifetime] is the time in milliseconds that the invite is valid for. Once the invite age exceeds this value,
    1532              :   /// clients should discard it. They should also no longer show the call as awaiting an answer in the UI.
    1533              :   /// [type] The type of session description. Must be 'offer'.
    1534              :   /// [sdp] The SDP text of the session description.
    1535              :   /// [invitee] The user ID of the person who is being invited. Invites without an invitee field are defined to be
    1536              :   /// intended for any member of the room other than the sender of the event.
    1537              :   /// [party_id] The party ID for call, Can be set to client.deviceId.
    1538            2 :   Future<String?> sendInviteToCall(
    1539              :     Room room,
    1540              :     String callId,
    1541              :     int lifetime,
    1542              :     String party_id,
    1543              :     String sdp, {
    1544              :     String type = 'offer',
    1545              :     String version = voipProtoVersion,
    1546              :     String? txid,
    1547              :     CallCapabilities? capabilities,
    1548              :     SDPStreamMetadata? metadata,
    1549              :   }) async {
    1550            2 :     final content = {
    1551            2 :       'call_id': callId,
    1552            2 :       'party_id': party_id,
    1553            2 :       if (groupCallId != null) 'conf_id': groupCallId!,
    1554            2 :       'version': version,
    1555            2 :       'lifetime': lifetime,
    1556            4 :       'offer': {'sdp': sdp, 'type': type},
    1557            2 :       if (remoteUserId != null)
    1558            2 :         'invitee':
    1559            2 :             remoteUserId!, // TODO: rename this to invitee_user_id? breaks spec though
    1560            2 :       if (remoteDeviceId != null) 'invitee_device_id': remoteDeviceId!,
    1561            2 :       if (remoteDeviceId != null)
    1562            0 :         'device_id': client
    1563            0 :             .deviceID!, // Having a remoteDeviceId means you are doing to-device events, so you want to send your deviceId too
    1564            4 :       if (capabilities != null) 'capabilities': capabilities.toJson(),
    1565            4 :       if (metadata != null) sdpStreamMetadataKey: metadata.toJson(),
    1566              :     };
    1567            2 :     return await _sendContent(
    1568              :       room,
    1569            2 :       isGroupCall ? EventTypes.GroupCallMemberInvite : EventTypes.CallInvite,
    1570              :       content,
    1571              :       txid: txid,
    1572              :     );
    1573              :   }
    1574              : 
    1575              :   /// The calling party sends the party_id of the first selected answer.
    1576              :   ///
    1577              :   /// Usually after receiving the first answer sdp in the client.onCallAnswer event,
    1578              :   /// save the `party_id`, and then send `CallSelectAnswer` to others peers that the call has been picked up.
    1579              :   ///
    1580              :   /// [callId] is a unique identifier for the call.
    1581              :   /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
    1582              :   /// [party_id] The party ID for call, Can be set to client.deviceId.
    1583              :   /// [selected_party_id] The party ID for the selected answer.
    1584            2 :   Future<String?> sendSelectCallAnswer(
    1585              :     Room room,
    1586              :     String callId,
    1587              :     String party_id,
    1588              :     String selected_party_id, {
    1589              :     String version = voipProtoVersion,
    1590              :     String? txid,
    1591              :   }) async {
    1592            2 :     final content = {
    1593            2 :       'call_id': callId,
    1594            2 :       'party_id': party_id,
    1595            2 :       if (groupCallId != null) 'conf_id': groupCallId!,
    1596            2 :       'version': version,
    1597            2 :       'selected_party_id': selected_party_id,
    1598              :     };
    1599              : 
    1600            2 :     return await _sendContent(
    1601              :       room,
    1602            2 :       isGroupCall
    1603              :           ? EventTypes.GroupCallMemberSelectAnswer
    1604              :           : EventTypes.CallSelectAnswer,
    1605              :       content,
    1606              :       txid: txid,
    1607              :     );
    1608              :   }
    1609              : 
    1610              :   /// Reject a call
    1611              :   /// [callId] is a unique identifier for the call.
    1612              :   /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
    1613              :   /// [party_id] The party ID for call, Can be set to client.deviceId.
    1614            2 :   Future<String?> sendCallReject(
    1615              :     Room room,
    1616              :     String callId,
    1617              :     String party_id, {
    1618              :     String version = voipProtoVersion,
    1619              :     String? txid,
    1620              :   }) async {
    1621            2 :     final content = {
    1622            2 :       'call_id': callId,
    1623            2 :       'party_id': party_id,
    1624            2 :       if (groupCallId != null) 'conf_id': groupCallId!,
    1625            2 :       'version': version,
    1626              :     };
    1627              : 
    1628            2 :     return await _sendContent(
    1629              :       room,
    1630            2 :       isGroupCall ? EventTypes.GroupCallMemberReject : EventTypes.CallReject,
    1631              :       content,
    1632              :       txid: txid,
    1633              :     );
    1634              :   }
    1635              : 
    1636              :   /// When local audio/video tracks are added/deleted or hold/unhold,
    1637              :   /// need to createOffer and renegotiation.
    1638              :   /// [callId] is a unique identifier for the call.
    1639              :   /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
    1640              :   /// [party_id] The party ID for call, Can be set to client.deviceId.
    1641            2 :   Future<String?> sendCallNegotiate(
    1642              :     Room room,
    1643              :     String callId,
    1644              :     int lifetime,
    1645              :     String party_id,
    1646              :     String sdp, {
    1647              :     String type = 'offer',
    1648              :     String version = voipProtoVersion,
    1649              :     String? txid,
    1650              :     CallCapabilities? capabilities,
    1651              :     SDPStreamMetadata? metadata,
    1652              :   }) async {
    1653            2 :     final content = {
    1654            2 :       'call_id': callId,
    1655            2 :       'party_id': party_id,
    1656            2 :       if (groupCallId != null) 'conf_id': groupCallId!,
    1657            2 :       'version': version,
    1658            2 :       'lifetime': lifetime,
    1659            4 :       'description': {'sdp': sdp, 'type': type},
    1660            0 :       if (capabilities != null) 'capabilities': capabilities.toJson(),
    1661            0 :       if (metadata != null) sdpStreamMetadataKey: metadata.toJson(),
    1662              :     };
    1663            2 :     return await _sendContent(
    1664              :       room,
    1665            2 :       isGroupCall
    1666              :           ? EventTypes.GroupCallMemberNegotiate
    1667              :           : EventTypes.CallNegotiate,
    1668              :       content,
    1669              :       txid: txid,
    1670              :     );
    1671              :   }
    1672              : 
    1673              :   /// This is sent by callers after sending an invite and by the callee after answering.
    1674              :   /// Its purpose is to give the other party additional ICE candidates to try using to communicate.
    1675              :   ///
    1676              :   /// [callId] The ID of the call this event relates to.
    1677              :   ///
    1678              :   /// [version] The version of the VoIP specification this messages adheres to. This specification is version 1.
    1679              :   ///
    1680              :   /// [party_id] The party ID for call, Can be set to client.deviceId.
    1681              :   ///
    1682              :   /// [candidates] Array of objects describing the candidates. Example:
    1683              :   ///
    1684              :   /// ```
    1685              :   /// [
    1686              :   ///       {
    1687              :   ///           "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0",
    1688              :   ///           "sdpMLineIndex": 0,
    1689              :   ///           "sdpMid": "audio"
    1690              :   ///       }
    1691              :   ///   ],
    1692              :   /// ```
    1693            2 :   Future<String?> sendCallCandidates(
    1694              :     Room room,
    1695              :     String callId,
    1696              :     String party_id,
    1697              :     List<Map<String, dynamic>> candidates, {
    1698              :     String version = voipProtoVersion,
    1699              :     String? txid,
    1700              :   }) async {
    1701            2 :     final content = {
    1702            2 :       'call_id': callId,
    1703            2 :       'party_id': party_id,
    1704            2 :       if (groupCallId != null) 'conf_id': groupCallId!,
    1705            2 :       'version': version,
    1706            2 :       'candidates': candidates,
    1707              :     };
    1708            2 :     return await _sendContent(
    1709              :       room,
    1710            2 :       isGroupCall
    1711              :           ? EventTypes.GroupCallMemberCandidates
    1712              :           : EventTypes.CallCandidates,
    1713              :       content,
    1714              :       txid: txid,
    1715              :     );
    1716              :   }
    1717              : 
    1718              :   /// This event is sent by the callee when they wish to answer the call.
    1719              :   /// [callId] is a unique identifier for the call.
    1720              :   /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
    1721              :   /// [type] The type of session description. Must be 'answer'.
    1722              :   /// [sdp] The SDP text of the session description.
    1723              :   /// [party_id] The party ID for call, Can be set to client.deviceId.
    1724            2 :   Future<String?> sendAnswerCall(
    1725              :     Room room,
    1726              :     String callId,
    1727              :     String sdp,
    1728              :     String party_id, {
    1729              :     String type = 'answer',
    1730              :     String version = voipProtoVersion,
    1731              :     String? txid,
    1732              :     CallCapabilities? capabilities,
    1733              :     SDPStreamMetadata? metadata,
    1734              :   }) async {
    1735            2 :     final content = {
    1736            2 :       'call_id': callId,
    1737            2 :       'party_id': party_id,
    1738            2 :       if (groupCallId != null) 'conf_id': groupCallId!,
    1739            2 :       'version': version,
    1740            4 :       'answer': {'sdp': sdp, 'type': type},
    1741            4 :       if (capabilities != null) 'capabilities': capabilities.toJson(),
    1742            4 :       if (metadata != null) sdpStreamMetadataKey: metadata.toJson(),
    1743              :     };
    1744            2 :     return await _sendContent(
    1745              :       room,
    1746            2 :       isGroupCall ? EventTypes.GroupCallMemberAnswer : EventTypes.CallAnswer,
    1747              :       content,
    1748              :       txid: txid,
    1749              :     );
    1750              :   }
    1751              : 
    1752              :   /// This event is sent by the callee when they wish to answer the call.
    1753              :   /// [callId] The ID of the call this event relates to.
    1754              :   /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
    1755              :   /// [party_id] The party ID for call, Can be set to client.deviceId.
    1756            2 :   Future<String?> sendHangupCall(
    1757              :     Room room,
    1758              :     String callId,
    1759              :     String party_id,
    1760              :     String? hangupCause, {
    1761              :     String version = voipProtoVersion,
    1762              :     String? txid,
    1763              :   }) async {
    1764            2 :     final content = {
    1765            2 :       'call_id': callId,
    1766            2 :       'party_id': party_id,
    1767            2 :       if (groupCallId != null) 'conf_id': groupCallId!,
    1768            2 :       'version': version,
    1769            2 :       if (hangupCause != null) 'reason': hangupCause,
    1770              :     };
    1771            2 :     return await _sendContent(
    1772              :       room,
    1773            2 :       isGroupCall ? EventTypes.GroupCallMemberHangup : EventTypes.CallHangup,
    1774              :       content,
    1775              :       txid: txid,
    1776              :     );
    1777              :   }
    1778              : 
    1779              :   /// Send SdpStreamMetadata Changed event.
    1780              :   ///
    1781              :   /// This MSC also adds a new call event m.call.sdp_stream_metadata_changed,
    1782              :   /// which has the common VoIP fields as specified in
    1783              :   /// MSC2746 (version, call_id, party_id) and a sdp_stream_metadata object which
    1784              :   /// is the same thing as sdp_stream_metadata in m.call.negotiate, m.call.invite
    1785              :   /// and m.call.answer. The client sends this event the when sdp_stream_metadata
    1786              :   /// has changed but no negotiation is required
    1787              :   ///  (e.g. the user mutes their camera/microphone).
    1788              :   ///
    1789              :   /// [callId] The ID of the call this event relates to.
    1790              :   /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
    1791              :   /// [party_id] The party ID for call, Can be set to client.deviceId.
    1792              :   /// [metadata] The sdp_stream_metadata object.
    1793            2 :   Future<String?> sendSDPStreamMetadataChanged(
    1794              :     Room room,
    1795              :     String callId,
    1796              :     String party_id,
    1797              :     SDPStreamMetadata metadata, {
    1798              :     String version = voipProtoVersion,
    1799              :     String? txid,
    1800              :   }) async {
    1801            2 :     final content = {
    1802            2 :       'call_id': callId,
    1803            2 :       'party_id': party_id,
    1804            2 :       if (groupCallId != null) 'conf_id': groupCallId!,
    1805            2 :       'version': version,
    1806            4 :       sdpStreamMetadataKey: metadata.toJson(),
    1807              :     };
    1808            2 :     return await _sendContent(
    1809              :       room,
    1810            2 :       isGroupCall
    1811              :           ? EventTypes.GroupCallMemberSDPStreamMetadataChanged
    1812              :           : EventTypes.CallSDPStreamMetadataChanged,
    1813              :       content,
    1814              :       txid: txid,
    1815              :     );
    1816              :   }
    1817              : 
    1818              :   /// CallReplacesEvent for Transfered calls
    1819              :   ///
    1820              :   /// [callId] The ID of the call this event relates to.
    1821              :   /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
    1822              :   /// [party_id] The party ID for call, Can be set to client.deviceId.
    1823              :   /// [callReplaces] transfer info
    1824            2 :   Future<String?> sendCallReplaces(
    1825              :     Room room,
    1826              :     String callId,
    1827              :     String party_id,
    1828              :     CallReplaces callReplaces, {
    1829              :     String version = voipProtoVersion,
    1830              :     String? txid,
    1831              :   }) async {
    1832            2 :     final content = {
    1833            2 :       'call_id': callId,
    1834            2 :       'party_id': party_id,
    1835            2 :       if (groupCallId != null) 'conf_id': groupCallId!,
    1836            2 :       'version': version,
    1837            2 :       ...callReplaces.toJson(),
    1838              :     };
    1839            2 :     return await _sendContent(
    1840              :       room,
    1841            2 :       isGroupCall
    1842              :           ? EventTypes.GroupCallMemberReplaces
    1843              :           : EventTypes.CallReplaces,
    1844              :       content,
    1845              :       txid: txid,
    1846              :     );
    1847              :   }
    1848              : 
    1849              :   /// send AssertedIdentity event
    1850              :   ///
    1851              :   /// [callId] The ID of the call this event relates to.
    1852              :   /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
    1853              :   /// [party_id] The party ID for call, Can be set to client.deviceId.
    1854              :   /// [assertedIdentity] the asserted identity
    1855            2 :   Future<String?> sendAssertedIdentity(
    1856              :     Room room,
    1857              :     String callId,
    1858              :     String party_id,
    1859              :     AssertedIdentity assertedIdentity, {
    1860              :     String version = voipProtoVersion,
    1861              :     String? txid,
    1862              :   }) async {
    1863            2 :     final content = {
    1864            2 :       'call_id': callId,
    1865            2 :       'party_id': party_id,
    1866            2 :       if (groupCallId != null) 'conf_id': groupCallId!,
    1867            2 :       'version': version,
    1868            4 :       'asserted_identity': assertedIdentity.toJson(),
    1869              :     };
    1870            2 :     return await _sendContent(
    1871              :       room,
    1872            2 :       isGroupCall
    1873              :           ? EventTypes.GroupCallMemberAssertedIdentity
    1874              :           : EventTypes.CallAssertedIdentity,
    1875              :       content,
    1876              :       txid: txid,
    1877              :     );
    1878              :   }
    1879              : 
    1880            2 :   Future<String?> _sendContent(
    1881              :     Room room,
    1882              :     String type,
    1883              :     Map<String, Object> content, {
    1884              :     String? txid,
    1885              :   }) async {
    1886            6 :     Logs().d('[VOIP] sending content type $type, with conf: $content');
    1887            0 :     txid ??= VoIP.customTxid ?? client.generateUniqueTransactionId();
    1888            2 :     final mustEncrypt = room.encrypted && client.encryptionEnabled;
    1889              : 
    1890              :     // opponentDeviceId is only set for a few events during group calls,
    1891              :     // therefore only group calls use to-device messages for call events
    1892            2 :     if (isGroupCall && remoteDeviceId != null) {
    1893            0 :       final toDeviceSeq = _toDeviceSeq++;
    1894            0 :       final Map<String, Object> data = {
    1895              :         ...content,
    1896            0 :         'seq': toDeviceSeq,
    1897            0 :         if (remoteSessionId != null) 'dest_session_id': remoteSessionId!,
    1898            0 :         'sender_session_id': voip.currentSessionId,
    1899            0 :         'room_id': room.id,
    1900              :       };
    1901              : 
    1902              :       if (mustEncrypt) {
    1903            0 :         await client.userDeviceKeysLoading;
    1904            0 :         if (client.userDeviceKeys[remoteUserId]?.deviceKeys[remoteDeviceId] !=
    1905              :             null) {
    1906            0 :           await client.sendToDeviceEncrypted(
    1907            0 :             [
    1908            0 :               client.userDeviceKeys[remoteUserId]!.deviceKeys[remoteDeviceId]!,
    1909              :             ],
    1910              :             type,
    1911              :             data,
    1912              :           );
    1913              :         } else {
    1914            0 :           Logs().w(
    1915            0 :             '[VOIP] _sendCallContent missing device keys for $remoteUserId',
    1916              :           );
    1917              :         }
    1918              :       } else {
    1919            0 :         await client.sendToDevice(
    1920              :           type,
    1921              :           txid,
    1922            0 :           {
    1923            0 :             remoteUserId!: {remoteDeviceId!: data},
    1924              :           },
    1925              :         );
    1926              :       }
    1927              :       return '';
    1928              :     } else {
    1929              :       final sendMessageContent = mustEncrypt
    1930            0 :           ? await client.encryption!
    1931            0 :               .encryptGroupMessagePayload(room.id, content, type: type)
    1932              :           : content;
    1933            4 :       return await client.sendMessage(
    1934            2 :         room.id,
    1935            2 :         sendMessageContent.containsKey('ciphertext')
    1936              :             ? EventTypes.Encrypted
    1937              :             : type,
    1938              :         txid,
    1939              :         sendMessageContent,
    1940              :       );
    1941              :     }
    1942              :   }
    1943              : }
        

Generated by: LCOV version 2.0-1