feat: implement expire_ts in group calls and provide methods to terminate stale calls

This commit is contained in:
td 2022-10-07 15:08:38 +05:30
parent ddb7cf8e30
commit da4174c91a
No known key found for this signature in database
GPG Key ID: F6D9E9BF14C7D103
2 changed files with 180 additions and 47 deletions

View File

@ -141,12 +141,17 @@ class IGroupCallRoomMemberCallState {
}
class IGroupCallRoomMemberState {
final DEFAULT_EXPIRE_TS = Duration(seconds: 300);
late int expireTs;
List<IGroupCallRoomMemberCallState> calls = [];
IGroupCallRoomMemberState.fromJson(Map<String, dynamic> json) {
if (json['m.calls'] != null) {
(json['m.calls'] as List<dynamic>).forEach(
IGroupCallRoomMemberState.fromJson(MatrixEvent event) {
if (event.content['m.calls'] != null) {
(event.content['m.calls'] as List<dynamic>).forEach(
(call) => calls.add(IGroupCallRoomMemberCallState.formJson(call)));
}
expireTs = event.content['m.expires_ts'] ??
event.originServerTs.add(DEFAULT_EXPIRE_TS).millisecondsSinceEpoch;
}
}
@ -168,6 +173,10 @@ abstract class ICallHandlers {
class GroupCall {
// Config
static const updateExpireTsTimerDuration = Duration(seconds: 15);
static const expireTsBumpDuration = Duration(seconds: 45);
var activeSpeakerInterval = 1000;
var retryCallInterval = 5000;
var participantTimeout = 1000 * 15;
@ -197,6 +206,8 @@ class GroupCall {
Timer? activeSpeakerLoopTimeout;
Timer? resendMemberStateEventTimer;
final CachedStreamController<GroupCall> onGroupCallFeedsChanged =
CachedStreamController();
@ -255,30 +266,32 @@ class GroupCall {
return room.unsafeGetUserFromMemoryOrFallback(client.userID!);
}
Future<List<MatrixEvent>> getStateEventsList(String type) async {
bool callMemberStateIsExpired(MatrixEvent event) {
final callMemberState = IGroupCallRoomMemberState.fromJson(event);
return callMemberState.expireTs < DateTime.now().millisecondsSinceEpoch;
}
Event? getMemberStateEvent(String userId) {
final event = room.getState(EventTypes.GroupCallMemberPrefix, userId);
if (event != null) {
return callMemberStateIsExpired(event) ? null : event;
}
return null;
}
Future<List<MatrixEvent>> getAllMemberStateEvents() async {
final List<MatrixEvent> events = [];
final roomStates = await client.getRoomState(room.id);
roomStates.sort((a, b) => a.originServerTs.compareTo(b.originServerTs));
final events = <MatrixEvent>[];
roomStates.forEach((evt) {
if (evt.type == type) {
events.add(evt);
roomStates.forEach((value) {
if (value.type == EventTypes.GroupCallMemberPrefix &&
!callMemberStateIsExpired(value)) {
events.add(value);
}
});
return events;
}
Future<MatrixEvent?> getStateEvent(String type, [String? userId]) async {
final roomStates = await client.getRoomState(room.id);
roomStates.sort((a, b) => a.originServerTs.compareTo(b.originServerTs));
MatrixEvent? event;
roomStates.forEach((value) {
if (value.type == type && (userId == null || value.senderId == userId)) {
event = value;
}
});
return event;
}
void setState(String newState) {
state = newState;
onGroupCallEvent.add(GroupCallEvent.GroupCallStateChanged);
@ -420,8 +433,7 @@ class GroupCall {
// Set up participants for the members currently in the room.
// Other members will be picked up by the RoomState.members event.
final memberStateEvents =
await getStateEventsList(EventTypes.GroupCallMemberPrefix);
final memberStateEvents = await getAllMemberStateEvents();
memberStateEvents.forEach((stateEvent) {
onMemberStateChanged(stateEvent);
@ -470,33 +482,34 @@ class GroupCall {
setState(GroupCallState.LocalCallFeedUninitialized);
voip.currentGroupCID = null;
voip.delegate.handleGroupCallEnded(this);
final justLeftGroupCall = voip.groupCalls.tryGet<GroupCall>(room.id);
// terminate group call if empty
if (justLeftGroupCall != null &&
justLeftGroupCall.intent != 'm.room' &&
justLeftGroupCall.participants.isEmpty &&
room.canCreateGroupCall) {
terminate();
} else {
Logs().d(
'[VOIP] left group call but cannot terminate. participants: ${participants.length}, pl: ${room.canCreateGroupCall}');
}
}
/// terminate group call.
void terminate({bool emitStateEvent = true}) async {
final existingStateEvent =
room.getState(EventTypes.GroupCallPrefix, groupCallId);
dispose();
participants = [];
//TODO(duan): remove this
/* client.removeListener(
'RoomState.members',
onMemberStateChanged,
);
*/
voip.groupCalls.remove(room.id);
voip.groupCalls.remove(groupCallId);
if (emitStateEvent) {
final existingStateEvent = await getStateEvent(
EventTypes.GroupCallPrefix,
groupCallId,
);
await client.setRoomStateWithKey(
room.id, EventTypes.GroupCallPrefix, groupCallId, {
...existingStateEvent!.content,
'm.terminated': GroupCallTerminationReason.CallEnded,
});
Logs().d('[VOIP] Group call $groupCallId was killed');
}
voip.delegate.handleGroupCallEnded(this);
setState(GroupCallState.Ended);
@ -665,9 +678,9 @@ class GroupCall {
newCall.answerWithStreams(getLocalStreams());
}
Future<void> sendMemberStateEvent() {
Future<void> sendMemberStateEvent() async {
final deviceId = client.deviceID;
return updateMemberCallState(IGroupCallRoomMemberCallState.formJson({
await updateMemberCallState(IGroupCallRoomMemberCallState.formJson({
'm.call_id': groupCallId,
'm.devices': [
{
@ -683,9 +696,23 @@ class GroupCall {
],
// TODO 'm.foci'
}));
if (resendMemberStateEventTimer != null) {
resendMemberStateEventTimer!.cancel();
}
resendMemberStateEventTimer =
Timer.periodic(updateExpireTsTimerDuration, ((timer) async {
Logs().d('updating member event with timer');
return await sendMemberStateEvent();
}));
}
Future<void> removeMemberStateEvent() {
if (resendMemberStateEventTimer != null) {
Logs().d('resend member event timer cancelled');
resendMemberStateEventTimer!.cancel();
resendMemberStateEventTimer = null;
}
return updateMemberCallState();
}
@ -693,13 +720,13 @@ class GroupCall {
[IGroupCallRoomMemberCallState? memberCallState]) async {
final localUserId = client.userID;
final currentStateEvent =
await getStateEvent(EventTypes.GroupCallMemberPrefix, localUserId);
final currentStateEvent = getMemberStateEvent(localUserId!);
final eventContent = currentStateEvent?.content ?? {};
var calls = <IGroupCallRoomMemberCallState>[];
if (currentStateEvent != null) {
final memberStateEvent = IGroupCallRoomMemberState.fromJson(eventContent);
final memberStateEvent =
IGroupCallRoomMemberState.fromJson(currentStateEvent);
calls = memberStateEvent.calls;
final existingCallIndex =
calls.indexWhere((element) => groupCallId == element.call_id);
@ -716,13 +743,15 @@ class GroupCall {
} else if (memberCallState != null) {
calls.add(memberCallState);
}
final content = {
'm.calls': calls.map((e) => e.toJson()).toList(),
'm.expires_ts': calls.isEmpty
? eventContent.tryGet('m.expires_ts')
: DateTime.now().add(expireTsBumpDuration).millisecondsSinceEpoch
};
await client.setRoomStateWithKey(
room.id, EventTypes.GroupCallMemberPrefix, localUserId!, content);
room.id, EventTypes.GroupCallMemberPrefix, localUserId, content);
}
void onMemberStateChanged(MatrixEvent event) async {
@ -737,7 +766,7 @@ class GroupCall {
return;
}
final callsState = IGroupCallRoomMemberState.fromJson(event.content);
final callsState = IGroupCallRoomMemberState.fromJson(event);
if (callsState is List) {
Logs()
@ -843,14 +872,12 @@ class GroupCall {
}
Future<IGroupCallRoomMemberDevice?> getDeviceForMember(String userId) async {
final memberStateEvent =
await getStateEvent(EventTypes.GroupCallMemberPrefix, userId);
final memberStateEvent = getMemberStateEvent(userId);
if (memberStateEvent == null) {
return null;
}
final memberState =
IGroupCallRoomMemberState.fromJson(memberStateEvent.content);
final memberState = IGroupCallRoomMemberState.fromJson(memberStateEvent);
final memberGroupCallState =
memberState.calls.where(((call) => call.call_id == groupCallId));

View File

@ -757,4 +757,110 @@ class VoIP {
groupCall.onMemberStateChanged(event);
}
}
bool hasActiveCall(Room room) {
final groupCallStates =
room.states.tryGetMap<dynamic, Event>(EventTypes.GroupCallPrefix);
if (groupCallStates != null) {
groupCallStates.values
.toList()
.sort((a, b) => a.originServerTs.compareTo(b.originServerTs));
final latestGroupCallEvent = groupCallStates.values.last;
if (!latestGroupCallEvent.content.containsKey('m.terminated')) {
return true;
}
}
return false;
}
Future sendGroupCallTerminateEvent(Room room, String groupCallId) async {
try {
Logs().d('[VOIP] running sendterminator');
final existingStateEvent =
room.getState(EventTypes.GroupCallPrefix, groupCallId);
if (existingStateEvent == null) {
Logs().e('could not find group call with id $groupCallId');
return;
}
await client.setRoomStateWithKey(
room.id, EventTypes.GroupCallPrefix, groupCallId, {
...existingStateEvent.content,
'm.terminated': GroupCallTerminationReason.CallEnded,
});
Logs().d('[VOIP] Group call $groupCallId was killed uwu');
} catch (e) {
Logs().i('killing stale call $groupCallId failed. reason: $e');
}
}
Map<String, Timer> staleGroupCallsTimer = {};
/// stops the stale call checker timer
void stopStaleCallsChecker(String roomId) {
if (staleGroupCallsTimer.tryGet(roomId) != null) {
staleGroupCallsTimer[roomId]!.cancel();
} else {
Logs().w('[VOIP] no stale call checker for room found');
}
}
static const staleCallCheckerDuration = Duration(seconds: 30);
/// checks for stale calls in a room and sends `m.terminated` if all the
/// expires_ts are expired. Call when opening a room
void startStaleCallsChecker(String roomId) async {
staleGroupCallsTimer[roomId] = Timer.periodic(
staleCallCheckerDuration,
(timer) {
final room = client.getRoomById(roomId);
if (room == null) {
Logs().w('[VOIP] stale call checker got incorrect room id');
} else {
Logs().d('checking for stale group calls.');
final copyGroupCallIds =
room.states.tryGetMap<dynamic, Event>(EventTypes.GroupCallPrefix);
if (copyGroupCallIds == null) return;
copyGroupCallIds.forEach(
(groupCallId, groupCallEvent) async {
if (groupCallEvent.content.tryGet('m.intent') == 'm.room') return;
if (!groupCallEvent.content.containsKey('m.terminated')) {
if (groupCallId != null) {
Logs().i(
'found non terminated group call with id $groupCallId');
// call is not empty but check for stale participants (gone offline)
// with expire_ts
final Map<String, int> participants = {};
final callMemberEvents = room.states.tryGetMap<String, Event>(
EventTypes.GroupCallMemberPrefix);
Logs().e(
'callmemeberEvents length ${callMemberEvents?.length}');
if (callMemberEvents != null) {
callMemberEvents.forEach((userId, memberEvent) async {
final callMemberEvent = groupCallEvent.room.getState(
EventTypes.GroupCallMemberPrefix,
userId,
);
if (callMemberEvent != null) {
final event =
IGroupCallRoomMemberState.fromJson(callMemberEvent);
participants[userId] = event.expireTs;
}
});
}
Logs().e(participants.toString());
if (!participants.values.any((expire_ts) =>
expire_ts > DateTime.now().millisecondsSinceEpoch)) {
Logs().i(
'Group call with expired timestamps detected, terminating');
await sendGroupCallTerminateEvent(room, groupCallId);
}
}
}
},
);
}
},
);
}
}