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