LCOV - code coverage report
Current view: top level - lib/src/voip - group_call_session.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 31.8 % 129 41
Test Date: 2025-01-14 13:39:53 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              :       CallTimeouts.updateExpireTsTimerDuration,
     177            0 :       ((timer) async {
     178            0 :         Logs().d('sendMemberStateEvent updating member event with timer');
     179            0 :         if (state != GroupCallState.ended ||
     180            0 :             state != GroupCallState.localCallFeedUninitialized) {
     181            0 :           await sendMemberStateEvent();
     182              :         } else {
     183            0 :           Logs().d(
     184            0 :             '[VOIP] deteceted groupCall in state $state, removing state event',
     185              :           );
     186            0 :           await removeMemberStateEvent();
     187              :         }
     188              :       }),
     189              :     );
     190              :   }
     191              : 
     192            0 :   Future<void> removeMemberStateEvent() {
     193            0 :     if (_resendMemberStateEventTimer != null) {
     194            0 :       Logs().d('resend member event timer cancelled');
     195            0 :       _resendMemberStateEventTimer!.cancel();
     196            0 :       _resendMemberStateEventTimer = null;
     197              :     }
     198            0 :     return room.removeFamedlyCallMemberEvent(
     199            0 :       groupCallId,
     200            0 :       client.deviceID!,
     201            0 :       application: application,
     202            0 :       scope: scope,
     203              :     );
     204              :   }
     205              : 
     206              :   /// compltetely rebuilds the local _participants list
     207            2 :   Future<void> onMemberStateChanged() async {
     208              :     // The member events may be received for another room, which we will ignore.
     209              :     final mems =
     210           10 :         room.getCallMembershipsFromRoom().values.expand((element) => element);
     211            4 :     final memsForCurrentGroupCall = mems.where((element) {
     212            6 :       return element.callId == groupCallId &&
     213            2 :           !element.isExpired &&
     214            6 :           element.application == application &&
     215            6 :           element.scope == scope &&
     216            8 :           element.roomId == room.id; // sanity checks
     217            2 :     }).toList();
     218              : 
     219              :     final ignoredMems =
     220            6 :         mems.where((element) => !memsForCurrentGroupCall.contains(element));
     221              : 
     222            4 :     for (final mem in ignoredMems) {
     223            4 :       Logs().v(
     224           10 :         '[VOIP] Ignored ${mem.userId}\'s mem event ${mem.toJson()} while updating _participants list for callId: $groupCallId, expiry status: ${mem.isExpired}',
     225              :       );
     226              :     }
     227              : 
     228              :     final Set<CallParticipant> newP = {};
     229              : 
     230            4 :     for (final mem in memsForCurrentGroupCall) {
     231            2 :       final rp = CallParticipant(
     232            2 :         voip,
     233            2 :         userId: mem.userId,
     234            2 :         deviceId: mem.deviceId,
     235              :       );
     236              : 
     237            2 :       newP.add(rp);
     238              : 
     239            2 :       if (rp.isLocal) continue;
     240              : 
     241            4 :       if (state != GroupCallState.entered) {
     242            4 :         Logs().w(
     243            4 :           '[VOIP] onMemberStateChanged groupCall state is currently $state, skipping member update',
     244              :         );
     245              :         continue;
     246              :       }
     247              : 
     248            0 :       await backend.setupP2PCallWithNewMember(this, rp, mem);
     249              :     }
     250            2 :     final newPcopy = Set<CallParticipant>.from(newP);
     251            4 :     final oldPcopy = Set<CallParticipant>.from(_participants);
     252            2 :     final anyJoined = newPcopy.difference(oldPcopy);
     253            2 :     final anyLeft = oldPcopy.difference(newPcopy);
     254              : 
     255            4 :     if (anyJoined.isNotEmpty || anyLeft.isNotEmpty) {
     256            2 :       if (anyJoined.isNotEmpty) {
     257            2 :         final nonLocalAnyJoined = Set<CallParticipant>.from(anyJoined)
     258            4 :           ..remove(localParticipant);
     259            6 :         if (nonLocalAnyJoined.isNotEmpty && state == GroupCallState.entered) {
     260            0 :           Logs().v(
     261            0 :             'nonLocalAnyJoined: ${nonLocalAnyJoined.map((e) => e.id).toString()} roomId: ${room.id} groupCallId: $groupCallId',
     262              :           );
     263            0 :           await backend.onNewParticipant(this, nonLocalAnyJoined.toList());
     264              :         }
     265            4 :         _participants.addAll(anyJoined);
     266            2 :         matrixRTCEventStream
     267            6 :             .add(ParticipantsJoinEvent(participants: anyJoined.toList()));
     268              :       }
     269            2 :       if (anyLeft.isNotEmpty) {
     270            0 :         final nonLocalAnyLeft = Set<CallParticipant>.from(anyLeft)
     271            0 :           ..remove(localParticipant);
     272            0 :         if (nonLocalAnyLeft.isNotEmpty && state == GroupCallState.entered) {
     273            0 :           Logs().v(
     274            0 :             'nonLocalAnyLeft: ${nonLocalAnyLeft.map((e) => e.id).toString()} roomId: ${room.id} groupCallId: $groupCallId',
     275              :           );
     276            0 :           await backend.onLeftParticipant(this, nonLocalAnyLeft.toList());
     277              :         }
     278            0 :         _participants.removeAll(anyLeft);
     279            0 :         matrixRTCEventStream
     280            0 :             .add(ParticipantsLeftEvent(participants: anyLeft.toList()));
     281              :       }
     282              : 
     283            4 :       onGroupCallEvent.add(GroupCallStateChange.participantsChanged);
     284            4 :       Logs().d(
     285           12 :         '[VOIP] onMemberStateChanged current list: ${_participants.map((e) => e.id).toString()}',
     286              :       );
     287              :     }
     288              :   }
     289              : }
        

Generated by: LCOV version 2.0-1