Line data Source code
1 : import 'dart:async';
2 : import 'dart:convert';
3 : import 'dart:core';
4 :
5 : import 'package:collection/collection.dart';
6 : import 'package:sdp_transform/sdp_transform.dart' as sdp_transform;
7 : import 'package:webrtc_interface/webrtc_interface.dart';
8 :
9 : import 'package:matrix/matrix.dart';
10 : import 'package:matrix/src/utils/cached_stream_controller.dart';
11 : import 'package:matrix/src/utils/crypto/crypto.dart';
12 : import 'package:matrix/src/voip/models/call_membership.dart';
13 : import 'package:matrix/src/voip/models/call_options.dart';
14 : import 'package:matrix/src/voip/models/voip_id.dart';
15 : import 'package:matrix/src/voip/utils/stream_helper.dart';
16 :
17 : /// The parent highlevel voip class, this trnslates matrix events to webrtc methods via
18 : /// `CallSession` or `GroupCallSession` methods
19 : class VoIP {
20 : // used only for internal tests, all txids for call events will be overwritten to this
21 : static String? customTxid;
22 :
23 : /// set to true if you want to use the ratcheting mechanism with your keyprovider
24 : /// remember to set the window size correctly on your keyprovider
25 : ///
26 : /// at client level because reinitializing a `GroupCallSession` and its `KeyProvider`
27 : /// everytime this changed would be a pain
28 : final bool enableSFUE2EEKeyRatcheting;
29 :
30 : /// cached turn creds
31 : TurnServerCredentials? _turnServerCredentials;
32 :
33 4 : Map<VoipId, CallSession> get calls => _calls;
34 : final Map<VoipId, CallSession> _calls = {};
35 :
36 4 : Map<VoipId, GroupCallSession> get groupCalls => _groupCalls;
37 : final Map<VoipId, GroupCallSession> _groupCalls = {};
38 :
39 : final CachedStreamController<CallSession> onIncomingCall =
40 : CachedStreamController();
41 :
42 : VoipId? currentCID;
43 : VoipId? currentGroupCID;
44 :
45 4 : String get localPartyId => currentSessionId;
46 :
47 : final Client client;
48 : final WebRTCDelegate delegate;
49 : final StreamController<GroupCallSession> onIncomingGroupCall =
50 : StreamController();
51 :
52 6 : CallParticipant? get localParticipant => client.isLogged()
53 2 : ? CallParticipant(
54 : this,
55 4 : userId: client.userID!,
56 4 : deviceId: client.deviceID,
57 : )
58 : : null;
59 :
60 : /// map of roomIds to the invites they are currently processing or in a call with
61 : /// used for handling glare in p2p calls
62 4 : Map<String, String> get incomingCallRoomId => _incomingCallRoomId;
63 : final Map<String, String> _incomingCallRoomId = {};
64 :
65 : /// the current instance of voip, changing this will drop any ongoing mesh calls
66 : /// with that sessionId
67 : late String currentSessionId;
68 2 : VoIP(
69 : this.client,
70 : this.delegate, {
71 : this.enableSFUE2EEKeyRatcheting = false,
72 2 : }) : super() {
73 6 : currentSessionId = base64Encode(secureRandomBytes(16));
74 8 : Logs().v('set currentSessionId to $currentSessionId');
75 : // to populate groupCalls with already present calls
76 6 : for (final room in client.rooms) {
77 2 : final memsList = room.getCallMembershipsFromRoom();
78 2 : for (final mems in memsList.values) {
79 0 : for (final mem in mems) {
80 0 : unawaited(createGroupCallFromRoomStateEvent(mem));
81 : }
82 : }
83 : }
84 :
85 : /// handles events todevice and matrix events for invite, candidates, hangup, etc.
86 10 : client.onCallEvents.stream.listen((events) async {
87 2 : await _handleCallEvents(events);
88 : });
89 :
90 : // handles the com.famedly.call events.
91 8 : client.onRoomState.stream.listen(
92 2 : (update) async {
93 : final event = update.state;
94 2 : if (event is! Event) return;
95 6 : if (event.room.membership != Membership.join) return;
96 4 : if (event.type != EventTypes.GroupCallMember) return;
97 :
98 8 : Logs().v('[VOIP] onRoomState: type ${event.toJson()}');
99 4 : final mems = event.room.getCallMembershipsFromEvent(event);
100 4 : for (final mem in mems) {
101 4 : unawaited(createGroupCallFromRoomStateEvent(mem));
102 : }
103 6 : for (final map in groupCalls.entries) {
104 10 : if (map.key.roomId == event.room.id) {
105 : // because we don't know which call got updated, just update all
106 : // group calls we have entered for that room
107 4 : await map.value.onMemberStateChanged();
108 : }
109 : }
110 : },
111 : );
112 :
113 8 : delegate.mediaDevices.ondevicechange = _onDeviceChange;
114 : }
115 :
116 2 : Future<void> _handleCallEvents(List<BasicEventWithSender> callEvents) async {
117 : // Call invites should be omitted for a call that is already answered,
118 : // has ended, is rejectd or replaced.
119 2 : final callEventsCopy = List<BasicEventWithSender>.from(callEvents);
120 4 : for (final callEvent in callEventsCopy) {
121 4 : final callId = callEvent.content.tryGet<String>('call_id');
122 :
123 4 : if (CallConstants.callEndedEventTypes.contains(callEvent.type)) {
124 0 : callEvents.removeWhere((event) {
125 0 : if (CallConstants.omitWhenCallEndedTypes.contains(event.type) &&
126 0 : event.content.tryGet<String>('call_id') == callId) {
127 0 : Logs().v(
128 0 : 'Ommit "${event.type}" event for an already terminated call',
129 : );
130 : return true;
131 : }
132 :
133 : return false;
134 : });
135 : }
136 :
137 : // checks for ended events and removes invites for that call id.
138 2 : if (callEvent is Event) {
139 : // removes expired invites
140 4 : final age = callEvent.unsigned?.tryGet<int>('age') ??
141 6 : (DateTime.now().millisecondsSinceEpoch -
142 4 : callEvent.originServerTs.millisecondsSinceEpoch);
143 :
144 4 : callEvents.removeWhere((element) {
145 4 : if (callEvent.type == EventTypes.CallInvite &&
146 2 : age >
147 4 : (callEvent.content.tryGet<int>('lifetime') ??
148 0 : CallTimeouts.callInviteLifetime.inMilliseconds)) {
149 4 : Logs().w(
150 4 : '[VOIP] Ommiting invite event ${callEvent.eventId} as age was older than lifetime',
151 : );
152 : return true;
153 : }
154 : return false;
155 : });
156 : }
157 : }
158 :
159 : // and finally call the respective methods on the clean callEvents list
160 4 : for (final callEvent in callEvents) {
161 2 : await _handleCallEvent(callEvent);
162 : }
163 : }
164 :
165 2 : Future<void> _handleCallEvent(BasicEventWithSender event) async {
166 : // member event updates handled in onRoomState for ease
167 4 : if (event.type == EventTypes.GroupCallMember) return;
168 :
169 : GroupCallSession? groupCallSession;
170 : Room? room;
171 2 : final remoteUserId = event.senderId;
172 : String? remoteDeviceId;
173 :
174 2 : if (event is Event) {
175 2 : room = event.room;
176 :
177 : /// this can also be sent in p2p calls when they want to call a specific device
178 4 : remoteDeviceId = event.content.tryGet<String>('invitee_device_id');
179 0 : } else if (event is ToDeviceEvent) {
180 0 : final roomId = event.content.tryGet<String>('room_id');
181 0 : final confId = event.content.tryGet<String>('conf_id');
182 :
183 : /// to-device events specifically, m.call.invite and encryption key sending and requesting
184 0 : remoteDeviceId = event.content.tryGet<String>('device_id');
185 :
186 : if (roomId != null && confId != null) {
187 0 : room = client.getRoomById(roomId);
188 0 : groupCallSession = groupCalls[VoipId(roomId: roomId, callId: confId)];
189 : } else {
190 0 : Logs().w(
191 0 : '[VOIP] Ignoring to_device event of type ${event.type} but did not find group call for id: $confId',
192 : );
193 : return;
194 : }
195 :
196 0 : if (!event.type.startsWith(EventTypes.GroupCallMemberEncryptionKeys)) {
197 : // livekit calls have their own session deduplication logic so ignore sessionId deduplication for them
198 0 : final destSessionId = event.content.tryGet<String>('dest_session_id');
199 0 : if (destSessionId != currentSessionId) {
200 0 : Logs().w(
201 0 : '[VOIP] Ignoring to_device event of type ${event.type} did not match currentSessionId: $currentSessionId, dest_session_id was set to $destSessionId',
202 : );
203 : return;
204 : }
205 : } else if (groupCallSession == null || remoteDeviceId == null) {
206 0 : Logs().w(
207 0 : '[VOIP] _handleCallEvent ${event.type} recieved but either groupCall ${groupCallSession?.groupCallId} or deviceId $remoteDeviceId was null, ignoring',
208 : );
209 : return;
210 : }
211 : } else {
212 0 : Logs().w(
213 0 : '[VOIP] _handleCallEvent can only handle Event or ToDeviceEvent, it got ${event.runtimeType}',
214 : );
215 : return;
216 : }
217 :
218 2 : final content = event.content;
219 :
220 : if (room == null) {
221 0 : Logs().w(
222 : '[VOIP] _handleCallEvent call event does not contain a room_id, ignoring',
223 : );
224 : return;
225 4 : } else if (client.userID != null &&
226 4 : client.deviceID != null &&
227 6 : remoteUserId == client.userID &&
228 0 : remoteDeviceId == client.deviceID) {
229 0 : Logs().v(
230 0 : 'Ignoring call event ${event.type} for room ${room.id} from our own device',
231 : );
232 : return;
233 2 : } else if (!event.type
234 2 : .startsWith(EventTypes.GroupCallMemberEncryptionKeys)) {
235 : // skip webrtc event checks on encryption_keys
236 2 : final callId = content['call_id'] as String?;
237 2 : final partyId = content['party_id'] as String?;
238 0 : if (callId == null && event.type.startsWith('m.call')) {
239 0 : Logs().w('Ignoring call event ${event.type} because call_id was null');
240 : return;
241 : }
242 : if (callId != null) {
243 8 : final call = calls[VoipId(roomId: room.id, callId: callId)];
244 : if (call == null &&
245 4 : !{EventTypes.CallInvite, EventTypes.GroupCallMemberInvite}
246 4 : .contains(event.type)) {
247 0 : Logs().w(
248 0 : 'Ignoring call event ${event.type} for room ${room.id} because we do not have the call',
249 : );
250 : return;
251 : } else if (call != null) {
252 : // multiple checks to make sure the events sent are from the the
253 : // expected party
254 8 : if (call.room.id != room.id) {
255 0 : Logs().w(
256 0 : 'Ignoring call event ${event.type} for room ${room.id} claiming to be for call in room ${call.room.id}',
257 : );
258 : return;
259 : }
260 6 : if (call.remoteUserId != null && call.remoteUserId != remoteUserId) {
261 0 : Logs().d(
262 0 : 'Ignoring call event ${event.type} for room ${room.id} from sender $remoteUserId, expected sender: ${call.remoteUserId}',
263 : );
264 : return;
265 : }
266 6 : if (call.remotePartyId != null && call.remotePartyId != partyId) {
267 0 : Logs().w(
268 0 : 'Ignoring call event ${event.type} for room ${room.id} from sender with a different party_id $partyId, expected party_id: ${call.remotePartyId}',
269 : );
270 : return;
271 : }
272 2 : if ((call.remotePartyId != null &&
273 6 : call.remotePartyId == localPartyId)) {
274 0 : Logs().v(
275 0 : 'Ignoring call event ${event.type} for room ${room.id} from our own partyId',
276 : );
277 : return;
278 : }
279 : }
280 : }
281 : }
282 4 : Logs().v(
283 8 : '[VOIP] Handling event of type: ${event.type}, content ${event.content} from sender ${event.senderId} rp: $remoteUserId:$remoteDeviceId',
284 : );
285 :
286 2 : switch (event.type) {
287 2 : case EventTypes.CallInvite:
288 2 : case EventTypes.GroupCallMemberInvite:
289 2 : await onCallInvite(room, remoteUserId, remoteDeviceId, content);
290 : break;
291 2 : case EventTypes.CallAnswer:
292 2 : case EventTypes.GroupCallMemberAnswer:
293 0 : await onCallAnswer(room, remoteUserId, remoteDeviceId, content);
294 : break;
295 2 : case EventTypes.CallCandidates:
296 2 : case EventTypes.GroupCallMemberCandidates:
297 2 : await onCallCandidates(room, content);
298 : break;
299 2 : case EventTypes.CallHangup:
300 2 : case EventTypes.GroupCallMemberHangup:
301 0 : await onCallHangup(room, content);
302 : break;
303 2 : case EventTypes.CallReject:
304 2 : case EventTypes.GroupCallMemberReject:
305 0 : await onCallReject(room, content);
306 : break;
307 2 : case EventTypes.CallNegotiate:
308 2 : case EventTypes.GroupCallMemberNegotiate:
309 0 : await onCallNegotiate(room, content);
310 : break;
311 : // case EventTypes.CallReplaces:
312 : // await onCallReplaces(room, content);
313 : // break;
314 2 : case EventTypes.CallSelectAnswer:
315 0 : case EventTypes.GroupCallMemberSelectAnswer:
316 2 : await onCallSelectAnswer(room, content);
317 : break;
318 0 : case EventTypes.CallSDPStreamMetadataChanged:
319 0 : case EventTypes.CallSDPStreamMetadataChangedPrefix:
320 0 : case EventTypes.GroupCallMemberSDPStreamMetadataChanged:
321 0 : await onSDPStreamMetadataChangedReceived(room, content);
322 : break;
323 0 : case EventTypes.CallAssertedIdentity:
324 0 : case EventTypes.CallAssertedIdentityPrefix:
325 0 : case EventTypes.GroupCallMemberAssertedIdentity:
326 0 : await onAssertedIdentityReceived(room, content);
327 : break;
328 0 : case EventTypes.GroupCallMemberEncryptionKeys:
329 0 : await groupCallSession!.backend.onCallEncryption(
330 : groupCallSession,
331 : remoteUserId,
332 : remoteDeviceId!,
333 : content,
334 : );
335 : break;
336 0 : case EventTypes.GroupCallMemberEncryptionKeysRequest:
337 0 : await groupCallSession!.backend.onCallEncryptionKeyRequest(
338 : groupCallSession,
339 : remoteUserId,
340 : remoteDeviceId!,
341 : content,
342 : );
343 : break;
344 : }
345 : }
346 :
347 0 : Future<void> _onDeviceChange(dynamic _) async {
348 0 : Logs().v('[VOIP] _onDeviceChange');
349 0 : for (final call in calls.values) {
350 0 : if (call.state == CallState.kConnected && !call.isGroupCall) {
351 0 : await call.updateMediaDeviceForCall();
352 : }
353 : }
354 0 : for (final groupCall in groupCalls.values) {
355 0 : if (groupCall.state == GroupCallState.entered) {
356 0 : await groupCall.backend.updateMediaDeviceForCalls();
357 : }
358 : }
359 : }
360 :
361 2 : Future<void> onCallInvite(
362 : Room room,
363 : String remoteUserId,
364 : String? remoteDeviceId,
365 : Map<String, dynamic> content,
366 : ) async {
367 4 : Logs().v(
368 12 : '[VOIP] onCallInvite $remoteUserId:$remoteDeviceId => ${client.userID}:${client.deviceID}, \ncontent => ${content.toString()}',
369 : );
370 :
371 2 : final String callId = content['call_id'];
372 2 : final int lifetime = content['lifetime'];
373 2 : final String? confId = content['conf_id'];
374 :
375 8 : final call = calls[VoipId(roomId: room.id, callId: callId)];
376 :
377 4 : Logs().d(
378 10 : '[glare] got new call ${content.tryGet('call_id')} and currently room id is mapped to ${incomingCallRoomId.tryGet(room.id)}',
379 : );
380 :
381 0 : if (call != null && call.state == CallState.kEnded) {
382 : // Session already exist.
383 0 : Logs().v('[VOIP] onCallInvite: Session [$callId] already exist.');
384 : return;
385 : }
386 :
387 2 : final inviteeUserId = content['invitee'];
388 0 : if (inviteeUserId != null && inviteeUserId != localParticipant?.userId) {
389 0 : Logs().w('[VOIP] Ignoring call, meant for user $inviteeUserId');
390 : return; // This invite was meant for another user in the room
391 : }
392 2 : final inviteeDeviceId = content['invitee_device_id'];
393 : if (inviteeDeviceId != null &&
394 0 : inviteeDeviceId != localParticipant?.deviceId) {
395 0 : Logs().w('[VOIP] Ignoring call, meant for device $inviteeDeviceId');
396 : return; // This invite was meant for another device in the room
397 : }
398 :
399 2 : if (content['capabilities'] != null) {
400 0 : final capabilities = CallCapabilities.fromJson(content['capabilities']);
401 0 : Logs().v(
402 0 : '[VOIP] CallCapabilities: dtmf => ${capabilities.dtmf}, transferee => ${capabilities.transferee}',
403 : );
404 : }
405 :
406 : var callType = CallType.kVoice;
407 : SDPStreamMetadata? sdpStreamMetadata;
408 2 : if (content[sdpStreamMetadataKey] != null) {
409 : sdpStreamMetadata =
410 0 : SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]);
411 0 : sdpStreamMetadata.sdpStreamMetadatas
412 0 : .forEach((streamId, SDPStreamPurpose purpose) {
413 0 : Logs().v(
414 0 : '[VOIP] [$streamId] => purpose: ${purpose.purpose}, audioMuted: ${purpose.audio_muted}, videoMuted: ${purpose.video_muted}',
415 : );
416 :
417 0 : if (!purpose.video_muted) {
418 : callType = CallType.kVideo;
419 : }
420 : });
421 : } else {
422 6 : callType = getCallType(content['offer']['sdp']);
423 : }
424 :
425 2 : final opts = CallOptions(
426 : voip: this,
427 : callId: callId,
428 : groupCallId: confId,
429 : dir: CallDirection.kIncoming,
430 : type: callType,
431 : room: room,
432 2 : localPartyId: localPartyId,
433 2 : iceServers: await getIceServers(),
434 : );
435 :
436 2 : final newCall = createNewCall(opts);
437 :
438 : /// both invitee userId and deviceId are set here because there can be
439 : /// multiple devices from same user in a call, so we specifiy who the
440 : /// invite is for
441 2 : newCall.remoteUserId = remoteUserId;
442 2 : newCall.remoteDeviceId = remoteDeviceId;
443 4 : newCall.remotePartyId = content['party_id'];
444 4 : newCall.remoteSessionId = content['sender_session_id'];
445 :
446 : // newCall.remoteSessionId = remoteParticipant.sessionId;
447 :
448 4 : if (!delegate.canHandleNewCall &&
449 : (confId == null ||
450 0 : currentGroupCID != VoipId(roomId: room.id, callId: confId))) {
451 0 : Logs().v(
452 : '[VOIP] onCallInvite: Unable to handle new calls, maybe user is busy.',
453 : );
454 : // no need to emit here because handleNewCall was never triggered yet
455 0 : await newCall.reject(reason: CallErrorCode.userBusy, shouldEmit: false);
456 0 : await delegate.handleMissedCall(newCall);
457 : return;
458 : }
459 :
460 2 : final offer = RTCSessionDescription(
461 4 : content['offer']['sdp'],
462 4 : content['offer']['type'],
463 : );
464 :
465 : /// play ringtone. We decided to play the ringtone before adding the call to
466 : /// the incoming call stream because getUserMedia from initWithInvite fails
467 : /// on firefox unless the tab is in focus. We should atleast be able to notify
468 : /// the user about an incoming call
469 : ///
470 : /// Autoplay on firefox still needs interaction, without which all notifications
471 : /// could be blocked.
472 : if (confId == null) {
473 4 : await delegate.playRingtone();
474 : }
475 :
476 : // When getUserMedia throws an exception, we handle it by terminating the call,
477 : // and all this happens inside initWithInvite. If we set currentCID after
478 : // initWithInvite, we might set it to callId even after it was reset to null
479 : // by terminate.
480 6 : currentCID = VoipId(roomId: room.id, callId: callId);
481 :
482 2 : await newCall.initWithInvite(
483 : callType,
484 : offer,
485 : sdpStreamMetadata,
486 : lifetime,
487 : confId != null,
488 : );
489 :
490 : // Popup CallingPage for incoming call.
491 2 : if (confId == null && !newCall.callHasEnded) {
492 4 : await delegate.handleNewCall(newCall);
493 : }
494 :
495 : if (confId != null) {
496 : // the stream is used to monitor incoming peer calls in a mesh call
497 0 : onIncomingCall.add(newCall);
498 : }
499 : }
500 :
501 0 : Future<void> onCallAnswer(
502 : Room room,
503 : String remoteUserId,
504 : String? remoteDeviceId,
505 : Map<String, dynamic> content,
506 : ) async {
507 0 : Logs().v('[VOIP] onCallAnswer => ${content.toString()}');
508 0 : final String callId = content['call_id'];
509 :
510 0 : final call = calls[VoipId(roomId: room.id, callId: callId)];
511 : if (call != null) {
512 0 : if (!call.answeredByUs) {
513 0 : await delegate.stopRingtone();
514 : }
515 0 : if (call.state == CallState.kRinging) {
516 0 : await call.onAnsweredElsewhere();
517 : }
518 :
519 0 : if (call.room.id != room.id) {
520 0 : Logs().w(
521 0 : 'Ignoring call answer for room ${room.id} claiming to be for call in room ${call.room.id}',
522 : );
523 : return;
524 : }
525 :
526 0 : if (call.remoteUserId == null) {
527 0 : Logs().i(
528 : '[VOIP] you probably called the room without setting a userId in invite, setting the calls remote user id to what I get from m.call.answer now',
529 : );
530 0 : call.remoteUserId = remoteUserId;
531 : }
532 :
533 0 : if (call.remoteDeviceId == null) {
534 0 : Logs().i(
535 : '[VOIP] you probably called the room without setting a userId in invite, setting the calls remote user id to what I get from m.call.answer now',
536 : );
537 0 : call.remoteDeviceId = remoteDeviceId;
538 : }
539 0 : if (call.remotePartyId != null) {
540 0 : Logs().d(
541 0 : 'Ignoring call answer from party ${content['party_id']}, we are already with ${call.remotePartyId}',
542 : );
543 : return;
544 : } else {
545 0 : call.remotePartyId = content['party_id'];
546 : }
547 :
548 0 : final answer = RTCSessionDescription(
549 0 : content['answer']['sdp'],
550 0 : content['answer']['type'],
551 : );
552 :
553 : SDPStreamMetadata? metadata;
554 0 : if (content[sdpStreamMetadataKey] != null) {
555 0 : metadata = SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]);
556 : }
557 0 : await call.onAnswerReceived(answer, metadata);
558 : } else {
559 0 : Logs().v('[VOIP] onCallAnswer: Session [$callId] not found!');
560 : }
561 : }
562 :
563 2 : Future<void> onCallCandidates(Room room, Map<String, dynamic> content) async {
564 8 : Logs().v('[VOIP] onCallCandidates => ${content.toString()}');
565 2 : final String callId = content['call_id'];
566 8 : final call = calls[VoipId(roomId: room.id, callId: callId)];
567 : if (call != null) {
568 4 : await call.onCandidatesReceived(content['candidates']);
569 : } else {
570 0 : Logs().v('[VOIP] onCallCandidates: Session [$callId] not found!');
571 : }
572 : }
573 :
574 0 : Future<void> onCallHangup(Room room, Map<String, dynamic> content) async {
575 : // stop play ringtone, if this is an incoming call
576 0 : await delegate.stopRingtone();
577 0 : Logs().v('[VOIP] onCallHangup => ${content.toString()}');
578 0 : final String callId = content['call_id'];
579 :
580 0 : final call = calls[VoipId(roomId: room.id, callId: callId)];
581 : if (call != null) {
582 : // hangup in any case, either if the other party hung up or we did on another device
583 0 : await call.terminate(
584 : CallParty.kRemote,
585 0 : CallErrorCode.values.firstWhereOrNull(
586 0 : (element) => element.reason == content['reason'],
587 : ) ??
588 : CallErrorCode.userHangup,
589 : true,
590 : );
591 : } else {
592 0 : Logs().v('[VOIP] onCallHangup: Session [$callId] not found!');
593 : }
594 0 : if (callId == currentCID?.callId) {
595 0 : currentCID = null;
596 : }
597 : }
598 :
599 0 : Future<void> onCallReject(Room room, Map<String, dynamic> content) async {
600 0 : final String callId = content['call_id'];
601 0 : Logs().d('Reject received for call ID $callId');
602 :
603 0 : final call = calls[VoipId(roomId: room.id, callId: callId)];
604 : if (call != null) {
605 0 : await call.onRejectReceived(
606 0 : CallErrorCode.values.firstWhereOrNull(
607 0 : (element) => element.reason == content['reason'],
608 : ) ??
609 : CallErrorCode.userHangup,
610 : );
611 : } else {
612 0 : Logs().v('[VOIP] onCallReject: Session [$callId] not found!');
613 : }
614 : }
615 :
616 2 : Future<void> onCallSelectAnswer(
617 : Room room,
618 : Map<String, dynamic> content,
619 : ) async {
620 2 : final String callId = content['call_id'];
621 6 : Logs().d('SelectAnswer received for call ID $callId');
622 2 : final String selectedPartyId = content['selected_party_id'];
623 :
624 8 : final call = calls[VoipId(roomId: room.id, callId: callId)];
625 : if (call != null) {
626 8 : if (call.room.id != room.id) {
627 0 : Logs().w(
628 0 : 'Ignoring call select answer for room ${room.id} claiming to be for call in room ${call.room.id}',
629 : );
630 : return;
631 : }
632 2 : await call.onSelectAnswerReceived(selectedPartyId);
633 : }
634 : }
635 :
636 0 : Future<void> onSDPStreamMetadataChangedReceived(
637 : Room room,
638 : Map<String, dynamic> content,
639 : ) async {
640 0 : final String callId = content['call_id'];
641 0 : Logs().d('SDP Stream metadata received for call ID $callId');
642 :
643 0 : final call = calls[VoipId(roomId: room.id, callId: callId)];
644 : if (call != null) {
645 0 : if (content[sdpStreamMetadataKey] == null) {
646 0 : Logs().d('SDP Stream metadata is null');
647 : return;
648 : }
649 0 : await call.onSDPStreamMetadataReceived(
650 0 : SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]),
651 : );
652 : }
653 : }
654 :
655 0 : Future<void> onAssertedIdentityReceived(
656 : Room room,
657 : Map<String, dynamic> content,
658 : ) async {
659 0 : final String callId = content['call_id'];
660 0 : Logs().d('Asserted identity received for call ID $callId');
661 :
662 0 : final call = calls[VoipId(roomId: room.id, callId: callId)];
663 : if (call != null) {
664 0 : if (content['asserted_identity'] == null) {
665 0 : Logs().d('asserted_identity is null ');
666 : return;
667 : }
668 0 : call.onAssertedIdentityReceived(
669 0 : AssertedIdentity.fromJson(content['asserted_identity']),
670 : );
671 : }
672 : }
673 :
674 0 : Future<void> onCallNegotiate(Room room, Map<String, dynamic> content) async {
675 0 : final String callId = content['call_id'];
676 0 : Logs().d('Negotiate received for call ID $callId');
677 :
678 0 : final call = calls[VoipId(roomId: room.id, callId: callId)];
679 : if (call != null) {
680 : // ideally you also check the lifetime here and discard negotiation events
681 : // if age of the event was older than the lifetime but as to device events
682 : // do not have a unsigned age nor a origin_server_ts there's no easy way to
683 : // override this one function atm
684 :
685 0 : final description = content['description'];
686 : try {
687 : SDPStreamMetadata? metadata;
688 0 : if (content[sdpStreamMetadataKey] != null) {
689 0 : metadata = SDPStreamMetadata.fromJson(content[sdpStreamMetadataKey]);
690 : }
691 0 : await call.onNegotiateReceived(
692 : metadata,
693 0 : RTCSessionDescription(description['sdp'], description['type']),
694 : );
695 : } catch (e, s) {
696 0 : Logs().e('[VOIP] Failed to complete negotiation', e, s);
697 : }
698 : }
699 : }
700 :
701 2 : CallType getCallType(String sdp) {
702 : try {
703 2 : final session = sdp_transform.parse(sdp);
704 8 : if (session['media'].indexWhere((e) => e['type'] == 'video') != -1) {
705 : return CallType.kVideo;
706 : }
707 : } catch (e, s) {
708 0 : Logs().e('[VOIP] Failed to getCallType', e, s);
709 : }
710 :
711 : return CallType.kVoice;
712 : }
713 :
714 2 : Future<List<Map<String, dynamic>>> getIceServers() async {
715 2 : if (_turnServerCredentials == null) {
716 : try {
717 6 : _turnServerCredentials = await client.getTurnServer();
718 : } catch (e) {
719 0 : Logs().v('[VOIP] getTurnServerCredentials error => ${e.toString()}');
720 : }
721 : }
722 :
723 2 : if (_turnServerCredentials == null) {
724 0 : return [];
725 : }
726 :
727 2 : return [
728 2 : {
729 4 : 'username': _turnServerCredentials!.username,
730 4 : 'credential': _turnServerCredentials!.password,
731 4 : 'urls': _turnServerCredentials!.uris,
732 : }
733 : ];
734 : }
735 :
736 : /// Make a P2P call to room
737 : ///
738 : /// Pretty important to set the userId, or all the users in the room get a call.
739 : /// Including your own other devices, so just set it to directChatMatrixId
740 : ///
741 : /// Setting the deviceId would make all other devices for that userId ignore the call
742 : /// Ideally only group calls would need setting both userId and deviceId to allow
743 : /// having 2 devices from the same user in a group call
744 : ///
745 : /// For p2p call, you want to have all the devices of the specified `userId` ring
746 2 : Future<CallSession> inviteToCall(
747 : Room room,
748 : CallType type, {
749 : String? userId,
750 : String? deviceId,
751 : }) async {
752 2 : final roomId = room.id;
753 2 : final callId = genCallID();
754 2 : if (currentGroupCID == null) {
755 4 : incomingCallRoomId[roomId] = callId;
756 : }
757 2 : final opts = CallOptions(
758 : callId: callId,
759 : type: type,
760 : dir: CallDirection.kOutgoing,
761 : room: room,
762 : voip: this,
763 2 : localPartyId: localPartyId,
764 2 : iceServers: await getIceServers(),
765 : );
766 2 : final newCall = createNewCall(opts);
767 :
768 2 : newCall.remoteUserId = userId;
769 2 : newCall.remoteDeviceId = deviceId;
770 :
771 4 : currentCID = VoipId(roomId: roomId, callId: callId);
772 6 : await newCall.initOutboundCall(type).then((_) {
773 4 : delegate.handleNewCall(newCall);
774 : });
775 : return newCall;
776 : }
777 :
778 2 : CallSession createNewCall(CallOptions opts) {
779 2 : final call = CallSession(opts);
780 12 : calls[VoipId(roomId: opts.room.id, callId: opts.callId)] = call;
781 : return call;
782 : }
783 :
784 : /// Create a new group call in an existing room.
785 : ///
786 : /// [groupCallId] The room id to call
787 : ///
788 : /// [application] normal group call, thrirdroom, etc
789 : ///
790 : /// [scope] room, between specifc users, etc.
791 0 : Future<GroupCallSession> _newGroupCall(
792 : String groupCallId,
793 : Room room,
794 : CallBackend backend,
795 : String? application,
796 : String? scope,
797 : ) async {
798 0 : if (getGroupCallById(room.id, groupCallId) != null) {
799 0 : Logs().v('[VOIP] [$groupCallId] already exists.');
800 0 : return getGroupCallById(room.id, groupCallId)!;
801 : }
802 :
803 0 : final groupCall = GroupCallSession(
804 : groupCallId: groupCallId,
805 0 : client: client,
806 : room: room,
807 : voip: this,
808 : backend: backend,
809 : application: application,
810 : scope: scope,
811 : );
812 :
813 0 : setGroupCallById(groupCall);
814 :
815 : return groupCall;
816 : }
817 :
818 : /// Create a new group call in an existing room.
819 : ///
820 : /// [groupCallId] The room id to call
821 : ///
822 : /// [application] normal group call, thrirdroom, etc
823 : ///
824 : /// [scope] room, between specifc users, etc.
825 : ///
826 : /// [preShareKey] for livekit calls it creates and shares a key with other
827 : /// participants in the call without entering, useful on onboarding screens.
828 : /// does not do anything in mesh calls
829 :
830 0 : Future<GroupCallSession> fetchOrCreateGroupCall(
831 : String groupCallId,
832 : Room room,
833 : CallBackend backend,
834 : String? application,
835 : String? scope, {
836 : bool preShareKey = true,
837 : }) async {
838 : // somehow user were mising their powerlevels events and got stuck
839 : // with the exception below, this part just makes sure importantStateEvents
840 : // does not cause it.
841 0 : await room.postLoad();
842 :
843 0 : if (!room.groupCallsEnabledForEveryone) {
844 0 : await room.enableGroupCalls();
845 : }
846 :
847 0 : if (!room.canJoinGroupCall) {
848 0 : throw MatrixSDKVoipException(
849 : '''
850 0 : User ${client.userID}:${client.deviceID} is not allowed to join famedly calls in room ${room.id},
851 0 : canJoinGroupCall: ${room.canJoinGroupCall},
852 0 : groupCallsEnabledForEveryone: ${room.groupCallsEnabledForEveryone},
853 0 : needed: ${room.powerForChangingStateEvent(EventTypes.GroupCallMember)},
854 0 : own: ${room.ownPowerLevel}}
855 0 : plMap: ${room.getState(EventTypes.RoomPowerLevels)?.content}
856 0 : ''',
857 : );
858 : }
859 :
860 0 : GroupCallSession? groupCall = getGroupCallById(room.id, groupCallId);
861 :
862 0 : groupCall ??= await _newGroupCall(
863 : groupCallId,
864 : room,
865 : backend,
866 : application,
867 : scope,
868 : );
869 :
870 : if (preShareKey) {
871 0 : await groupCall.backend.preShareKey(groupCall);
872 : }
873 :
874 : return groupCall;
875 : }
876 :
877 0 : GroupCallSession? getGroupCallById(String roomId, String groupCallId) {
878 0 : return groupCalls[VoipId(roomId: roomId, callId: groupCallId)];
879 : }
880 :
881 2 : void setGroupCallById(GroupCallSession groupCallSession) {
882 6 : groupCalls[VoipId(
883 4 : roomId: groupCallSession.room.id,
884 2 : callId: groupCallSession.groupCallId,
885 : )] = groupCallSession;
886 : }
887 :
888 : /// Create a new group call from a room state event.
889 2 : Future<void> createGroupCallFromRoomStateEvent(
890 : CallMembership membership, {
891 : bool emitHandleNewGroupCall = true,
892 : }) async {
893 2 : if (membership.isExpired) {
894 4 : Logs().d(
895 4 : 'Ignoring expired membership in passive groupCall creator. ${membership.toJson()}',
896 : );
897 : return;
898 : }
899 :
900 6 : final room = client.getRoomById(membership.roomId);
901 :
902 : if (room == null) {
903 0 : Logs().w('Couldn\'t find room ${membership.roomId} for GroupCallSession');
904 : return;
905 : }
906 :
907 4 : if (membership.application != 'm.call' && membership.scope != 'm.room') {
908 0 : Logs().w('Received invalid group call application or scope.');
909 : return;
910 : }
911 :
912 2 : final groupCall = GroupCallSession(
913 2 : client: client,
914 : voip: this,
915 : room: room,
916 2 : backend: membership.backend,
917 2 : groupCallId: membership.callId,
918 2 : application: membership.application,
919 2 : scope: membership.scope,
920 : );
921 :
922 4 : if (groupCalls.containsKey(
923 6 : VoipId(roomId: membership.roomId, callId: membership.callId),
924 : )) {
925 : return;
926 : }
927 :
928 2 : setGroupCallById(groupCall);
929 :
930 4 : onIncomingGroupCall.add(groupCall);
931 : if (emitHandleNewGroupCall) {
932 4 : await delegate.handleNewGroupCall(groupCall);
933 : }
934 : }
935 :
936 0 : @Deprecated('Call `hasActiveGroupCall` on the room directly instead')
937 0 : bool hasActiveCall(Room room) => room.hasActiveGroupCall;
938 : }
|