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