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 379 1.1 %
Date: 2024-05-13 12:56:47 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 :       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             : }

Generated by: LCOV version 1.14