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