Line data Source code
1 : import 'dart:async';
2 :
3 : import 'package:collection/collection.dart';
4 : import 'package:webrtc_interface/webrtc_interface.dart';
5 :
6 : import 'package:matrix/matrix.dart';
7 : import 'package:matrix/src/utils/cached_stream_controller.dart';
8 : import 'package:matrix/src/voip/models/call_membership.dart';
9 : import 'package:matrix/src/voip/models/call_options.dart';
10 : import 'package:matrix/src/voip/utils/stream_helper.dart';
11 : import 'package:matrix/src/voip/utils/user_media_constraints.dart';
12 :
13 : class MeshBackend extends CallBackend {
14 2 : MeshBackend({
15 : super.type = 'mesh',
16 : });
17 :
18 : final List<CallSession> _callSessions = [];
19 :
20 : /// participant:volume
21 : final Map<CallParticipant, double> _audioLevelsMap = {};
22 :
23 : StreamSubscription<CallSession>? _callSubscription;
24 :
25 : Timer? _activeSpeakerLoopTimeout;
26 :
27 : final CachedStreamController<WrappedMediaStream> onStreamAdd =
28 : CachedStreamController();
29 :
30 : final CachedStreamController<WrappedMediaStream> onStreamRemoved =
31 : CachedStreamController();
32 :
33 : final CachedStreamController<GroupCallSession> onGroupCallFeedsChanged =
34 : CachedStreamController();
35 :
36 2 : @override
37 : Map<String, Object?> toJson() {
38 2 : return {
39 2 : 'type': type,
40 : };
41 : }
42 :
43 : CallParticipant? _activeSpeaker;
44 : WrappedMediaStream? _localUserMediaStream;
45 : WrappedMediaStream? _localScreenshareStream;
46 : final List<WrappedMediaStream> _userMediaStreams = [];
47 : final List<WrappedMediaStream> _screenshareStreams = [];
48 :
49 0 : List<WrappedMediaStream> _getLocalStreams() {
50 0 : final feeds = <WrappedMediaStream>[];
51 :
52 0 : if (localUserMediaStream != null) {
53 0 : feeds.add(localUserMediaStream!);
54 : }
55 :
56 0 : if (localScreenshareStream != null) {
57 0 : feeds.add(localScreenshareStream!);
58 : }
59 :
60 : return feeds;
61 : }
62 :
63 0 : Future<MediaStream> _getUserMedia(
64 : GroupCallSession groupCall,
65 : CallType type,
66 : ) async {
67 0 : final mediaConstraints = {
68 : 'audio': UserMediaConstraints.micMediaConstraints,
69 0 : 'video': type == CallType.kVideo
70 : ? UserMediaConstraints.camMediaConstraints
71 : : false,
72 : };
73 :
74 : try {
75 0 : return await groupCall.voip.delegate.mediaDevices
76 0 : .getUserMedia(mediaConstraints);
77 : } catch (e) {
78 0 : groupCall.setState(GroupCallState.localCallFeedUninitialized);
79 : rethrow;
80 : }
81 : }
82 :
83 0 : Future<MediaStream> _getDisplayMedia(GroupCallSession groupCall) async {
84 0 : final mediaConstraints = {
85 : 'audio': false,
86 : 'video': true,
87 : };
88 : try {
89 0 : return await groupCall.voip.delegate.mediaDevices
90 0 : .getDisplayMedia(mediaConstraints);
91 : } catch (e, s) {
92 0 : throw MatrixSDKVoipException('_getDisplayMedia failed', stackTrace: s);
93 : }
94 : }
95 :
96 0 : CallSession? _getCallForParticipant(
97 : GroupCallSession groupCall,
98 : CallParticipant participant,
99 : ) {
100 0 : return _callSessions.singleWhereOrNull(
101 0 : (call) =>
102 0 : call.groupCallId == groupCall.groupCallId &&
103 0 : CallParticipant(
104 0 : groupCall.voip,
105 0 : userId: call.remoteUserId!,
106 0 : deviceId: call.remoteDeviceId,
107 0 : ) ==
108 : participant,
109 : );
110 : }
111 :
112 0 : Future<void> _addCall(GroupCallSession groupCall, CallSession call) async {
113 0 : _callSessions.add(call);
114 0 : await _initCall(groupCall, call);
115 0 : groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
116 : }
117 :
118 : /// init a peer call from group calls.
119 0 : Future<void> _initCall(GroupCallSession groupCall, CallSession call) async {
120 0 : if (call.remoteUserId == null) {
121 0 : throw MatrixSDKVoipException(
122 : 'Cannot init call without proper invitee user and device Id',
123 : );
124 : }
125 :
126 0 : call.onCallStateChanged.stream.listen(
127 0 : ((event) async {
128 0 : await _onCallStateChanged(call, event);
129 : }),
130 : );
131 :
132 0 : call.onCallReplaced.stream.listen((CallSession newCall) async {
133 0 : await _replaceCall(groupCall, call, newCall);
134 : });
135 :
136 0 : call.onCallStreamsChanged.stream.listen((call) async {
137 0 : await call.tryRemoveStopedStreams();
138 0 : await _onStreamsChanged(groupCall, call);
139 : });
140 :
141 0 : call.onCallHangupNotifierForGroupCalls.stream.listen((event) async {
142 0 : await _onCallHangup(groupCall, call);
143 : });
144 :
145 0 : call.onStreamAdd.stream.listen((stream) {
146 0 : if (!stream.isLocal()) {
147 0 : onStreamAdd.add(stream);
148 : }
149 : });
150 :
151 0 : call.onStreamRemoved.stream.listen((stream) {
152 0 : if (!stream.isLocal()) {
153 0 : onStreamRemoved.add(stream);
154 : }
155 : });
156 : }
157 :
158 0 : Future<void> _replaceCall(
159 : GroupCallSession groupCall,
160 : CallSession existingCall,
161 : CallSession replacementCall,
162 : ) async {
163 0 : final existingCallIndex = _callSessions
164 0 : .indexWhere((element) => element.callId == existingCall.callId);
165 :
166 0 : if (existingCallIndex == -1) {
167 0 : throw MatrixSDKVoipException('Couldn\'t find call to replace');
168 : }
169 :
170 0 : _callSessions.removeAt(existingCallIndex);
171 0 : _callSessions.add(replacementCall);
172 :
173 0 : await _disposeCall(groupCall, existingCall, CallErrorCode.replaced);
174 0 : await _initCall(groupCall, replacementCall);
175 :
176 0 : groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
177 : }
178 :
179 : /// Removes a peer call from group calls.
180 0 : Future<void> _removeCall(
181 : GroupCallSession groupCall,
182 : CallSession call,
183 : CallErrorCode hangupReason,
184 : ) async {
185 0 : await _disposeCall(groupCall, call, hangupReason);
186 :
187 0 : _callSessions.removeWhere((element) => call.callId == element.callId);
188 :
189 0 : groupCall.onGroupCallEvent.add(GroupCallStateChange.callsChanged);
190 : }
191 :
192 0 : Future<void> _disposeCall(
193 : GroupCallSession groupCall,
194 : CallSession call,
195 : CallErrorCode hangupReason,
196 : ) async {
197 0 : if (call.remoteUserId == null) {
198 0 : throw MatrixSDKVoipException(
199 : 'Cannot init call without proper invitee user and device Id',
200 : );
201 : }
202 :
203 0 : if (call.hangupReason == CallErrorCode.replaced) {
204 : return;
205 : }
206 :
207 0 : if (call.state != CallState.kEnded) {
208 : // no need to emit individual handleCallEnded on group calls
209 : // also prevents a loop of hangup and onCallHangupNotifierForGroupCalls
210 0 : await call.hangup(reason: hangupReason, shouldEmit: false);
211 : }
212 :
213 0 : final usermediaStream = _getUserMediaStreamByParticipantId(
214 0 : CallParticipant(
215 0 : groupCall.voip,
216 0 : userId: call.remoteUserId!,
217 0 : deviceId: call.remoteDeviceId,
218 0 : ).id,
219 : );
220 :
221 : if (usermediaStream != null) {
222 0 : await _removeUserMediaStream(groupCall, usermediaStream);
223 : }
224 :
225 0 : final screenshareStream = _getScreenshareStreamByParticipantId(
226 0 : CallParticipant(
227 0 : groupCall.voip,
228 0 : userId: call.remoteUserId!,
229 0 : deviceId: call.remoteDeviceId,
230 0 : ).id,
231 : );
232 :
233 : if (screenshareStream != null) {
234 0 : await _removeScreenshareStream(groupCall, screenshareStream);
235 : }
236 : }
237 :
238 0 : Future<void> _onStreamsChanged(
239 : GroupCallSession groupCall,
240 : CallSession call,
241 : ) async {
242 0 : if (call.remoteUserId == null) {
243 0 : throw MatrixSDKVoipException(
244 : 'Cannot init call without proper invitee user and device Id',
245 : );
246 : }
247 :
248 0 : final currentUserMediaStream = _getUserMediaStreamByParticipantId(
249 0 : CallParticipant(
250 0 : groupCall.voip,
251 0 : userId: call.remoteUserId!,
252 0 : deviceId: call.remoteDeviceId,
253 0 : ).id,
254 : );
255 :
256 0 : final remoteUsermediaStream = call.remoteUserMediaStream;
257 0 : final remoteStreamChanged = remoteUsermediaStream != currentUserMediaStream;
258 :
259 : if (remoteStreamChanged) {
260 : if (currentUserMediaStream == null && remoteUsermediaStream != null) {
261 0 : await _addUserMediaStream(groupCall, remoteUsermediaStream);
262 : } else if (currentUserMediaStream != null &&
263 : remoteUsermediaStream != null) {
264 0 : await _replaceUserMediaStream(
265 : groupCall,
266 : currentUserMediaStream,
267 : remoteUsermediaStream,
268 : );
269 : } else if (currentUserMediaStream != null &&
270 : remoteUsermediaStream == null) {
271 0 : await _removeUserMediaStream(groupCall, currentUserMediaStream);
272 : }
273 : }
274 :
275 0 : final currentScreenshareStream = _getScreenshareStreamByParticipantId(
276 0 : CallParticipant(
277 0 : groupCall.voip,
278 0 : userId: call.remoteUserId!,
279 0 : deviceId: call.remoteDeviceId,
280 0 : ).id,
281 : );
282 0 : final remoteScreensharingStream = call.remoteScreenSharingStream;
283 : final remoteScreenshareStreamChanged =
284 0 : remoteScreensharingStream != currentScreenshareStream;
285 :
286 : if (remoteScreenshareStreamChanged) {
287 : if (currentScreenshareStream == null &&
288 : remoteScreensharingStream != null) {
289 0 : _addScreenshareStream(groupCall, remoteScreensharingStream);
290 : } else if (currentScreenshareStream != null &&
291 : remoteScreensharingStream != null) {
292 0 : await _replaceScreenshareStream(
293 : groupCall,
294 : currentScreenshareStream,
295 : remoteScreensharingStream,
296 : );
297 : } else if (currentScreenshareStream != null &&
298 : remoteScreensharingStream == null) {
299 0 : await _removeScreenshareStream(groupCall, currentScreenshareStream);
300 : }
301 : }
302 :
303 0 : onGroupCallFeedsChanged.add(groupCall);
304 : }
305 :
306 0 : WrappedMediaStream? _getUserMediaStreamByParticipantId(String participantId) {
307 0 : final stream = _userMediaStreams
308 0 : .where((stream) => stream.participant.id == participantId);
309 0 : if (stream.isNotEmpty) {
310 0 : return stream.first;
311 : }
312 : return null;
313 : }
314 :
315 0 : void _onActiveSpeakerLoop(GroupCallSession groupCall) async {
316 : CallParticipant? nextActiveSpeaker;
317 : // idc about screen sharing atm.
318 : final userMediaStreamsCopyList =
319 0 : List<WrappedMediaStream>.from(_userMediaStreams);
320 0 : for (final stream in userMediaStreamsCopyList) {
321 0 : if (stream.participant.isLocal && stream.pc == null) {
322 : continue;
323 : }
324 :
325 0 : final List<StatsReport> statsReport = await stream.pc!.getStats();
326 : statsReport
327 0 : .removeWhere((element) => !element.values.containsKey('audioLevel'));
328 :
329 : // https://www.w3.org/TR/webrtc-stats/#summary
330 : final otherPartyAudioLevel = statsReport
331 0 : .singleWhereOrNull(
332 0 : (element) =>
333 0 : element.type == 'inbound-rtp' &&
334 0 : element.values['kind'] == 'audio',
335 : )
336 0 : ?.values['audioLevel'];
337 : if (otherPartyAudioLevel != null) {
338 0 : _audioLevelsMap[stream.participant] = otherPartyAudioLevel;
339 : }
340 :
341 : // https://www.w3.org/TR/webrtc-stats/#dom-rtcstatstype-media-source
342 : // firefox does not seem to have this though. Works on chrome and android
343 : final ownAudioLevel = statsReport
344 0 : .singleWhereOrNull(
345 0 : (element) =>
346 0 : element.type == 'media-source' &&
347 0 : element.values['kind'] == 'audio',
348 : )
349 0 : ?.values['audioLevel'];
350 0 : if (groupCall.localParticipant != null &&
351 : ownAudioLevel != null &&
352 0 : _audioLevelsMap[groupCall.localParticipant] != ownAudioLevel) {
353 0 : _audioLevelsMap[groupCall.localParticipant!] = ownAudioLevel;
354 : }
355 : }
356 :
357 : double maxAudioLevel = double.negativeInfinity;
358 : // TODO: we probably want a threshold here?
359 0 : _audioLevelsMap.forEach((key, value) {
360 0 : if (value > maxAudioLevel) {
361 : nextActiveSpeaker = key;
362 : maxAudioLevel = value;
363 : }
364 : });
365 :
366 0 : if (nextActiveSpeaker != null && _activeSpeaker != nextActiveSpeaker) {
367 0 : _activeSpeaker = nextActiveSpeaker;
368 0 : groupCall.onGroupCallEvent.add(GroupCallStateChange.activeSpeakerChanged);
369 : }
370 0 : _activeSpeakerLoopTimeout?.cancel();
371 0 : _activeSpeakerLoopTimeout = Timer(
372 : CallConstants.activeSpeakerInterval,
373 0 : () => _onActiveSpeakerLoop(groupCall),
374 : );
375 : }
376 :
377 0 : WrappedMediaStream? _getScreenshareStreamByParticipantId(
378 : String participantId,
379 : ) {
380 0 : final stream = _screenshareStreams
381 0 : .where((stream) => stream.participant.id == participantId);
382 0 : if (stream.isNotEmpty) {
383 0 : return stream.first;
384 : }
385 : return null;
386 : }
387 :
388 0 : void _addScreenshareStream(
389 : GroupCallSession groupCall,
390 : WrappedMediaStream stream,
391 : ) {
392 0 : _screenshareStreams.add(stream);
393 0 : onStreamAdd.add(stream);
394 0 : groupCall.onGroupCallEvent
395 0 : .add(GroupCallStateChange.screenshareStreamsChanged);
396 : }
397 :
398 0 : Future<void> _replaceScreenshareStream(
399 : GroupCallSession groupCall,
400 : WrappedMediaStream existingStream,
401 : WrappedMediaStream replacementStream,
402 : ) async {
403 0 : final streamIndex = _screenshareStreams.indexWhere(
404 0 : (stream) => stream.participant.id == existingStream.participant.id,
405 : );
406 :
407 0 : if (streamIndex == -1) {
408 0 : throw MatrixSDKVoipException(
409 : 'Couldn\'t find screenshare stream to replace',
410 : );
411 : }
412 :
413 0 : _screenshareStreams.replaceRange(streamIndex, 1, [replacementStream]);
414 :
415 0 : await existingStream.dispose();
416 0 : groupCall.onGroupCallEvent
417 0 : .add(GroupCallStateChange.screenshareStreamsChanged);
418 : }
419 :
420 0 : Future<void> _removeScreenshareStream(
421 : GroupCallSession groupCall,
422 : WrappedMediaStream stream,
423 : ) async {
424 0 : final streamIndex = _screenshareStreams
425 0 : .indexWhere((stream) => stream.participant.id == stream.participant.id);
426 :
427 0 : if (streamIndex == -1) {
428 0 : throw MatrixSDKVoipException(
429 : 'Couldn\'t find screenshare stream to remove',
430 : );
431 : }
432 :
433 0 : _screenshareStreams.removeWhere(
434 0 : (element) => element.participant.id == stream.participant.id,
435 : );
436 :
437 0 : onStreamRemoved.add(stream);
438 :
439 0 : if (stream.isLocal()) {
440 0 : await stopMediaStream(stream.stream);
441 : }
442 :
443 0 : groupCall.onGroupCallEvent
444 0 : .add(GroupCallStateChange.screenshareStreamsChanged);
445 : }
446 :
447 0 : Future<void> _onCallStateChanged(CallSession call, CallState state) async {
448 0 : final audioMuted = localUserMediaStream?.isAudioMuted() ?? true;
449 0 : if (call.localUserMediaStream != null &&
450 0 : call.isMicrophoneMuted != audioMuted) {
451 0 : await call.setMicrophoneMuted(audioMuted);
452 : }
453 :
454 0 : final videoMuted = localUserMediaStream?.isVideoMuted() ?? true;
455 :
456 0 : if (call.localUserMediaStream != null &&
457 0 : call.isLocalVideoMuted != videoMuted) {
458 0 : await call.setLocalVideoMuted(videoMuted);
459 : }
460 : }
461 :
462 0 : Future<void> _onCallHangup(
463 : GroupCallSession groupCall,
464 : CallSession call,
465 : ) async {
466 0 : if (call.hangupReason == CallErrorCode.replaced) {
467 : return;
468 : }
469 0 : await _onStreamsChanged(groupCall, call);
470 0 : await _removeCall(groupCall, call, call.hangupReason!);
471 : }
472 :
473 0 : Future<void> _addUserMediaStream(
474 : GroupCallSession groupCall,
475 : WrappedMediaStream stream,
476 : ) async {
477 0 : _userMediaStreams.add(stream);
478 0 : onStreamAdd.add(stream);
479 0 : groupCall.onGroupCallEvent
480 0 : .add(GroupCallStateChange.userMediaStreamsChanged);
481 : }
482 :
483 0 : Future<void> _replaceUserMediaStream(
484 : GroupCallSession groupCall,
485 : WrappedMediaStream existingStream,
486 : WrappedMediaStream replacementStream,
487 : ) async {
488 0 : final streamIndex = _userMediaStreams.indexWhere(
489 0 : (stream) => stream.participant.id == existingStream.participant.id,
490 : );
491 :
492 0 : if (streamIndex == -1) {
493 0 : throw MatrixSDKVoipException(
494 : 'Couldn\'t find user media stream to replace',
495 : );
496 : }
497 :
498 0 : _userMediaStreams.replaceRange(streamIndex, 1, [replacementStream]);
499 :
500 0 : await existingStream.dispose();
501 0 : groupCall.onGroupCallEvent
502 0 : .add(GroupCallStateChange.userMediaStreamsChanged);
503 : }
504 :
505 0 : Future<void> _removeUserMediaStream(
506 : GroupCallSession groupCall,
507 : WrappedMediaStream stream,
508 : ) async {
509 0 : final streamIndex = _userMediaStreams.indexWhere(
510 0 : (element) => element.participant.id == stream.participant.id,
511 : );
512 :
513 0 : if (streamIndex == -1) {
514 0 : throw MatrixSDKVoipException(
515 : 'Couldn\'t find user media stream to remove',
516 : );
517 : }
518 :
519 0 : _userMediaStreams.removeWhere(
520 0 : (element) => element.participant.id == stream.participant.id,
521 : );
522 0 : _audioLevelsMap.remove(stream.participant);
523 0 : onStreamRemoved.add(stream);
524 :
525 0 : if (stream.isLocal()) {
526 0 : await stopMediaStream(stream.stream);
527 : }
528 :
529 0 : groupCall.onGroupCallEvent
530 0 : .add(GroupCallStateChange.userMediaStreamsChanged);
531 :
532 0 : if (_activeSpeaker == stream.participant && _userMediaStreams.isNotEmpty) {
533 0 : _activeSpeaker = _userMediaStreams[0].participant;
534 0 : groupCall.onGroupCallEvent.add(GroupCallStateChange.activeSpeakerChanged);
535 : }
536 : }
537 :
538 0 : @override
539 : bool get e2eeEnabled => false;
540 :
541 0 : @override
542 0 : CallParticipant? get activeSpeaker => _activeSpeaker;
543 :
544 0 : @override
545 0 : WrappedMediaStream? get localUserMediaStream => _localUserMediaStream;
546 :
547 0 : @override
548 0 : WrappedMediaStream? get localScreenshareStream => _localScreenshareStream;
549 :
550 0 : @override
551 : List<WrappedMediaStream> get userMediaStreams =>
552 0 : List.unmodifiable(_userMediaStreams);
553 :
554 0 : @override
555 : List<WrappedMediaStream> get screenShareStreams =>
556 0 : List.unmodifiable(_screenshareStreams);
557 :
558 0 : @override
559 : Future<void> updateMediaDeviceForCalls() async {
560 0 : for (final call in _callSessions) {
561 0 : await call.updateMediaDeviceForCall();
562 : }
563 : }
564 :
565 : /// Initializes the local user media stream.
566 : /// The media stream must be prepared before the group call enters.
567 : /// if you allow the user to configure their camera and such ahead of time,
568 : /// you can pass that `stream` on to this function.
569 : /// This allows you to configure the camera before joining the call without
570 : /// having to reopen the stream and possibly losing settings.
571 0 : @override
572 : Future<WrappedMediaStream?> initLocalStream(
573 : GroupCallSession groupCall, {
574 : WrappedMediaStream? stream,
575 : }) async {
576 0 : if (groupCall.state != GroupCallState.localCallFeedUninitialized) {
577 0 : throw MatrixSDKVoipException(
578 0 : 'Cannot initialize local call feed in the ${groupCall.state} state.',
579 : );
580 : }
581 :
582 0 : groupCall.setState(GroupCallState.initializingLocalCallFeed);
583 :
584 : WrappedMediaStream localWrappedMediaStream;
585 :
586 : if (stream == null) {
587 : MediaStream stream;
588 :
589 : try {
590 0 : stream = await _getUserMedia(groupCall, CallType.kVideo);
591 : } catch (error) {
592 0 : groupCall.setState(GroupCallState.localCallFeedUninitialized);
593 : rethrow;
594 : }
595 :
596 0 : localWrappedMediaStream = WrappedMediaStream(
597 : stream: stream,
598 0 : participant: groupCall.localParticipant!,
599 0 : room: groupCall.room,
600 0 : client: groupCall.client,
601 : purpose: SDPStreamMetadataPurpose.Usermedia,
602 0 : audioMuted: stream.getAudioTracks().isEmpty,
603 0 : videoMuted: stream.getVideoTracks().isEmpty,
604 : isGroupCall: true,
605 0 : voip: groupCall.voip,
606 : );
607 : } else {
608 : localWrappedMediaStream = stream;
609 : }
610 :
611 0 : _localUserMediaStream = localWrappedMediaStream;
612 0 : await _addUserMediaStream(groupCall, localWrappedMediaStream);
613 :
614 0 : groupCall.setState(GroupCallState.localCallFeedInitialized);
615 :
616 0 : _activeSpeaker = null;
617 :
618 : return localWrappedMediaStream;
619 : }
620 :
621 0 : @override
622 : Future<void> setDeviceMuted(
623 : GroupCallSession groupCall,
624 : bool muted,
625 : MediaInputKind kind,
626 : ) async {
627 0 : if (!await hasMediaDevice(groupCall.voip.delegate, kind)) {
628 : return;
629 : }
630 :
631 0 : if (localUserMediaStream != null) {
632 : switch (kind) {
633 0 : case MediaInputKind.audioinput:
634 0 : localUserMediaStream!.setAudioMuted(muted);
635 0 : setTracksEnabled(
636 0 : localUserMediaStream!.stream!.getAudioTracks(),
637 : !muted,
638 : );
639 0 : for (final call in _callSessions) {
640 0 : await call.setMicrophoneMuted(muted);
641 : }
642 : break;
643 0 : case MediaInputKind.videoinput:
644 0 : localUserMediaStream!.setVideoMuted(muted);
645 0 : setTracksEnabled(
646 0 : localUserMediaStream!.stream!.getVideoTracks(),
647 : !muted,
648 : );
649 0 : for (final call in _callSessions) {
650 0 : await call.setLocalVideoMuted(muted);
651 : }
652 : break;
653 : default:
654 : }
655 : }
656 :
657 0 : groupCall.onGroupCallEvent.add(GroupCallStateChange.localMuteStateChanged);
658 : return;
659 : }
660 :
661 0 : Future<void> _onIncomingCall(
662 : GroupCallSession groupCall,
663 : CallSession newCall,
664 : ) async {
665 : // The incoming calls may be for another room, which we will ignore.
666 0 : if (newCall.room.id != groupCall.room.id) {
667 : return;
668 : }
669 :
670 0 : if (newCall.state != CallState.kRinging) {
671 0 : Logs().w('Incoming call no longer in ringing state. Ignoring.');
672 : return;
673 : }
674 :
675 0 : if (newCall.groupCallId == null ||
676 0 : newCall.groupCallId != groupCall.groupCallId) {
677 0 : Logs().v(
678 0 : 'Incoming call with groupCallId ${newCall.groupCallId} ignored because it doesn\'t match the current group call',
679 : );
680 0 : await newCall.reject();
681 : return;
682 : }
683 :
684 0 : final existingCall = _getCallForParticipant(
685 : groupCall,
686 0 : CallParticipant(
687 0 : groupCall.voip,
688 0 : userId: newCall.remoteUserId!,
689 0 : deviceId: newCall.remoteDeviceId,
690 : ),
691 : );
692 :
693 0 : if (existingCall != null && existingCall.callId == newCall.callId) {
694 : return;
695 : }
696 :
697 0 : Logs().v(
698 0 : 'GroupCallSession: incoming call from: ${newCall.remoteUserId}${newCall.remoteDeviceId}${newCall.remotePartyId}',
699 : );
700 :
701 : // Check if the user calling has an existing call and use this call instead.
702 : if (existingCall != null) {
703 0 : await _replaceCall(groupCall, existingCall, newCall);
704 : } else {
705 0 : await _addCall(groupCall, newCall);
706 : }
707 :
708 0 : await newCall.answerWithStreams(_getLocalStreams());
709 : }
710 :
711 0 : @override
712 : Future<void> setScreensharingEnabled(
713 : GroupCallSession groupCall,
714 : bool enabled,
715 : String desktopCapturerSourceId,
716 : ) async {
717 0 : if (enabled == (localScreenshareStream != null)) {
718 : return;
719 : }
720 :
721 : if (enabled) {
722 : try {
723 0 : Logs().v('Asking for screensharing permissions...');
724 0 : final stream = await _getDisplayMedia(groupCall);
725 0 : for (final track in stream.getTracks()) {
726 : // screen sharing should only have 1 video track anyway, so this only
727 : // fires once
728 0 : track.onEnded = () async {
729 0 : await setScreensharingEnabled(groupCall, false, '');
730 : };
731 : }
732 0 : Logs().v(
733 : 'Screensharing permissions granted. Setting screensharing enabled on all calls',
734 : );
735 0 : _localScreenshareStream = WrappedMediaStream(
736 : stream: stream,
737 0 : participant: groupCall.localParticipant!,
738 0 : room: groupCall.room,
739 0 : client: groupCall.client,
740 : purpose: SDPStreamMetadataPurpose.Screenshare,
741 0 : audioMuted: stream.getAudioTracks().isEmpty,
742 0 : videoMuted: stream.getVideoTracks().isEmpty,
743 : isGroupCall: true,
744 0 : voip: groupCall.voip,
745 : );
746 :
747 0 : _addScreenshareStream(groupCall, localScreenshareStream!);
748 :
749 0 : groupCall.onGroupCallEvent
750 0 : .add(GroupCallStateChange.localScreenshareStateChanged);
751 0 : for (final call in _callSessions) {
752 0 : await call.addLocalStream(
753 0 : await localScreenshareStream!.stream!.clone(),
754 0 : localScreenshareStream!.purpose,
755 : );
756 : }
757 :
758 0 : await groupCall.sendMemberStateEvent();
759 :
760 : return;
761 : } catch (e, s) {
762 0 : Logs().e('[VOIP] Enabling screensharing error', e, s);
763 0 : groupCall.onGroupCallEvent.add(GroupCallStateChange.error);
764 : return;
765 : }
766 : } else {
767 0 : for (final call in _callSessions) {
768 0 : await call.removeLocalStream(call.localScreenSharingStream!);
769 : }
770 0 : await stopMediaStream(localScreenshareStream?.stream);
771 0 : await _removeScreenshareStream(groupCall, localScreenshareStream!);
772 0 : _localScreenshareStream = null;
773 :
774 0 : await groupCall.sendMemberStateEvent();
775 :
776 0 : groupCall.onGroupCallEvent
777 0 : .add(GroupCallStateChange.localMuteStateChanged);
778 : return;
779 : }
780 : }
781 :
782 0 : @override
783 : Future<void> dispose(GroupCallSession groupCall) async {
784 0 : if (localUserMediaStream != null) {
785 0 : await _removeUserMediaStream(groupCall, localUserMediaStream!);
786 0 : _localUserMediaStream = null;
787 : }
788 :
789 0 : if (localScreenshareStream != null) {
790 0 : await stopMediaStream(localScreenshareStream!.stream);
791 0 : await _removeScreenshareStream(groupCall, localScreenshareStream!);
792 0 : _localScreenshareStream = null;
793 : }
794 :
795 : // removeCall removes it from `_callSessions` later.
796 0 : final callsCopy = _callSessions.toList();
797 :
798 0 : for (final call in callsCopy) {
799 0 : await _removeCall(groupCall, call, CallErrorCode.userHangup);
800 : }
801 :
802 0 : _activeSpeaker = null;
803 0 : _activeSpeakerLoopTimeout?.cancel();
804 0 : await _callSubscription?.cancel();
805 : }
806 :
807 0 : @override
808 : bool get isLocalVideoMuted {
809 0 : if (localUserMediaStream != null) {
810 0 : return localUserMediaStream!.isVideoMuted();
811 : }
812 :
813 : return true;
814 : }
815 :
816 0 : @override
817 : bool get isMicrophoneMuted {
818 0 : if (localUserMediaStream != null) {
819 0 : return localUserMediaStream!.isAudioMuted();
820 : }
821 :
822 : return true;
823 : }
824 :
825 0 : @override
826 : Future<void> setupP2PCallsWithExistingMembers(
827 : GroupCallSession groupCall,
828 : ) async {
829 0 : for (final call in _callSessions) {
830 0 : await _onIncomingCall(groupCall, call);
831 : }
832 :
833 0 : _callSubscription = groupCall.voip.onIncomingCall.stream.listen(
834 0 : (newCall) => _onIncomingCall(groupCall, newCall),
835 : );
836 :
837 0 : _onActiveSpeakerLoop(groupCall);
838 : }
839 :
840 0 : @override
841 : Future<void> setupP2PCallWithNewMember(
842 : GroupCallSession groupCall,
843 : CallParticipant rp,
844 : CallMembership mem,
845 : ) async {
846 0 : final existingCall = _getCallForParticipant(groupCall, rp);
847 : if (existingCall != null) {
848 0 : if (existingCall.remoteSessionId != mem.membershipId) {
849 0 : await existingCall.hangup(reason: CallErrorCode.unknownError);
850 : } else {
851 0 : Logs().e(
852 0 : '[VOIP] onMemberStateChanged Not updating _participants list, already have a ongoing call with ${rp.id}',
853 : );
854 : return;
855 : }
856 : }
857 :
858 : // Only initiate a call with a participant who has a id that is lexicographically
859 : // less than your own. Otherwise, that user will call you.
860 0 : if (groupCall.localParticipant!.id.compareTo(rp.id) > 0) {
861 0 : Logs().i('[VOIP] Waiting for ${rp.id} to send call invite.');
862 : return;
863 : }
864 :
865 0 : final opts = CallOptions(
866 0 : callId: genCallID(),
867 0 : room: groupCall.room,
868 0 : voip: groupCall.voip,
869 : dir: CallDirection.kOutgoing,
870 0 : localPartyId: groupCall.voip.currentSessionId,
871 0 : groupCallId: groupCall.groupCallId,
872 : type: CallType.kVideo,
873 0 : iceServers: await groupCall.voip.getIceServers(),
874 : );
875 0 : final newCall = groupCall.voip.createNewCall(opts);
876 :
877 : /// both invitee userId and deviceId are set here because there can be
878 : /// multiple devices from same user in a call, so we specifiy who the
879 : /// invite is for
880 : ///
881 : /// MOVE TO CREATENEWCALL?
882 0 : newCall.remoteUserId = mem.userId;
883 0 : newCall.remoteDeviceId = mem.deviceId;
884 : // party id set to when answered
885 0 : newCall.remoteSessionId = mem.membershipId;
886 :
887 0 : await newCall.placeCallWithStreams(
888 0 : _getLocalStreams(),
889 0 : requestScreenSharing: mem.feeds?.any(
890 0 : (element) =>
891 0 : element['purpose'] == SDPStreamMetadataPurpose.Screenshare,
892 : ) ??
893 : false,
894 : );
895 :
896 0 : await _addCall(groupCall, newCall);
897 : }
898 :
899 0 : @override
900 : List<Map<String, String>>? getCurrentFeeds() {
901 0 : return _getLocalStreams()
902 0 : .map(
903 0 : (feed) => ({
904 0 : 'purpose': feed.purpose,
905 : }),
906 : )
907 0 : .toList();
908 : }
909 :
910 0 : @override
911 : bool operator ==(Object other) =>
912 0 : identical(this, other) || (other is MeshBackend && type == other.type);
913 0 : @override
914 0 : int get hashCode => type.hashCode;
915 :
916 : /// get everything is livekit specific mesh calls shouldn't be affected by these
917 0 : @override
918 : Future<void> onCallEncryption(
919 : GroupCallSession groupCall,
920 : String userId,
921 : String deviceId,
922 : Map<String, dynamic> content,
923 : ) async {
924 : return;
925 : }
926 :
927 0 : @override
928 : Future<void> onCallEncryptionKeyRequest(
929 : GroupCallSession groupCall,
930 : String userId,
931 : String deviceId,
932 : Map<String, dynamic> content,
933 : ) async {
934 : return;
935 : }
936 :
937 0 : @override
938 : Future<void> onLeftParticipant(
939 : GroupCallSession groupCall,
940 : List<CallParticipant> anyLeft,
941 : ) async {
942 : return;
943 : }
944 :
945 0 : @override
946 : Future<void> onNewParticipant(
947 : GroupCallSession groupCall,
948 : List<CallParticipant> anyJoined,
949 : ) async {
950 : return;
951 : }
952 :
953 0 : @override
954 : Future<void> requestEncrytionKey(
955 : GroupCallSession groupCall,
956 : List<CallParticipant> remoteParticipants,
957 : ) async {
958 : return;
959 : }
960 :
961 0 : @override
962 : Future<void> preShareKey(GroupCallSession groupCall) async {
963 : return;
964 : }
965 : }
|