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 0 : Future<bool> setScreensharingEnabled(bool enabled) async {
563 : // Skip if there is nothing to do
564 0 : 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 0 : Logs().d('Set screensharing enabled? $enabled');
577 :
578 : if (enabled) {
579 : try {
580 0 : 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 0 : fireCallEvent(CallStateChange.kError);
596 : return false;
597 : }
598 : } else {
599 : try {
600 0 : for (final sender in _screensharingSenders) {
601 0 : await pc!.removeTrack(sender);
602 : }
603 0 : for (final track in localScreenSharingStream!.stream!.getTracks()) {
604 0 : await track.stop();
605 : }
606 0 : localScreenSharingStream!.stopped = true;
607 0 : await _removeStream(localScreenSharingStream!.stream!);
608 0 : fireCallEvent(CallStateChange.kFeedsChanged);
609 : return false;
610 : } catch (e, s) {
611 0 : Logs().e('[VOIP] stopping screen sharing track failed', e, s);
612 : return false;
613 : }
614 : }
615 : }
616 :
617 2 : Future<void> addLocalStream(
618 : MediaStream stream,
619 : String purpose, {
620 : bool addToPeerConnection = true,
621 : }) async {
622 : final existingStream =
623 4 : getLocalStreams.where((element) => element.purpose == purpose);
624 2 : if (existingStream.isNotEmpty) {
625 0 : existingStream.first.setNewStream(stream);
626 : } else {
627 2 : final newStream = WrappedMediaStream(
628 2 : participant: localParticipant!,
629 4 : room: opts.room,
630 : stream: stream,
631 : purpose: purpose,
632 2 : client: client,
633 4 : audioMuted: stream.getAudioTracks().isEmpty,
634 4 : videoMuted: stream.getVideoTracks().isEmpty,
635 2 : isGroupCall: groupCallId != null,
636 2 : pc: pc,
637 2 : voip: voip,
638 : );
639 4 : _streams.add(newStream);
640 4 : onStreamAdd.add(newStream);
641 : }
642 :
643 : if (addToPeerConnection) {
644 2 : if (purpose == SDPStreamMetadataPurpose.Screenshare) {
645 0 : _screensharingSenders.clear();
646 0 : for (final track in stream.getTracks()) {
647 0 : _screensharingSenders.add(await pc!.addTrack(track, stream));
648 : }
649 2 : } else if (purpose == SDPStreamMetadataPurpose.Usermedia) {
650 4 : _usermediaSenders.clear();
651 2 : for (final track in stream.getTracks()) {
652 0 : _usermediaSenders.add(await pc!.addTrack(track, stream));
653 : }
654 : }
655 : }
656 :
657 2 : if (purpose == SDPStreamMetadataPurpose.Usermedia) {
658 6 : _speakerOn = type == CallType.kVideo;
659 10 : if (!voip.delegate.isWeb && stream.getAudioTracks().isNotEmpty) {
660 0 : final audioTrack = stream.getAudioTracks()[0];
661 0 : audioTrack.enableSpeakerphone(_speakerOn);
662 : }
663 : }
664 :
665 2 : fireCallEvent(CallStateChange.kFeedsChanged);
666 : }
667 :
668 0 : Future<void> _addRemoteStream(MediaStream stream) async {
669 : //final userId = remoteUser.id;
670 0 : final metadata = _remoteSDPStreamMetadata?.sdpStreamMetadatas[stream.id];
671 : if (metadata == null) {
672 0 : Logs().i(
673 0 : 'Ignoring stream with id ${stream.id} because we didn\'t get any metadata about it',
674 : );
675 : return;
676 : }
677 :
678 0 : final purpose = metadata.purpose;
679 0 : final audioMuted = metadata.audio_muted;
680 0 : final videoMuted = metadata.video_muted;
681 :
682 : // Try to find a feed with the same purpose as the new stream,
683 : // if we find it replace the old stream with the new one
684 : final existingStream =
685 0 : getRemoteStreams.where((element) => element.purpose == purpose);
686 0 : if (existingStream.isNotEmpty) {
687 0 : existingStream.first.setNewStream(stream);
688 : } else {
689 0 : final newStream = WrappedMediaStream(
690 0 : participant: CallParticipant(
691 0 : voip,
692 0 : userId: remoteUserId!,
693 0 : deviceId: remoteDeviceId,
694 : ),
695 0 : room: opts.room,
696 : stream: stream,
697 : purpose: purpose,
698 0 : client: client,
699 : audioMuted: audioMuted,
700 : videoMuted: videoMuted,
701 0 : isGroupCall: groupCallId != null,
702 0 : pc: pc,
703 0 : voip: voip,
704 : );
705 0 : _streams.add(newStream);
706 0 : onStreamAdd.add(newStream);
707 : }
708 0 : fireCallEvent(CallStateChange.kFeedsChanged);
709 0 : Logs().i('Pushed remote stream (id="${stream.id}", purpose=$purpose)');
710 : }
711 :
712 0 : Future<void> deleteAllStreams() async {
713 0 : for (final stream in _streams) {
714 0 : if (stream.isLocal() || groupCallId == null) {
715 0 : await stream.dispose();
716 : }
717 : }
718 0 : _streams.clear();
719 0 : fireCallEvent(CallStateChange.kFeedsChanged);
720 : }
721 :
722 0 : Future<void> deleteFeedByStream(MediaStream stream) async {
723 : final index =
724 0 : _streams.indexWhere((element) => element.stream!.id == stream.id);
725 0 : if (index == -1) {
726 0 : Logs().w('Didn\'t find the feed with stream id ${stream.id} to delete');
727 : return;
728 : }
729 0 : final wstream = _streams.elementAt(index);
730 0 : onStreamRemoved.add(wstream);
731 0 : await deleteStream(wstream);
732 : }
733 :
734 0 : Future<void> deleteStream(WrappedMediaStream stream) async {
735 0 : await stream.dispose();
736 0 : _streams.removeAt(_streams.indexOf(stream));
737 0 : fireCallEvent(CallStateChange.kFeedsChanged);
738 : }
739 :
740 0 : Future<void> removeLocalStream(WrappedMediaStream callFeed) async {
741 0 : final senderArray = callFeed.purpose == SDPStreamMetadataPurpose.Usermedia
742 0 : ? _usermediaSenders
743 0 : : _screensharingSenders;
744 :
745 0 : for (final element in senderArray) {
746 0 : await pc!.removeTrack(element);
747 : }
748 :
749 0 : if (callFeed.purpose == SDPStreamMetadataPurpose.Screenshare) {
750 0 : await stopMediaStream(callFeed.stream);
751 : }
752 :
753 : // Empty the array
754 0 : senderArray.removeRange(0, senderArray.length);
755 0 : onStreamRemoved.add(callFeed);
756 0 : await deleteStream(callFeed);
757 : }
758 :
759 2 : void setCallState(CallState newState) {
760 2 : _state = newState;
761 4 : onCallStateChanged.add(newState);
762 2 : fireCallEvent(CallStateChange.kState);
763 : }
764 :
765 0 : Future<void> setLocalVideoMuted(bool muted) async {
766 : if (!muted) {
767 0 : final videoToSend = await hasVideoToSend();
768 : if (!videoToSend) {
769 0 : if (_remoteSDPStreamMetadata == null) return;
770 0 : await insertVideoTrackToAudioOnlyStream();
771 : }
772 : }
773 0 : localUserMediaStream?.setVideoMuted(muted);
774 0 : await updateMuteStatus();
775 : }
776 :
777 : // used for upgrading 1:1 calls
778 0 : Future<void> insertVideoTrackToAudioOnlyStream() async {
779 0 : if (localUserMediaStream != null && localUserMediaStream!.stream != null) {
780 0 : final stream = await _getUserMedia(CallType.kVideo);
781 : if (stream != null) {
782 0 : Logs().d('[VOIP] running replaceTracks() on stream: ${stream.id}');
783 0 : _setTracksEnabled(stream.getVideoTracks(), true);
784 : // replace local tracks
785 0 : for (final track in localUserMediaStream!.stream!.getTracks()) {
786 : try {
787 0 : await localUserMediaStream!.stream!.removeTrack(track);
788 0 : await track.stop();
789 : } catch (e) {
790 0 : Logs().w('failed to stop track');
791 : }
792 : }
793 0 : final streamTracks = stream.getTracks();
794 0 : for (final newTrack in streamTracks) {
795 0 : await localUserMediaStream!.stream!.addTrack(newTrack);
796 : }
797 :
798 : // remove any screen sharing or remote transceivers, these don't need
799 : // to be replaced anyway.
800 0 : final transceivers = await pc!.getTransceivers();
801 0 : transceivers.removeWhere(
802 0 : (transceiver) =>
803 0 : transceiver.sender.track == null ||
804 0 : (localScreenSharingStream != null &&
805 0 : localScreenSharingStream!.stream != null &&
806 0 : localScreenSharingStream!.stream!
807 0 : .getTracks()
808 0 : .map((e) => e.id)
809 0 : .contains(transceiver.sender.track?.id)),
810 : );
811 :
812 : // in an ideal case the following should happen
813 : // - audio track gets replaced
814 : // - new video track gets added
815 0 : for (final newTrack in streamTracks) {
816 0 : final transceiver = transceivers.singleWhereOrNull(
817 0 : (transceiver) => transceiver.sender.track!.kind == newTrack.kind,
818 : );
819 : if (transceiver != null) {
820 0 : Logs().d(
821 0 : '[VOIP] replacing ${transceiver.sender.track} in transceiver',
822 : );
823 0 : final oldSender = transceiver.sender;
824 0 : await oldSender.replaceTrack(newTrack);
825 0 : await transceiver.setDirection(
826 0 : await transceiver.getDirection() ==
827 : TransceiverDirection.Inactive // upgrade, send now
828 : ? TransceiverDirection.SendOnly
829 : : TransceiverDirection.SendRecv,
830 : );
831 : } else {
832 : // adding transceiver
833 0 : Logs().d('[VOIP] adding track $newTrack to pc');
834 0 : await pc!.addTrack(newTrack, localUserMediaStream!.stream!);
835 : }
836 : }
837 : // for renderer to be able to show new video track
838 0 : localUserMediaStream?.onStreamChanged
839 0 : .add(localUserMediaStream!.stream!);
840 : }
841 : }
842 : }
843 :
844 0 : Future<void> setMicrophoneMuted(bool muted) async {
845 0 : localUserMediaStream?.setAudioMuted(muted);
846 0 : await updateMuteStatus();
847 : }
848 :
849 0 : Future<void> setRemoteOnHold(bool onHold) async {
850 0 : if (remoteOnHold == onHold) return;
851 0 : _remoteOnHold = onHold;
852 0 : final transceivers = await pc!.getTransceivers();
853 0 : for (final transceiver in transceivers) {
854 0 : await transceiver.setDirection(
855 : onHold ? TransceiverDirection.SendOnly : TransceiverDirection.SendRecv,
856 : );
857 : }
858 0 : await updateMuteStatus();
859 0 : fireCallEvent(CallStateChange.kRemoteHoldUnhold);
860 : }
861 :
862 0 : Future<bool> isLocalOnHold() async {
863 0 : if (state != CallState.kConnected) return false;
864 : var callOnHold = true;
865 : // We consider a call to be on hold only if *all* the tracks are on hold
866 : // (is this the right thing to do?)
867 0 : final transceivers = await pc!.getTransceivers();
868 0 : for (final transceiver in transceivers) {
869 0 : final currentDirection = await transceiver.getCurrentDirection();
870 0 : final trackOnHold = (currentDirection == TransceiverDirection.Inactive ||
871 0 : currentDirection == TransceiverDirection.RecvOnly);
872 : if (!trackOnHold) {
873 : callOnHold = false;
874 : }
875 : }
876 : return callOnHold;
877 : }
878 :
879 2 : Future<void> answer({String? txid}) async {
880 2 : if (_inviteOrAnswerSent) {
881 : return;
882 : }
883 : // stop play ringtone
884 6 : await voip.delegate.stopRingtone();
885 :
886 4 : if (direction == CallDirection.kIncoming) {
887 2 : setCallState(CallState.kCreateAnswer);
888 :
889 6 : final answer = await pc!.createAnswer({});
890 4 : for (final candidate in _remoteCandidates) {
891 4 : await pc!.addCandidate(candidate);
892 : }
893 :
894 2 : final callCapabilities = CallCapabilities()
895 2 : ..dtmf = false
896 2 : ..transferee = false;
897 :
898 4 : final metadata = SDPStreamMetadata({
899 2 : if (localUserMediaStream != null)
900 10 : localUserMediaStream!.stream!.id: SDPStreamPurpose(
901 : purpose: SDPStreamMetadataPurpose.Usermedia,
902 4 : audio_muted: localUserMediaStream!.audioMuted,
903 4 : video_muted: localUserMediaStream!.videoMuted,
904 : ),
905 2 : if (localScreenSharingStream != null)
906 0 : localScreenSharingStream!.stream!.id: SDPStreamPurpose(
907 : purpose: SDPStreamMetadataPurpose.Screenshare,
908 0 : audio_muted: localScreenSharingStream!.audioMuted,
909 0 : video_muted: localScreenSharingStream!.videoMuted,
910 : ),
911 : });
912 :
913 4 : await pc!.setLocalDescription(answer);
914 2 : setCallState(CallState.kConnecting);
915 :
916 : // Allow a short time for initial candidates to be gathered
917 4 : await Future.delayed(Duration(milliseconds: 200));
918 :
919 2 : final res = await sendAnswerCall(
920 2 : room,
921 2 : callId,
922 2 : answer.sdp!,
923 2 : localPartyId,
924 2 : type: answer.type!,
925 : capabilities: callCapabilities,
926 : metadata: metadata,
927 : txid: txid,
928 : );
929 6 : Logs().v('[VOIP] answer res => $res');
930 :
931 2 : _inviteOrAnswerSent = true;
932 2 : _answeredByUs = true;
933 : }
934 : }
935 :
936 : /// Reject a call
937 : /// This used to be done by calling hangup, but is a separate method and protocol
938 : /// event as of MSC2746.
939 2 : Future<void> reject({CallErrorCode? reason, bool shouldEmit = true}) async {
940 2 : setCallState(CallState.kEnding);
941 8 : if (state != CallState.kRinging && state != CallState.kFledgling) {
942 4 : Logs().e(
943 6 : '[VOIP] Call must be in \'ringing|fledgling\' state to reject! (current state was: ${state.toString()}) Calling hangup instead',
944 : );
945 2 : await hangup(reason: CallErrorCode.userHangup, shouldEmit: shouldEmit);
946 : return;
947 : }
948 0 : Logs().d('[VOIP] Rejecting call: $callId');
949 0 : await terminate(CallParty.kLocal, CallErrorCode.userHangup, shouldEmit);
950 : if (shouldEmit) {
951 0 : await sendCallReject(room, callId, localPartyId);
952 : }
953 : }
954 :
955 2 : Future<void> hangup({
956 : required CallErrorCode reason,
957 : bool shouldEmit = true,
958 : }) async {
959 2 : setCallState(CallState.kEnding);
960 2 : await terminate(CallParty.kLocal, reason, shouldEmit);
961 : try {
962 : final res =
963 8 : await sendHangupCall(room, callId, localPartyId, 'userHangup');
964 6 : Logs().v('[VOIP] hangup res => $res');
965 : } catch (e) {
966 0 : Logs().v('[VOIP] hangup error => ${e.toString()}');
967 : }
968 : }
969 :
970 0 : Future<void> sendDTMF(String tones) async {
971 0 : final senders = await pc!.getSenders();
972 0 : for (final sender in senders) {
973 0 : if (sender.track != null && sender.track!.kind == 'audio') {
974 0 : await sender.dtmfSender.insertDTMF(tones);
975 : return;
976 : } else {
977 0 : Logs().w('[VOIP] Unable to find a track to send DTMF on');
978 : }
979 : }
980 : }
981 :
982 2 : Future<void> terminate(
983 : CallParty party,
984 : CallErrorCode reason,
985 : bool shouldEmit,
986 : ) async {
987 4 : if (state == CallState.kConnected) {
988 0 : await hangup(
989 : reason: CallErrorCode.userHangup,
990 : shouldEmit: true,
991 : );
992 : return;
993 : }
994 :
995 4 : Logs().d('[VOIP] terminating call');
996 4 : _inviteTimer?.cancel();
997 2 : _inviteTimer = null;
998 :
999 4 : _ringingTimer?.cancel();
1000 2 : _ringingTimer = null;
1001 :
1002 : try {
1003 6 : await voip.delegate.stopRingtone();
1004 : } catch (e) {
1005 : // maybe rigntone never started (group calls) or has been stopped already
1006 0 : Logs().d('stopping ringtone failed ', e);
1007 : }
1008 :
1009 2 : hangupReason = reason;
1010 :
1011 : // don't see any reason to wrap this with shouldEmit atm,
1012 : // looks like a local state change only
1013 2 : setCallState(CallState.kEnded);
1014 :
1015 2 : if (!isGroupCall) {
1016 : // when a call crash and this call is already terminated the currentCId is null.
1017 : // So don't return bc the hangup or reject will not proceed anymore.
1018 4 : if (voip.currentCID != null &&
1019 14 : voip.currentCID != VoipId(roomId: room.id, callId: callId)) return;
1020 4 : voip.currentCID = null;
1021 12 : voip.incomingCallRoomId.removeWhere((key, value) => value == callId);
1022 : }
1023 :
1024 14 : voip.calls.removeWhere((key, value) => key.callId == callId);
1025 :
1026 2 : await cleanUp();
1027 : if (shouldEmit) {
1028 4 : onCallHangupNotifierForGroupCalls.add(this);
1029 6 : await voip.delegate.handleCallEnded(this);
1030 2 : fireCallEvent(CallStateChange.kHangup);
1031 2 : if ((party == CallParty.kRemote &&
1032 2 : _missedCall &&
1033 2 : reason != CallErrorCode.answeredElsewhere)) {
1034 0 : await voip.delegate.handleMissedCall(this);
1035 : }
1036 : }
1037 : }
1038 :
1039 0 : Future<void> onRejectReceived(CallErrorCode? reason) async {
1040 0 : Logs().v('[VOIP] Reject received for call ID $callId');
1041 : // No need to check party_id for reject because if we'd received either
1042 : // an answer or reject, we wouldn't be in state InviteSent
1043 0 : final shouldTerminate = (state == CallState.kFledgling &&
1044 0 : direction == CallDirection.kIncoming) ||
1045 0 : CallState.kInviteSent == state ||
1046 0 : CallState.kRinging == state;
1047 :
1048 : if (shouldTerminate) {
1049 0 : await terminate(
1050 : CallParty.kRemote,
1051 : reason ?? CallErrorCode.userHangup,
1052 : true,
1053 : );
1054 : } else {
1055 0 : Logs().e('[VOIP] Call is in state: ${state.toString()}: ignoring reject');
1056 : }
1057 : }
1058 :
1059 2 : Future<void> _gotLocalOffer(RTCSessionDescription offer) async {
1060 2 : if (callHasEnded) {
1061 0 : Logs().d(
1062 0 : 'Ignoring newly created offer on call ID ${opts.callId} because the call has ended',
1063 : );
1064 : return;
1065 : }
1066 :
1067 : try {
1068 4 : await pc!.setLocalDescription(offer);
1069 : } catch (err) {
1070 0 : Logs().d('Error setting local description! ${err.toString()}');
1071 0 : await terminate(
1072 : CallParty.kLocal,
1073 : CallErrorCode.setLocalDescription,
1074 : true,
1075 : );
1076 : return;
1077 : }
1078 :
1079 6 : if (pc!.iceGatheringState ==
1080 : RTCIceGatheringState.RTCIceGatheringStateGathering) {
1081 : // Allow a short time for initial candidates to be gathered
1082 0 : await Future.delayed(CallTimeouts.iceGatheringDelay);
1083 : }
1084 :
1085 2 : if (callHasEnded) return;
1086 :
1087 2 : final callCapabilities = CallCapabilities()
1088 2 : ..dtmf = false
1089 2 : ..transferee = false;
1090 2 : final metadata = _getLocalSDPStreamMetadata();
1091 4 : if (state == CallState.kCreateOffer) {
1092 2 : await sendInviteToCall(
1093 2 : room,
1094 2 : callId,
1095 2 : CallTimeouts.callInviteLifetime.inMilliseconds,
1096 2 : localPartyId,
1097 2 : offer.sdp!,
1098 : capabilities: callCapabilities,
1099 : metadata: metadata,
1100 : );
1101 : // just incase we ended the call but already sent the invite
1102 : // raraley happens during glares
1103 4 : if (state == CallState.kEnded) {
1104 0 : await hangup(reason: CallErrorCode.replaced);
1105 : return;
1106 : }
1107 2 : _inviteOrAnswerSent = true;
1108 :
1109 2 : if (!isGroupCall) {
1110 4 : Logs().d('[glare] set callid because new invite sent');
1111 12 : voip.incomingCallRoomId[room.id] = callId;
1112 : }
1113 :
1114 2 : setCallState(CallState.kInviteSent);
1115 :
1116 4 : _inviteTimer = Timer(CallTimeouts.callInviteLifetime, () {
1117 0 : if (state == CallState.kInviteSent) {
1118 0 : hangup(reason: CallErrorCode.inviteTimeout);
1119 : }
1120 0 : _inviteTimer?.cancel();
1121 0 : _inviteTimer = null;
1122 : });
1123 : } else {
1124 0 : await sendCallNegotiate(
1125 0 : room,
1126 0 : callId,
1127 0 : CallTimeouts.defaultCallEventLifetime.inMilliseconds,
1128 0 : localPartyId,
1129 0 : offer.sdp!,
1130 0 : type: offer.type!,
1131 : capabilities: callCapabilities,
1132 : metadata: metadata,
1133 : );
1134 : }
1135 : }
1136 :
1137 2 : Future<void> onNegotiationNeeded() async {
1138 4 : Logs().d('Negotiation is needed!');
1139 2 : _makingOffer = true;
1140 : try {
1141 : // The first addTrack(audio track) on iOS will trigger
1142 : // onNegotiationNeeded, which causes creatOffer to only include
1143 : // audio m-line, add delay and wait for video track to be added,
1144 : // then createOffer can get audio/video m-line correctly.
1145 2 : await Future.delayed(CallTimeouts.delayBeforeOffer);
1146 6 : final offer = await pc!.createOffer({});
1147 2 : await _gotLocalOffer(offer);
1148 : } catch (e) {
1149 0 : await _getLocalOfferFailed(e);
1150 : return;
1151 : } finally {
1152 2 : _makingOffer = false;
1153 : }
1154 : }
1155 :
1156 2 : Future<void> _preparePeerConnection() async {
1157 : int iceRestartedCount = 0;
1158 :
1159 : try {
1160 4 : pc = await _createPeerConnection();
1161 6 : pc!.onRenegotiationNeeded = onNegotiationNeeded;
1162 :
1163 4 : pc!.onIceCandidate = (RTCIceCandidate candidate) async {
1164 0 : if (callHasEnded) return;
1165 0 : _localCandidates.add(candidate);
1166 :
1167 0 : if (state == CallState.kRinging || !_inviteOrAnswerSent) return;
1168 :
1169 : // MSC2746 recommends these values (can be quite long when calling because the
1170 : // callee will need a while to answer the call)
1171 0 : final delay = direction == CallDirection.kIncoming ? 500 : 2000;
1172 0 : if (_candidateSendTries == 0) {
1173 0 : Timer(Duration(milliseconds: delay), () {
1174 0 : _sendCandidateQueue();
1175 : });
1176 : }
1177 : };
1178 :
1179 6 : pc!.onIceGatheringState = (RTCIceGatheringState state) async {
1180 8 : Logs().v('[VOIP] IceGatheringState => ${state.toString()}');
1181 2 : if (state == RTCIceGatheringState.RTCIceGatheringStateGathering) {
1182 0 : Timer(Duration(seconds: 3), () async {
1183 0 : if (!_iceGatheringFinished) {
1184 0 : _iceGatheringFinished = true;
1185 0 : await _sendCandidateQueue();
1186 : }
1187 : });
1188 : }
1189 2 : if (state == RTCIceGatheringState.RTCIceGatheringStateComplete) {
1190 2 : if (!_iceGatheringFinished) {
1191 2 : _iceGatheringFinished = true;
1192 2 : await _sendCandidateQueue();
1193 : }
1194 : }
1195 : };
1196 6 : pc!.onIceConnectionState = (RTCIceConnectionState state) async {
1197 8 : Logs().v('[VOIP] RTCIceConnectionState => ${state.toString()}');
1198 2 : if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) {
1199 4 : _localCandidates.clear();
1200 4 : _remoteCandidates.clear();
1201 : iceRestartedCount = 0;
1202 2 : setCallState(CallState.kConnected);
1203 : // fix any state/race issues we had with sdp packets and cloned streams
1204 2 : await updateMuteStatus();
1205 2 : _missedCall = false;
1206 : } else if ({
1207 2 : RTCIceConnectionState.RTCIceConnectionStateFailed,
1208 2 : RTCIceConnectionState.RTCIceConnectionStateDisconnected,
1209 2 : }.contains(state)) {
1210 0 : if (iceRestartedCount < 3) {
1211 0 : await restartIce();
1212 0 : iceRestartedCount++;
1213 : } else {
1214 0 : await hangup(reason: CallErrorCode.iceFailed);
1215 : }
1216 : }
1217 : };
1218 : } catch (e) {
1219 0 : Logs().v('[VOIP] prepareMediaStream error => ${e.toString()}');
1220 : }
1221 : }
1222 :
1223 0 : Future<void> onAnsweredElsewhere() async {
1224 0 : Logs().d('Call ID $callId answered elsewhere');
1225 0 : await terminate(CallParty.kRemote, CallErrorCode.answeredElsewhere, true);
1226 : }
1227 :
1228 2 : Future<void> cleanUp() async {
1229 : try {
1230 4 : for (final stream in _streams) {
1231 2 : await stream.dispose();
1232 : }
1233 4 : _streams.clear();
1234 : } catch (e, s) {
1235 0 : Logs().e('[VOIP] cleaning up streams failed', e, s);
1236 : }
1237 :
1238 : try {
1239 2 : if (pc != null) {
1240 4 : await pc!.close();
1241 4 : await pc!.dispose();
1242 : }
1243 : } catch (e, s) {
1244 0 : Logs().e('[VOIP] removing pc failed', e, s);
1245 : }
1246 : }
1247 :
1248 2 : Future<void> updateMuteStatus() async {
1249 2 : final micShouldBeMuted = (localUserMediaStream != null &&
1250 0 : localUserMediaStream!.isAudioMuted()) ||
1251 2 : _remoteOnHold;
1252 2 : final vidShouldBeMuted = (localUserMediaStream != null &&
1253 0 : localUserMediaStream!.isVideoMuted()) ||
1254 2 : _remoteOnHold;
1255 :
1256 2 : _setTracksEnabled(
1257 4 : localUserMediaStream?.stream?.getAudioTracks() ?? [],
1258 : !micShouldBeMuted,
1259 : );
1260 2 : _setTracksEnabled(
1261 4 : localUserMediaStream?.stream?.getVideoTracks() ?? [],
1262 : !vidShouldBeMuted,
1263 : );
1264 :
1265 2 : await sendSDPStreamMetadataChanged(
1266 2 : room,
1267 2 : callId,
1268 2 : localPartyId,
1269 2 : _getLocalSDPStreamMetadata(),
1270 : );
1271 : }
1272 :
1273 2 : void _setTracksEnabled(List<MediaStreamTrack> tracks, bool enabled) {
1274 2 : for (final track in tracks) {
1275 0 : track.enabled = enabled;
1276 : }
1277 : }
1278 :
1279 2 : SDPStreamMetadata _getLocalSDPStreamMetadata() {
1280 2 : final sdpStreamMetadatas = <String, SDPStreamPurpose>{};
1281 4 : for (final wpstream in getLocalStreams) {
1282 2 : if (wpstream.stream != null) {
1283 8 : sdpStreamMetadatas[wpstream.stream!.id] = SDPStreamPurpose(
1284 2 : purpose: wpstream.purpose,
1285 2 : audio_muted: wpstream.audioMuted,
1286 2 : video_muted: wpstream.videoMuted,
1287 : );
1288 : }
1289 : }
1290 2 : final metadata = SDPStreamMetadata(sdpStreamMetadatas);
1291 10 : Logs().v('Got local SDPStreamMetadata ${metadata.toJson().toString()}');
1292 : return metadata;
1293 : }
1294 :
1295 0 : Future<void> restartIce() async {
1296 0 : Logs().v('[VOIP] iceRestart.');
1297 : // Needs restart ice on session.pc and renegotiation.
1298 0 : _iceGatheringFinished = false;
1299 0 : _localCandidates.clear();
1300 0 : await pc!.restartIce();
1301 : }
1302 :
1303 2 : Future<MediaStream?> _getUserMedia(CallType type) async {
1304 2 : final mediaConstraints = {
1305 : 'audio': UserMediaConstraints.micMediaConstraints,
1306 2 : 'video': type == CallType.kVideo
1307 : ? UserMediaConstraints.camMediaConstraints
1308 : : false,
1309 : };
1310 : try {
1311 8 : return await voip.delegate.mediaDevices.getUserMedia(mediaConstraints);
1312 : } catch (e) {
1313 0 : await _getUserMediaFailed(e);
1314 : rethrow;
1315 : }
1316 : }
1317 :
1318 0 : Future<MediaStream?> _getDisplayMedia() async {
1319 : try {
1320 0 : return await voip.delegate.mediaDevices
1321 0 : .getDisplayMedia(UserMediaConstraints.screenMediaConstraints);
1322 : } catch (e) {
1323 0 : await _getUserMediaFailed(e);
1324 : }
1325 : return null;
1326 : }
1327 :
1328 2 : Future<RTCPeerConnection> _createPeerConnection() async {
1329 2 : final configuration = <String, dynamic>{
1330 4 : 'iceServers': opts.iceServers,
1331 : 'sdpSemantics': 'unified-plan',
1332 : };
1333 6 : final pc = await voip.delegate.createPeerConnection(configuration);
1334 2 : pc.onTrack = (RTCTrackEvent event) async {
1335 0 : for (final stream in event.streams) {
1336 0 : await _addRemoteStream(stream);
1337 0 : for (final track in stream.getTracks()) {
1338 0 : track.onEnded = () async {
1339 0 : if (stream.getTracks().isEmpty) {
1340 0 : Logs().d('[VOIP] detected a empty stream, removing it');
1341 0 : await _removeStream(stream);
1342 : }
1343 : };
1344 : }
1345 : }
1346 : };
1347 : return pc;
1348 : }
1349 :
1350 0 : Future<void> createDataChannel(
1351 : String label,
1352 : RTCDataChannelInit dataChannelDict,
1353 : ) async {
1354 0 : await pc?.createDataChannel(label, dataChannelDict);
1355 : }
1356 :
1357 0 : Future<void> tryRemoveStopedStreams() async {
1358 0 : final removedStreams = <String, WrappedMediaStream>{};
1359 0 : for (final stream in _streams) {
1360 0 : if (stream.stopped) {
1361 0 : removedStreams[stream.stream!.id] = stream;
1362 : }
1363 : }
1364 0 : _streams
1365 0 : .removeWhere((stream) => removedStreams.containsKey(stream.stream!.id));
1366 0 : for (final element in removedStreams.entries) {
1367 0 : await _removeStream(element.value.stream!);
1368 : }
1369 : }
1370 :
1371 0 : Future<void> _removeStream(MediaStream stream) async {
1372 0 : Logs().v('Removing feed with stream id ${stream.id}');
1373 :
1374 0 : final it = _streams.where((element) => element.stream!.id == stream.id);
1375 0 : if (it.isEmpty) {
1376 0 : Logs().v('Didn\'t find the feed with stream id ${stream.id} to delete');
1377 : return;
1378 : }
1379 0 : final wpstream = it.first;
1380 0 : _streams.removeWhere((element) => element.stream!.id == stream.id);
1381 0 : onStreamRemoved.add(wpstream);
1382 0 : fireCallEvent(CallStateChange.kFeedsChanged);
1383 0 : await wpstream.dispose();
1384 : }
1385 :
1386 2 : Future<void> _sendCandidateQueue() async {
1387 2 : if (callHasEnded) return;
1388 : /*
1389 : Currently, trickle-ice is not supported, so it will take a
1390 : long time to wait to collect all the canidates, set the
1391 : timeout for collection canidates to speed up the connection.
1392 : */
1393 2 : final candidatesQueue = _localCandidates;
1394 : try {
1395 2 : if (candidatesQueue.isNotEmpty) {
1396 0 : final candidates = <Map<String, dynamic>>[];
1397 0 : for (final element in candidatesQueue) {
1398 0 : candidates.add(element.toMap());
1399 : }
1400 0 : _localCandidates.clear();
1401 0 : final res = await sendCallCandidates(
1402 0 : opts.room,
1403 0 : callId,
1404 0 : localPartyId,
1405 : candidates,
1406 : );
1407 0 : Logs().v('[VOIP] sendCallCandidates res => $res');
1408 : }
1409 : } catch (e) {
1410 0 : Logs().v('[VOIP] sendCallCandidates e => ${e.toString()}');
1411 0 : _candidateSendTries++;
1412 0 : _localCandidates.clear();
1413 0 : _localCandidates.addAll(candidatesQueue);
1414 :
1415 0 : if (_candidateSendTries > 5) {
1416 0 : Logs().d(
1417 0 : 'Failed to send candidates on attempt $_candidateSendTries Giving up on this call.',
1418 : );
1419 0 : await hangup(reason: CallErrorCode.iceTimeout);
1420 : return;
1421 : }
1422 :
1423 0 : final delay = 500 * pow(2, _candidateSendTries);
1424 0 : Timer(Duration(milliseconds: delay as int), () {
1425 0 : _sendCandidateQueue();
1426 : });
1427 : }
1428 : }
1429 :
1430 2 : void fireCallEvent(CallStateChange event) {
1431 4 : onCallEventChanged.add(event);
1432 8 : Logs().i('CallStateChange: ${event.toString()}');
1433 : switch (event) {
1434 2 : case CallStateChange.kFeedsChanged:
1435 4 : onCallStreamsChanged.add(this);
1436 : break;
1437 2 : case CallStateChange.kState:
1438 10 : Logs().i('CallState: ${state.toString()}');
1439 : break;
1440 2 : case CallStateChange.kError:
1441 : break;
1442 2 : case CallStateChange.kHangup:
1443 : break;
1444 0 : case CallStateChange.kReplaced:
1445 : break;
1446 0 : case CallStateChange.kLocalHoldUnhold:
1447 : break;
1448 0 : case CallStateChange.kRemoteHoldUnhold:
1449 : break;
1450 0 : case CallStateChange.kAssertedIdentityChanged:
1451 : break;
1452 : }
1453 : }
1454 :
1455 0 : Future<void> _getLocalOfferFailed(dynamic err) async {
1456 0 : Logs().e('Failed to get local offer ${err.toString()}');
1457 0 : fireCallEvent(CallStateChange.kError);
1458 :
1459 0 : await terminate(CallParty.kLocal, CallErrorCode.localOfferFailed, true);
1460 : }
1461 :
1462 0 : Future<void> _getUserMediaFailed(dynamic err) async {
1463 0 : Logs().w('Failed to get user media - ending call ${err.toString()}');
1464 0 : fireCallEvent(CallStateChange.kError);
1465 0 : await terminate(CallParty.kLocal, CallErrorCode.userMediaFailed, true);
1466 : }
1467 :
1468 2 : Future<void> onSelectAnswerReceived(String? selectedPartyId) async {
1469 4 : if (direction != CallDirection.kIncoming) {
1470 0 : Logs().w('Got select_answer for an outbound call: ignoring');
1471 : return;
1472 : }
1473 : if (selectedPartyId == null) {
1474 0 : Logs().w(
1475 : 'Got nonsensical select_answer with null/undefined selected_party_id: ignoring',
1476 : );
1477 : return;
1478 : }
1479 :
1480 4 : if (selectedPartyId != localPartyId) {
1481 4 : Logs().w(
1482 4 : 'Got select_answer for party ID $selectedPartyId: we are party ID $localPartyId.',
1483 : );
1484 : // The other party has picked somebody else's answer
1485 2 : await terminate(CallParty.kRemote, CallErrorCode.answeredElsewhere, true);
1486 : }
1487 : }
1488 :
1489 : /// This is sent by the caller when they wish to establish a call.
1490 : /// [callId] is a unique identifier for the call.
1491 : /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
1492 : /// [lifetime] is the time in milliseconds that the invite is valid for. Once the invite age exceeds this value,
1493 : /// clients should discard it. They should also no longer show the call as awaiting an answer in the UI.
1494 : /// [type] The type of session description. Must be 'offer'.
1495 : /// [sdp] The SDP text of the session description.
1496 : /// [invitee] The user ID of the person who is being invited. Invites without an invitee field are defined to be
1497 : /// intended for any member of the room other than the sender of the event.
1498 : /// [party_id] The party ID for call, Can be set to client.deviceId.
1499 2 : Future<String?> sendInviteToCall(
1500 : Room room,
1501 : String callId,
1502 : int lifetime,
1503 : String party_id,
1504 : String sdp, {
1505 : String type = 'offer',
1506 : String version = voipProtoVersion,
1507 : String? txid,
1508 : CallCapabilities? capabilities,
1509 : SDPStreamMetadata? metadata,
1510 : }) async {
1511 2 : final content = {
1512 2 : 'call_id': callId,
1513 2 : 'party_id': party_id,
1514 2 : if (groupCallId != null) 'conf_id': groupCallId!,
1515 2 : 'version': version,
1516 2 : 'lifetime': lifetime,
1517 4 : 'offer': {'sdp': sdp, 'type': type},
1518 2 : if (remoteUserId != null)
1519 2 : 'invitee':
1520 2 : remoteUserId!, // TODO: rename this to invitee_user_id? breaks spec though
1521 2 : if (remoteDeviceId != null) 'invitee_device_id': remoteDeviceId!,
1522 2 : if (remoteDeviceId != null)
1523 0 : 'device_id': client
1524 0 : .deviceID!, // Having a remoteDeviceId means you are doing to-device events, so you want to send your deviceId too
1525 4 : if (capabilities != null) 'capabilities': capabilities.toJson(),
1526 4 : if (metadata != null) sdpStreamMetadataKey: metadata.toJson(),
1527 : };
1528 2 : return await _sendContent(
1529 : room,
1530 2 : isGroupCall ? EventTypes.GroupCallMemberInvite : EventTypes.CallInvite,
1531 : content,
1532 : txid: txid,
1533 : );
1534 : }
1535 :
1536 : /// The calling party sends the party_id of the first selected answer.
1537 : ///
1538 : /// Usually after receiving the first answer sdp in the client.onCallAnswer event,
1539 : /// save the `party_id`, and then send `CallSelectAnswer` to others peers that the call has been picked up.
1540 : ///
1541 : /// [callId] is a unique identifier for the call.
1542 : /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
1543 : /// [party_id] The party ID for call, Can be set to client.deviceId.
1544 : /// [selected_party_id] The party ID for the selected answer.
1545 2 : Future<String?> sendSelectCallAnswer(
1546 : Room room,
1547 : String callId,
1548 : String party_id,
1549 : String selected_party_id, {
1550 : String version = voipProtoVersion,
1551 : String? txid,
1552 : }) async {
1553 2 : final content = {
1554 2 : 'call_id': callId,
1555 2 : 'party_id': party_id,
1556 2 : if (groupCallId != null) 'conf_id': groupCallId!,
1557 2 : 'version': version,
1558 2 : 'selected_party_id': selected_party_id,
1559 : };
1560 :
1561 2 : return await _sendContent(
1562 : room,
1563 2 : isGroupCall
1564 : ? EventTypes.GroupCallMemberSelectAnswer
1565 : : EventTypes.CallSelectAnswer,
1566 : content,
1567 : txid: txid,
1568 : );
1569 : }
1570 :
1571 : /// Reject a call
1572 : /// [callId] is a unique identifier for the call.
1573 : /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
1574 : /// [party_id] The party ID for call, Can be set to client.deviceId.
1575 2 : Future<String?> sendCallReject(
1576 : Room room,
1577 : String callId,
1578 : String party_id, {
1579 : String version = voipProtoVersion,
1580 : String? txid,
1581 : }) async {
1582 2 : final content = {
1583 2 : 'call_id': callId,
1584 2 : 'party_id': party_id,
1585 2 : if (groupCallId != null) 'conf_id': groupCallId!,
1586 2 : 'version': version,
1587 : };
1588 :
1589 2 : return await _sendContent(
1590 : room,
1591 2 : isGroupCall ? EventTypes.GroupCallMemberReject : EventTypes.CallReject,
1592 : content,
1593 : txid: txid,
1594 : );
1595 : }
1596 :
1597 : /// When local audio/video tracks are added/deleted or hold/unhold,
1598 : /// need to createOffer and renegotiation.
1599 : /// [callId] is a unique identifier for the call.
1600 : /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
1601 : /// [party_id] The party ID for call, Can be set to client.deviceId.
1602 2 : Future<String?> sendCallNegotiate(
1603 : Room room,
1604 : String callId,
1605 : int lifetime,
1606 : String party_id,
1607 : String sdp, {
1608 : String type = 'offer',
1609 : String version = voipProtoVersion,
1610 : String? txid,
1611 : CallCapabilities? capabilities,
1612 : SDPStreamMetadata? metadata,
1613 : }) async {
1614 2 : final content = {
1615 2 : 'call_id': callId,
1616 2 : 'party_id': party_id,
1617 2 : if (groupCallId != null) 'conf_id': groupCallId!,
1618 2 : 'version': version,
1619 2 : 'lifetime': lifetime,
1620 4 : 'description': {'sdp': sdp, 'type': type},
1621 0 : if (capabilities != null) 'capabilities': capabilities.toJson(),
1622 0 : if (metadata != null) sdpStreamMetadataKey: metadata.toJson(),
1623 : };
1624 2 : return await _sendContent(
1625 : room,
1626 2 : isGroupCall
1627 : ? EventTypes.GroupCallMemberNegotiate
1628 : : EventTypes.CallNegotiate,
1629 : content,
1630 : txid: txid,
1631 : );
1632 : }
1633 :
1634 : /// This is sent by callers after sending an invite and by the callee after answering.
1635 : /// Its purpose is to give the other party additional ICE candidates to try using to communicate.
1636 : ///
1637 : /// [callId] The ID of the call this event relates to.
1638 : ///
1639 : /// [version] The version of the VoIP specification this messages adheres to. This specification is version 1.
1640 : ///
1641 : /// [party_id] The party ID for call, Can be set to client.deviceId.
1642 : ///
1643 : /// [candidates] Array of objects describing the candidates. Example:
1644 : ///
1645 : /// ```
1646 : /// [
1647 : /// {
1648 : /// "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0",
1649 : /// "sdpMLineIndex": 0,
1650 : /// "sdpMid": "audio"
1651 : /// }
1652 : /// ],
1653 : /// ```
1654 2 : Future<String?> sendCallCandidates(
1655 : Room room,
1656 : String callId,
1657 : String party_id,
1658 : List<Map<String, dynamic>> candidates, {
1659 : String version = voipProtoVersion,
1660 : String? txid,
1661 : }) async {
1662 2 : final content = {
1663 2 : 'call_id': callId,
1664 2 : 'party_id': party_id,
1665 2 : if (groupCallId != null) 'conf_id': groupCallId!,
1666 2 : 'version': version,
1667 2 : 'candidates': candidates,
1668 : };
1669 2 : return await _sendContent(
1670 : room,
1671 2 : isGroupCall
1672 : ? EventTypes.GroupCallMemberCandidates
1673 : : EventTypes.CallCandidates,
1674 : content,
1675 : txid: txid,
1676 : );
1677 : }
1678 :
1679 : /// This event is sent by the callee when they wish to answer the call.
1680 : /// [callId] is a unique identifier for the call.
1681 : /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
1682 : /// [type] The type of session description. Must be 'answer'.
1683 : /// [sdp] The SDP text of the session description.
1684 : /// [party_id] The party ID for call, Can be set to client.deviceId.
1685 2 : Future<String?> sendAnswerCall(
1686 : Room room,
1687 : String callId,
1688 : String sdp,
1689 : String party_id, {
1690 : String type = 'answer',
1691 : String version = voipProtoVersion,
1692 : String? txid,
1693 : CallCapabilities? capabilities,
1694 : SDPStreamMetadata? metadata,
1695 : }) async {
1696 2 : final content = {
1697 2 : 'call_id': callId,
1698 2 : 'party_id': party_id,
1699 2 : if (groupCallId != null) 'conf_id': groupCallId!,
1700 2 : 'version': version,
1701 4 : 'answer': {'sdp': sdp, 'type': type},
1702 4 : if (capabilities != null) 'capabilities': capabilities.toJson(),
1703 4 : if (metadata != null) sdpStreamMetadataKey: metadata.toJson(),
1704 : };
1705 2 : return await _sendContent(
1706 : room,
1707 2 : isGroupCall ? EventTypes.GroupCallMemberAnswer : EventTypes.CallAnswer,
1708 : content,
1709 : txid: txid,
1710 : );
1711 : }
1712 :
1713 : /// This event is sent by the callee when they wish to answer the call.
1714 : /// [callId] The ID of the call this event relates to.
1715 : /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
1716 : /// [party_id] The party ID for call, Can be set to client.deviceId.
1717 2 : Future<String?> sendHangupCall(
1718 : Room room,
1719 : String callId,
1720 : String party_id,
1721 : String? hangupCause, {
1722 : String version = voipProtoVersion,
1723 : String? txid,
1724 : }) async {
1725 2 : final content = {
1726 2 : 'call_id': callId,
1727 2 : 'party_id': party_id,
1728 2 : if (groupCallId != null) 'conf_id': groupCallId!,
1729 2 : 'version': version,
1730 2 : if (hangupCause != null) 'reason': hangupCause,
1731 : };
1732 2 : return await _sendContent(
1733 : room,
1734 2 : isGroupCall ? EventTypes.GroupCallMemberHangup : EventTypes.CallHangup,
1735 : content,
1736 : txid: txid,
1737 : );
1738 : }
1739 :
1740 : /// Send SdpStreamMetadata Changed event.
1741 : ///
1742 : /// This MSC also adds a new call event m.call.sdp_stream_metadata_changed,
1743 : /// which has the common VoIP fields as specified in
1744 : /// MSC2746 (version, call_id, party_id) and a sdp_stream_metadata object which
1745 : /// is the same thing as sdp_stream_metadata in m.call.negotiate, m.call.invite
1746 : /// and m.call.answer. The client sends this event the when sdp_stream_metadata
1747 : /// has changed but no negotiation is required
1748 : /// (e.g. the user mutes their camera/microphone).
1749 : ///
1750 : /// [callId] The ID of the call this event relates to.
1751 : /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
1752 : /// [party_id] The party ID for call, Can be set to client.deviceId.
1753 : /// [metadata] The sdp_stream_metadata object.
1754 2 : Future<String?> sendSDPStreamMetadataChanged(
1755 : Room room,
1756 : String callId,
1757 : String party_id,
1758 : SDPStreamMetadata metadata, {
1759 : String version = voipProtoVersion,
1760 : String? txid,
1761 : }) async {
1762 2 : final content = {
1763 2 : 'call_id': callId,
1764 2 : 'party_id': party_id,
1765 2 : if (groupCallId != null) 'conf_id': groupCallId!,
1766 2 : 'version': version,
1767 4 : sdpStreamMetadataKey: metadata.toJson(),
1768 : };
1769 2 : return await _sendContent(
1770 : room,
1771 2 : isGroupCall
1772 : ? EventTypes.GroupCallMemberSDPStreamMetadataChanged
1773 : : EventTypes.CallSDPStreamMetadataChanged,
1774 : content,
1775 : txid: txid,
1776 : );
1777 : }
1778 :
1779 : /// CallReplacesEvent for Transfered calls
1780 : ///
1781 : /// [callId] The ID of the call this event relates to.
1782 : /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
1783 : /// [party_id] The party ID for call, Can be set to client.deviceId.
1784 : /// [callReplaces] transfer info
1785 2 : Future<String?> sendCallReplaces(
1786 : Room room,
1787 : String callId,
1788 : String party_id,
1789 : CallReplaces callReplaces, {
1790 : String version = voipProtoVersion,
1791 : String? txid,
1792 : }) async {
1793 2 : final content = {
1794 2 : 'call_id': callId,
1795 2 : 'party_id': party_id,
1796 2 : if (groupCallId != null) 'conf_id': groupCallId!,
1797 2 : 'version': version,
1798 2 : ...callReplaces.toJson(),
1799 : };
1800 2 : return await _sendContent(
1801 : room,
1802 2 : isGroupCall
1803 : ? EventTypes.GroupCallMemberReplaces
1804 : : EventTypes.CallReplaces,
1805 : content,
1806 : txid: txid,
1807 : );
1808 : }
1809 :
1810 : /// send AssertedIdentity event
1811 : ///
1812 : /// [callId] The ID of the call this event relates to.
1813 : /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
1814 : /// [party_id] The party ID for call, Can be set to client.deviceId.
1815 : /// [assertedIdentity] the asserted identity
1816 2 : Future<String?> sendAssertedIdentity(
1817 : Room room,
1818 : String callId,
1819 : String party_id,
1820 : AssertedIdentity assertedIdentity, {
1821 : String version = voipProtoVersion,
1822 : String? txid,
1823 : }) async {
1824 2 : final content = {
1825 2 : 'call_id': callId,
1826 2 : 'party_id': party_id,
1827 2 : if (groupCallId != null) 'conf_id': groupCallId!,
1828 2 : 'version': version,
1829 4 : 'asserted_identity': assertedIdentity.toJson(),
1830 : };
1831 2 : return await _sendContent(
1832 : room,
1833 2 : isGroupCall
1834 : ? EventTypes.GroupCallMemberAssertedIdentity
1835 : : EventTypes.CallAssertedIdentity,
1836 : content,
1837 : txid: txid,
1838 : );
1839 : }
1840 :
1841 2 : Future<String?> _sendContent(
1842 : Room room,
1843 : String type,
1844 : Map<String, Object> content, {
1845 : String? txid,
1846 : }) async {
1847 6 : Logs().d('[VOIP] sending content type $type, with conf: $content');
1848 0 : txid ??= VoIP.customTxid ?? client.generateUniqueTransactionId();
1849 2 : final mustEncrypt = room.encrypted && client.encryptionEnabled;
1850 :
1851 : // opponentDeviceId is only set for a few events during group calls,
1852 : // therefore only group calls use to-device messages for call events
1853 2 : if (isGroupCall && remoteDeviceId != null) {
1854 0 : final toDeviceSeq = _toDeviceSeq++;
1855 0 : final Map<String, Object> data = {
1856 : ...content,
1857 0 : 'seq': toDeviceSeq,
1858 0 : if (remoteSessionId != null) 'dest_session_id': remoteSessionId!,
1859 0 : 'sender_session_id': voip.currentSessionId,
1860 0 : 'room_id': room.id,
1861 : };
1862 :
1863 : if (mustEncrypt) {
1864 0 : await client.userDeviceKeysLoading;
1865 0 : if (client.userDeviceKeys[remoteUserId]?.deviceKeys[remoteDeviceId] !=
1866 : null) {
1867 0 : await client.sendToDeviceEncrypted(
1868 0 : [
1869 0 : client.userDeviceKeys[remoteUserId]!.deviceKeys[remoteDeviceId]!,
1870 : ],
1871 : type,
1872 : data,
1873 : );
1874 : } else {
1875 0 : Logs().w(
1876 0 : '[VOIP] _sendCallContent missing device keys for $remoteUserId',
1877 : );
1878 : }
1879 : } else {
1880 0 : await client.sendToDevice(
1881 : type,
1882 : txid,
1883 0 : {
1884 0 : remoteUserId!: {remoteDeviceId!: data},
1885 : },
1886 : );
1887 : }
1888 : return '';
1889 : } else {
1890 : final sendMessageContent = mustEncrypt
1891 0 : ? await client.encryption!
1892 0 : .encryptGroupMessagePayload(room.id, content, type: type)
1893 : : content;
1894 4 : return await client.sendMessage(
1895 2 : room.id,
1896 2 : sendMessageContent.containsKey('ciphertext')
1897 : ? EventTypes.Encrypted
1898 : : type,
1899 : txid,
1900 : sendMessageContent,
1901 : );
1902 : }
1903 : }
1904 : }
|