LCOV - code coverage report
Current view: top level - lib/src/voip - group_call_session.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 5 122 4.1 %
Date: 2024-05-13 12:56:47 Functions: 0 0 -

          Line data    Source code
       1             : /*
       2             :  *   Famedly Matrix SDK
       3             :  *   Copyright (C) 2021 Famedly GmbH
       4             :  *
       5             :  *   This program is free software: you can redistribute it and/or modify
       6             :  *   it under the terms of the GNU Affero General License as
       7             :  *   published by the Free Software Foundation, either version 3 of the
       8             :  *   License, or (at your option) any later version.
       9             :  *
      10             :  *   This program is distributed in the hope that it will be useful,
      11             :  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
      12             :  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      13             :  *   GNU Affero General License for more details.
      14             :  *
      15             :  *   You should have received a copy of the GNU Affero General License
      16             :  *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
      17             :  */
      18             : 
      19             : import 'dart:async';
      20             : import 'dart:core';
      21             : 
      22             : import 'package:matrix/matrix.dart';
      23             : import 'package:matrix/src/utils/cached_stream_controller.dart';
      24             : import 'package:matrix/src/voip/models/call_membership.dart';
      25             : import 'package:matrix/src/voip/models/voip_id.dart';
      26             : import 'package:matrix/src/voip/utils/stream_helper.dart';
      27             : 
      28             : /// Holds methods for managing a group call. This class is also responsible for
      29             : /// holding and managing the individual `CallSession`s in a group call.
      30             : class GroupCallSession {
      31             :   // Config
      32             :   final Client client;
      33             :   final VoIP voip;
      34             :   final Room room;
      35             : 
      36             :   /// is a list of backend to allow passing multiple backend in the future
      37             :   /// we use the first backend everywhere as of now
      38             :   final CallBackend backend;
      39             : 
      40             :   /// something like normal calls or thirdroom
      41             :   final String? application;
      42             : 
      43             :   /// either room scoped or user scoped calls
      44             :   final String? scope;
      45             : 
      46             :   GroupCallState state = GroupCallState.localCallFeedUninitialized;
      47             : 
      48           0 :   CallParticipant? get localParticipant => voip.localParticipant;
      49             : 
      50           0 :   List<CallParticipant> get participants => List.unmodifiable(_participants);
      51             :   final List<CallParticipant> _participants = [];
      52             : 
      53             :   String groupCallId;
      54             : 
      55             :   final CachedStreamController<GroupCallState> onGroupCallState =
      56             :       CachedStreamController();
      57             : 
      58             :   final CachedStreamController<GroupCallStateChange> onGroupCallEvent =
      59             :       CachedStreamController();
      60             : 
      61             :   Timer? _resendMemberStateEventTimer;
      62             : 
      63           0 :   factory GroupCallSession.withAutoGenId(
      64             :     Room room,
      65             :     VoIP voip,
      66             :     CallBackend backend,
      67             :     String? application,
      68             :     String? scope,
      69             :     String? groupCallId,
      70             :   ) {
      71           0 :     return GroupCallSession(
      72           0 :       client: room.client,
      73             :       room: room,
      74             :       voip: voip,
      75             :       backend: backend,
      76             :       application: application ?? 'm.call',
      77             :       scope: scope ?? 'm.room',
      78           0 :       groupCallId: groupCallId ?? genCallID(),
      79             :     );
      80             :   }
      81             : 
      82           2 :   GroupCallSession({
      83             :     required this.client,
      84             :     required this.room,
      85             :     required this.voip,
      86             :     required this.backend,
      87             :     required this.groupCallId,
      88             :     required this.application,
      89             :     required this.scope,
      90             :   });
      91             : 
      92           0 :   String get avatarName =>
      93           0 :       _getUser().calcDisplayname(mxidLocalPartFallback: false);
      94             : 
      95           0 :   String? get displayName => _getUser().displayName;
      96             : 
      97           0 :   User _getUser() {
      98           0 :     return room.unsafeGetUserFromMemoryOrFallback(client.userID!);
      99             :   }
     100             : 
     101           0 :   void setState(GroupCallState newState) {
     102           0 :     state = newState;
     103           0 :     onGroupCallState.add(newState);
     104           0 :     onGroupCallEvent.add(GroupCallStateChange.groupCallStateChanged);
     105             :   }
     106             : 
     107           0 :   bool hasLocalParticipant() {
     108           0 :     return _participants.contains(localParticipant);
     109             :   }
     110             : 
     111             :   /// enter the group call.
     112           0 :   Future<void> enter({WrappedMediaStream? stream}) async {
     113           0 :     if (!(state == GroupCallState.localCallFeedUninitialized ||
     114           0 :         state == GroupCallState.localCallFeedInitialized)) {
     115           0 :       throw Exception('Cannot enter call in the $state state');
     116             :     }
     117             : 
     118           0 :     if (state == GroupCallState.localCallFeedUninitialized) {
     119           0 :       await backend.initLocalStream(this, stream: stream);
     120             :     }
     121             : 
     122           0 :     await sendMemberStateEvent();
     123             : 
     124           0 :     setState(GroupCallState.entered);
     125             : 
     126           0 :     Logs().v('Entered group call $groupCallId');
     127             : 
     128             :     // Set up _participants for the members currently in the call.
     129             :     // Other members will be picked up by the RoomState.members event.
     130           0 :     await onMemberStateChanged();
     131             : 
     132           0 :     await backend.setupP2PCallsWithExistingMembers(this);
     133             : 
     134           0 :     voip.currentGroupCID = VoipId(roomId: room.id, callId: groupCallId);
     135             : 
     136           0 :     await voip.delegate.handleNewGroupCall(this);
     137             :   }
     138             : 
     139           0 :   Future<void> leave() async {
     140           0 :     await removeMemberStateEvent();
     141           0 :     await backend.dispose(this);
     142           0 :     setState(GroupCallState.localCallFeedUninitialized);
     143           0 :     voip.currentGroupCID = null;
     144           0 :     _participants.clear();
     145           0 :     voip.groupCalls.remove(VoipId(roomId: room.id, callId: groupCallId));
     146           0 :     await voip.delegate.handleGroupCallEnded(this);
     147           0 :     _resendMemberStateEventTimer?.cancel();
     148           0 :     setState(GroupCallState.ended);
     149             :   }
     150             : 
     151           0 :   Future<void> sendMemberStateEvent() async {
     152           0 :     await room.updateFamedlyCallMemberStateEvent(
     153           0 :       CallMembership(
     154           0 :         userId: client.userID!,
     155           0 :         roomId: room.id,
     156           0 :         callId: groupCallId,
     157           0 :         application: application,
     158           0 :         scope: scope,
     159           0 :         backend: backend,
     160           0 :         deviceId: client.deviceID!,
     161           0 :         expiresTs: DateTime.now()
     162           0 :             .add(CallTimeouts.expireTsBumpDuration)
     163           0 :             .millisecondsSinceEpoch,
     164           0 :         membershipId: voip.currentSessionId,
     165           0 :         feeds: backend.getCurrentFeeds(),
     166             :       ),
     167             :     );
     168             : 
     169           0 :     if (_resendMemberStateEventTimer != null) {
     170           0 :       _resendMemberStateEventTimer!.cancel();
     171             :     }
     172           0 :     _resendMemberStateEventTimer = Timer.periodic(
     173           0 :         CallTimeouts.updateExpireTsTimerDuration, ((timer) async {
     174           0 :       Logs().d('sendMemberStateEvent updating member event with timer');
     175           0 :       if (state != GroupCallState.ended ||
     176           0 :           state != GroupCallState.localCallFeedUninitialized) {
     177           0 :         await sendMemberStateEvent();
     178             :       } else {
     179           0 :         Logs().d(
     180           0 :             '[VOIP] deteceted groupCall in state $state, removing state event');
     181           0 :         await removeMemberStateEvent();
     182             :       }
     183             :     }));
     184             :   }
     185             : 
     186           0 :   Future<void> removeMemberStateEvent() {
     187           0 :     if (_resendMemberStateEventTimer != null) {
     188           0 :       Logs().d('resend member event timer cancelled');
     189           0 :       _resendMemberStateEventTimer!.cancel();
     190           0 :       _resendMemberStateEventTimer = null;
     191             :     }
     192           0 :     return room.removeFamedlyCallMemberEvent(
     193           0 :       groupCallId,
     194           0 :       client.deviceID!,
     195           0 :       application: application,
     196           0 :       scope: scope,
     197             :     );
     198             :   }
     199             : 
     200             :   /// compltetely rebuilds the local _participants list
     201           2 :   Future<void> onMemberStateChanged() async {
     202           4 :     if (state != GroupCallState.entered) {
     203           4 :       Logs().d(
     204           6 :           '[VOIP] early return onMemberStateChanged, group call state is not Entered. Actual state: ${state.toString()} ');
     205             :       return;
     206             :     }
     207             : 
     208             :     // The member events may be received for another room, which we will ignore.
     209             :     final mems =
     210           0 :         room.getCallMembershipsFromRoom().values.expand((element) => element);
     211           0 :     final memsForCurrentGroupCall = mems.where((element) {
     212           0 :       return element.callId == groupCallId &&
     213           0 :           !element.isExpired &&
     214           0 :           element.application == application &&
     215           0 :           element.scope == scope &&
     216           0 :           element.roomId == room.id; // sanity checks
     217           0 :     }).toList();
     218             : 
     219             :     final ignoredMems =
     220           0 :         mems.where((element) => !memsForCurrentGroupCall.contains(element));
     221             : 
     222           0 :     for (final mem in ignoredMems) {
     223           0 :       Logs().w(
     224           0 :           '[VOIP] Ignored ${mem.userId}\'s mem event ${mem.toJson()} while updating _participants list for callId: $groupCallId, expiry status: ${mem.isExpired}');
     225             :     }
     226             : 
     227           0 :     final List<CallParticipant> newP = [];
     228             : 
     229           0 :     for (final mem in memsForCurrentGroupCall) {
     230           0 :       final rp = CallParticipant(
     231           0 :         voip,
     232           0 :         userId: mem.userId,
     233           0 :         deviceId: mem.deviceId,
     234             :       );
     235             : 
     236           0 :       newP.add(rp);
     237             : 
     238           0 :       if (rp.isLocal) continue;
     239             : 
     240           0 :       if (state != GroupCallState.entered) {
     241           0 :         Logs().w(
     242           0 :             '[VOIP] onMemberStateChanged groupCall state is currently $state, skipping member update');
     243             :         continue;
     244             :       }
     245             : 
     246           0 :       await backend.setupP2PCallWithNewMember(this, rp, mem);
     247             :     }
     248           0 :     final newPcopy = List<CallParticipant>.from(newP);
     249           0 :     final oldPcopy = List<CallParticipant>.from(_participants);
     250           0 :     final anyJoined = newPcopy.where((element) => !oldPcopy.contains(element));
     251           0 :     final anyLeft = oldPcopy.where((element) => !newPcopy.contains(element));
     252             : 
     253           0 :     if (anyJoined.isNotEmpty || anyLeft.isNotEmpty) {
     254           0 :       if (anyJoined.isNotEmpty) {
     255           0 :         Logs().d('anyJoined: ${anyJoined.map((e) => e.id).toString()}');
     256           0 :         _participants.addAll(anyJoined);
     257           0 :         await backend.onNewParticipant(this, anyJoined.toList());
     258             :       }
     259           0 :       if (anyLeft.isNotEmpty) {
     260           0 :         Logs().d('anyLeft: ${anyLeft.map((e) => e.id).toString()}');
     261           0 :         for (final leftp in anyLeft) {
     262           0 :           _participants.remove(leftp);
     263             :         }
     264           0 :         await backend.onLeftParticipant(this, anyLeft.toList());
     265             :       }
     266             : 
     267           0 :       onGroupCallEvent.add(GroupCallStateChange.participantsChanged);
     268           0 :       Logs().d(
     269           0 :           '[VOIP] onMemberStateChanged current list: ${_participants.map((e) => e.id).toString()}');
     270             :     }
     271             :   }
     272             : }

Generated by: LCOV version 1.14