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

Generated by: LCOV version 1.14