fix: move expires_ts according to spec (breaks group call compatibility with older sdks)

this fixes group calls with element calls
This commit is contained in:
td 2023-01-31 19:17:29 +05:30
parent 47a8e32c57
commit bdf2c01a5a
No known key found for this signature in database
GPG Key ID: F6D9E9BF14C7D103
2 changed files with 80 additions and 55 deletions

View File

@ -19,6 +19,7 @@
import 'dart:async';
import 'dart:core';
import 'package:collection/collection.dart';
import 'package:webrtc_interface/webrtc_interface.dart';
import 'package:matrix/matrix.dart';
@ -92,10 +93,14 @@ class IGroupCallRoomMemberFeed {
class IGroupCallRoomMemberDevice {
String? device_id;
String? session_id;
int? expires_ts;
List<IGroupCallRoomMemberFeed> feeds = [];
IGroupCallRoomMemberDevice.fromJson(Map<String, dynamic> json) {
device_id = json['device_id'];
session_id = json['session_id'];
expires_ts = json['expires_ts'];
if (json['feeds'] != null) {
feeds = (json['feeds'] as List<dynamic>)
.map((feed) => IGroupCallRoomMemberFeed.fromJson(feed))
@ -107,6 +112,7 @@ class IGroupCallRoomMemberDevice {
final data = <String, dynamic>{};
data['device_id'] = device_id;
data['session_id'] = session_id;
data['expires_ts'] = expires_ts;
data['feeds'] = feeds.map((feed) => feed.toJson()).toList();
return data;
}
@ -116,7 +122,7 @@ class IGroupCallRoomMemberCallState {
String? call_id;
List<String>? foci;
List<IGroupCallRoomMemberDevice> devices = [];
IGroupCallRoomMemberCallState.formJson(Map<String, dynamic> json) {
IGroupCallRoomMemberCallState.fromJson(Map<String, dynamic> json) {
call_id = json['m.call_id'];
if (json['m.foci'] != null) {
foci = (json['m.foci'] as List<dynamic>).cast<String>();
@ -141,17 +147,12 @@ class IGroupCallRoomMemberCallState {
}
class IGroupCallRoomMemberState {
final DEFAULT_EXPIRE_TS = Duration(seconds: 300);
late int expireTs;
List<IGroupCallRoomMemberCallState> calls = [];
IGroupCallRoomMemberState.fromJson(MatrixEvent event) {
if (event.content['m.calls'] != null) {
(event.content['m.calls'] as List<dynamic>).forEach(
(call) => calls.add(IGroupCallRoomMemberCallState.formJson(call)));
(call) => calls.add(IGroupCallRoomMemberCallState.fromJson(call)));
}
expireTs = event.content['m.expires_ts'] ??
event.originServerTs.add(DEFAULT_EXPIRE_TS).millisecondsSinceEpoch;
}
}
@ -264,15 +265,10 @@ class GroupCall {
return room.unsafeGetUserFromMemoryOrFallback(client.userID!);
}
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 voip.callMemberStateIsExpired(event, groupCallId) ? null : event;
}
return null;
}
@ -283,7 +279,7 @@ class GroupCall {
roomStates.sort((a, b) => a.originServerTs.compareTo(b.originServerTs));
roomStates.forEach((value) {
if (value.type == EventTypes.GroupCallMemberPrefix &&
!callMemberStateIsExpired(value)) {
!voip.callMemberStateIsExpired(value, groupCallId)) {
events.add(value);
}
});
@ -685,12 +681,17 @@ class GroupCall {
Future<void> sendMemberStateEvent() async {
final deviceId = client.deviceID;
await updateMemberCallState(IGroupCallRoomMemberCallState.formJson({
await updateMemberCallState(
IGroupCallRoomMemberCallState.fromJson(
{
'm.call_id': groupCallId,
'm.devices': [
{
'device_id': deviceId,
'session_id': client.groupCallSessionId,
'expires_ts': DateTime.now()
.add(expireTsBumpDuration)
.millisecondsSinceEpoch,
'feeds': getLocalStreams()
.map((feed) => ({
'purpose': feed.purpose,
@ -700,7 +701,9 @@ class GroupCall {
},
],
// TODO 'm.foci'
}));
},
),
);
if (resendMemberStateEventTimer != null) {
resendMemberStateEventTimer!.cancel();
@ -726,7 +729,6 @@ class GroupCall {
final localUserId = client.userID;
final currentStateEvent = getMemberStateEvent(localUserId!);
final eventContent = currentStateEvent?.content ?? {};
var calls = <IGroupCallRoomMemberCallState>[];
if (currentStateEvent != null) {
@ -750,9 +752,6 @@ class GroupCall {
}
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(
@ -1153,17 +1152,27 @@ class GroupCall {
statsReport
.removeWhere((element) => !element.values.containsKey('audioLevel'));
// https://www.w3.org/TR/webrtc-stats/#summary
final otherPartyAudioLevel = statsReport
.singleWhereOrNull((element) =>
element.type == 'inbound-rtp' &&
element.values['kind'] == 'audio')
?.values['audioLevel'];
if (otherPartyAudioLevel != null) {
audioLevelsMap[callFeed.userId] = otherPartyAudioLevel;
}
// https://www.w3.org/TR/webrtc-stats/#dom-rtcstatstype-media-source
// firefox does not seem to have this though. Works on chrome and android
audioLevelsMap[client.userID!] = statsReport
.lastWhere((element) =>
final ownAudioLevel = statsReport
.singleWhereOrNull((element) =>
element.type == 'media-source' &&
element.values['kind'] == 'audio')
.values['audioLevel'];
// works everywhere?
audioLevelsMap[callFeed.userId] = statsReport
.lastWhere((element) => element.type == 'inbound-rtp')
.values['audioLevel'];
?.values['audioLevel'];
if (ownAudioLevel != null &&
audioLevelsMap[client.userID] != ownAudioLevel) {
audioLevelsMap[client.userID!] = ownAudioLevel;
}
}
double maxAudioLevel = double.negativeInfinity;

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:core';
import 'package:collection/collection.dart';
import 'package:sdp_transform/sdp_transform.dart' as sdp_transform;
import 'package:webrtc_interface/webrtc_interface.dart';
@ -832,6 +833,23 @@ class VoIP {
static const staleCallCheckerDuration = Duration(seconds: 30);
bool callMemberStateIsExpired(
MatrixEvent groupCallMemberStateEvent, String groupCallId) {
final callMemberState =
IGroupCallRoomMemberState.fromJson(groupCallMemberStateEvent);
final calls = callMemberState.calls;
if (calls.isNotEmpty) {
final call =
calls.singleWhereOrNull((call) => call.call_id == groupCallId);
if (call != null) {
return call.devices.where((device) => device.expires_ts != null).every(
(device) =>
device.expires_ts! < DateTime.now().millisecondsSinceEpoch);
}
}
return true;
}
/// 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 {
@ -855,28 +873,26 @@ class VoIP {
'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 = {};
bool callExpired = true; // assume call is expired
final callMemberEvents = room.states.tryGetMap<String, Event>(
EventTypes.GroupCallMemberPrefix);
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;
}
});
}
for (var i = 0; i < callMemberEvents.length; i++) {
final groupCallMemberEventMap =
callMemberEvents.entries.toList()[i];
if (!participants.values.any((expire_ts) =>
expire_ts > DateTime.now().millisecondsSinceEpoch)) {
final groupCallMemberEvent =
groupCallMemberEventMap.value;
callExpired = callMemberStateIsExpired(
groupCallMemberEvent, groupCallId);
// no need to iterate further even if one participant says call isn't expired
if (!callExpired) break;
}
}
if (callExpired) {
Logs().i(
'Group call with expired timestamps detected, terminating');
'Group call with only expired timestamps detected, terminating');
await sendGroupCallTerminateEvent(room, groupCallId);
}
}