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 : }
|