LCOV - code coverage report
Current view: top level - lib/src/voip/backend - mesh_backend.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 4 380 1.1 %
Date: 2024-07-12 20:20:16 Functions: 0 0 -

          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             : }

Generated by: LCOV version 1.14