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 : }
|