LCOV - code coverage report
Current view: top level - lib/src/voip - group_call_session.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 41 129 31.8 %
Date: 2024-09-28 12:47:43 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           6 :   CallParticipant? get localParticipant => voip.localParticipant;
      49             : 
      50           0 :   List<CallParticipant> get participants => List.unmodifiable(_participants);
      51             :   final Set<CallParticipant> _participants = {};
      52             : 
      53             :   String groupCallId;
      54             : 
      55             :   final CachedStreamController<GroupCallState> onGroupCallState =
      56             :       CachedStreamController();
      57             : 
      58             :   final CachedStreamController<GroupCallStateChange> onGroupCallEvent =
      59             :       CachedStreamController();
      60             : 
      61             :   final CachedStreamController<MatrixRTCCallEvent> matrixRTCEventStream =
      62             :       CachedStreamController();
      63             : 
      64             :   Timer? _resendMemberStateEventTimer;
      65             : 
      66           0 :   factory GroupCallSession.withAutoGenId(
      67             :     Room room,
      68             :     VoIP voip,
      69             :     CallBackend backend,
      70             :     String? application,
      71             :     String? scope,
      72             :     String? groupCallId,
      73             :   ) {
      74           0 :     return GroupCallSession(
      75           0 :       client: room.client,
      76             :       room: room,
      77             :       voip: voip,
      78             :       backend: backend,
      79             :       application: application ?? 'm.call',
      80             :       scope: scope ?? 'm.room',
      81           0 :       groupCallId: groupCallId ?? genCallID(),
      82             :     );
      83             :   }
      84             : 
      85           2 :   GroupCallSession({
      86             :     required this.client,
      87             :     required this.room,
      88             :     required this.voip,
      89             :     required this.backend,
      90             :     required this.groupCallId,
      91             :     required this.application,
      92             :     required this.scope,
      93             :   });
      94             : 
      95           0 :   String get avatarName =>
      96           0 :       _getUser().calcDisplayname(mxidLocalPartFallback: false);
      97             : 
      98           0 :   String? get displayName => _getUser().displayName;
      99             : 
     100           0 :   User _getUser() {
     101           0 :     return room.unsafeGetUserFromMemoryOrFallback(client.userID!);
     102             :   }
     103             : 
     104           0 :   void setState(GroupCallState newState) {
     105           0 :     state = newState;
     106           0 :     onGroupCallState.add(newState);
     107           0 :     onGroupCallEvent.add(GroupCallStateChange.groupCallStateChanged);
     108             :   }
     109             : 
     110           0 :   bool hasLocalParticipant() {
     111           0 :     return _participants.contains(localParticipant);
     112             :   }
     113             : 
     114             :   /// enter the group call.
     115           0 :   Future<void> enter({WrappedMediaStream? stream}) async {
     116           0 :     if (!(state == GroupCallState.localCallFeedUninitialized ||
     117           0 :         state == GroupCallState.localCallFeedInitialized)) {
     118           0 :       throw MatrixSDKVoipException('Cannot enter call in the $state state');
     119             :     }
     120             : 
     121           0 :     if (state == GroupCallState.localCallFeedUninitialized) {
     122           0 :       await backend.initLocalStream(this, stream: stream);
     123             :     }
     124             : 
     125           0 :     await sendMemberStateEvent();
     126             : 
     127           0 :     setState(GroupCallState.entered);
     128             : 
     129           0 :     Logs().v('Entered group call $groupCallId');
     130             : 
     131             :     // Set up _participants for the members currently in the call.
     132             :     // Other members will be picked up by the RoomState.members event.
     133           0 :     await onMemberStateChanged();
     134             : 
     135           0 :     await backend.setupP2PCallsWithExistingMembers(this);
     136             : 
     137           0 :     voip.currentGroupCID = VoipId(roomId: room.id, callId: groupCallId);
     138             : 
     139           0 :     await voip.delegate.handleNewGroupCall(this);
     140             :   }
     141             : 
     142           0 :   Future<void> leave() async {
     143           0 :     await removeMemberStateEvent();
     144           0 :     await backend.dispose(this);
     145           0 :     setState(GroupCallState.localCallFeedUninitialized);
     146           0 :     voip.currentGroupCID = null;
     147           0 :     _participants.clear();
     148           0 :     voip.groupCalls.remove(VoipId(roomId: room.id, callId: groupCallId));
     149           0 :     await voip.delegate.handleGroupCallEnded(this);
     150           0 :     _resendMemberStateEventTimer?.cancel();
     151           0 :     setState(GroupCallState.ended);
     152             :   }
     153             : 
     154           0 :   Future<void> sendMemberStateEvent() async {
     155           0 :     await room.updateFamedlyCallMemberStateEvent(
     156           0 :       CallMembership(
     157           0 :         userId: client.userID!,
     158           0 :         roomId: room.id,
     159           0 :         callId: groupCallId,
     160           0 :         application: application,
     161           0 :         scope: scope,
     162           0 :         backend: backend,
     163           0 :         deviceId: client.deviceID!,
     164           0 :         expiresTs: DateTime.now()
     165           0 :             .add(CallTimeouts.expireTsBumpDuration)
     166           0 :             .millisecondsSinceEpoch,
     167           0 :         membershipId: voip.currentSessionId,
     168           0 :         feeds: backend.getCurrentFeeds(),
     169             :       ),
     170             :     );
     171             : 
     172           0 :     if (_resendMemberStateEventTimer != null) {
     173           0 :       _resendMemberStateEventTimer!.cancel();
     174             :     }
     175           0 :     _resendMemberStateEventTimer = Timer.periodic(
     176           0 :         CallTimeouts.updateExpireTsTimerDuration, ((timer) async {
     177           0 :       Logs().d('sendMemberStateEvent updating member event with timer');
     178           0 :       if (state != GroupCallState.ended ||
     179           0 :           state != GroupCallState.localCallFeedUninitialized) {
     180           0 :         await sendMemberStateEvent();
     181             :       } else {
     182           0 :         Logs().d(
     183           0 :             '[VOIP] deteceted groupCall in state $state, removing state event');
     184           0 :         await removeMemberStateEvent();
     185             :       }
     186             :     }));
     187             :   }
     188             : 
     189           0 :   Future<void> removeMemberStateEvent() {
     190           0 :     if (_resendMemberStateEventTimer != null) {
     191           0 :       Logs().d('resend member event timer cancelled');
     192           0 :       _resendMemberStateEventTimer!.cancel();
     193           0 :       _resendMemberStateEventTimer = null;
     194             :     }
     195           0 :     return room.removeFamedlyCallMemberEvent(
     196           0 :       groupCallId,
     197           0 :       client.deviceID!,
     198           0 :       application: application,
     199           0 :       scope: scope,
     200             :     );
     201             :   }
     202             : 
     203             :   /// compltetely rebuilds the local _participants list
     204           2 :   Future<void> onMemberStateChanged() async {
     205             :     // The member events may be received for another room, which we will ignore.
     206             :     final mems =
     207          10 :         room.getCallMembershipsFromRoom().values.expand((element) => element);
     208           4 :     final memsForCurrentGroupCall = mems.where((element) {
     209           6 :       return element.callId == groupCallId &&
     210           2 :           !element.isExpired &&
     211           6 :           element.application == application &&
     212           6 :           element.scope == scope &&
     213           8 :           element.roomId == room.id; // sanity checks
     214           2 :     }).toList();
     215             : 
     216             :     final ignoredMems =
     217           6 :         mems.where((element) => !memsForCurrentGroupCall.contains(element));
     218             : 
     219           4 :     for (final mem in ignoredMems) {
     220           4 :       Logs().v(
     221          10 :           '[VOIP] Ignored ${mem.userId}\'s mem event ${mem.toJson()} while updating _participants list for callId: $groupCallId, expiry status: ${mem.isExpired}');
     222             :     }
     223             : 
     224             :     final Set<CallParticipant> newP = {};
     225             : 
     226           4 :     for (final mem in memsForCurrentGroupCall) {
     227           2 :       final rp = CallParticipant(
     228           2 :         voip,
     229           2 :         userId: mem.userId,
     230           2 :         deviceId: mem.deviceId,
     231             :       );
     232             : 
     233           2 :       newP.add(rp);
     234             : 
     235           2 :       if (rp.isLocal) continue;
     236             : 
     237           4 :       if (state != GroupCallState.entered) {
     238           4 :         Logs().w(
     239           4 :             '[VOIP] onMemberStateChanged groupCall state is currently $state, skipping member update');
     240             :         continue;
     241             :       }
     242             : 
     243           0 :       await backend.setupP2PCallWithNewMember(this, rp, mem);
     244             :     }
     245           2 :     final newPcopy = Set<CallParticipant>.from(newP);
     246           4 :     final oldPcopy = Set<CallParticipant>.from(_participants);
     247           2 :     final anyJoined = newPcopy.difference(oldPcopy);
     248           2 :     final anyLeft = oldPcopy.difference(newPcopy);
     249             : 
     250           4 :     if (anyJoined.isNotEmpty || anyLeft.isNotEmpty) {
     251           2 :       if (anyJoined.isNotEmpty) {
     252           2 :         final nonLocalAnyJoined = Set<CallParticipant>.from(anyJoined)
     253           4 :           ..remove(localParticipant);
     254           6 :         if (nonLocalAnyJoined.isNotEmpty && state == GroupCallState.entered) {
     255           0 :           Logs().v(
     256           0 :               'nonLocalAnyJoined: ${nonLocalAnyJoined.map((e) => e.id).toString()} roomId: ${room.id} groupCallId: $groupCallId');
     257           0 :           await backend.onNewParticipant(this, nonLocalAnyJoined.toList());
     258             :         }
     259           4 :         _participants.addAll(anyJoined);
     260           2 :         matrixRTCEventStream
     261           6 :             .add(ParticipantsJoinEvent(participants: anyJoined.toList()));
     262             :       }
     263           2 :       if (anyLeft.isNotEmpty) {
     264           0 :         final nonLocalAnyLeft = Set<CallParticipant>.from(anyLeft)
     265           0 :           ..remove(localParticipant);
     266           0 :         if (nonLocalAnyLeft.isNotEmpty && state == GroupCallState.entered) {
     267           0 :           Logs().v(
     268           0 :               'nonLocalAnyLeft: ${nonLocalAnyLeft.map((e) => e.id).toString()} roomId: ${room.id} groupCallId: $groupCallId');
     269           0 :           await backend.onLeftParticipant(this, nonLocalAnyLeft.toList());
     270             :         }
     271           0 :         _participants.removeAll(anyLeft);
     272           0 :         matrixRTCEventStream
     273           0 :             .add(ParticipantsLeftEvent(participants: anyLeft.toList()));
     274             :       }
     275             : 
     276           4 :       onGroupCallEvent.add(GroupCallStateChange.participantsChanged);
     277           4 :       Logs().d(
     278          12 :           '[VOIP] onMemberStateChanged current list: ${_participants.map((e) => e.id).toString()}');
     279             :     }
     280             :   }
     281             : }

Generated by: LCOV version 1.14