Merge branch 'td/expiresTsGroupCallMove' into 'main'
fix: move expires_ts according to spec (breaks group call compatibility with older sdks) See merge request famedly/company/frontend/famedlysdk!1229
This commit is contained in:
commit
dc444538bf
|
|
@ -19,6 +19,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:core';
|
import 'dart:core';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:webrtc_interface/webrtc_interface.dart';
|
import 'package:webrtc_interface/webrtc_interface.dart';
|
||||||
|
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
@ -92,10 +93,14 @@ class IGroupCallRoomMemberFeed {
|
||||||
class IGroupCallRoomMemberDevice {
|
class IGroupCallRoomMemberDevice {
|
||||||
String? device_id;
|
String? device_id;
|
||||||
String? session_id;
|
String? session_id;
|
||||||
|
int? expires_ts;
|
||||||
|
|
||||||
List<IGroupCallRoomMemberFeed> feeds = [];
|
List<IGroupCallRoomMemberFeed> feeds = [];
|
||||||
IGroupCallRoomMemberDevice.fromJson(Map<String, dynamic> json) {
|
IGroupCallRoomMemberDevice.fromJson(Map<String, dynamic> json) {
|
||||||
device_id = json['device_id'];
|
device_id = json['device_id'];
|
||||||
session_id = json['session_id'];
|
session_id = json['session_id'];
|
||||||
|
expires_ts = json['expires_ts'];
|
||||||
|
|
||||||
if (json['feeds'] != null) {
|
if (json['feeds'] != null) {
|
||||||
feeds = (json['feeds'] as List<dynamic>)
|
feeds = (json['feeds'] as List<dynamic>)
|
||||||
.map((feed) => IGroupCallRoomMemberFeed.fromJson(feed))
|
.map((feed) => IGroupCallRoomMemberFeed.fromJson(feed))
|
||||||
|
|
@ -107,6 +112,7 @@ class IGroupCallRoomMemberDevice {
|
||||||
final data = <String, dynamic>{};
|
final data = <String, dynamic>{};
|
||||||
data['device_id'] = device_id;
|
data['device_id'] = device_id;
|
||||||
data['session_id'] = session_id;
|
data['session_id'] = session_id;
|
||||||
|
data['expires_ts'] = expires_ts;
|
||||||
data['feeds'] = feeds.map((feed) => feed.toJson()).toList();
|
data['feeds'] = feeds.map((feed) => feed.toJson()).toList();
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +122,7 @@ class IGroupCallRoomMemberCallState {
|
||||||
String? call_id;
|
String? call_id;
|
||||||
List<String>? foci;
|
List<String>? foci;
|
||||||
List<IGroupCallRoomMemberDevice> devices = [];
|
List<IGroupCallRoomMemberDevice> devices = [];
|
||||||
IGroupCallRoomMemberCallState.formJson(Map<String, dynamic> json) {
|
IGroupCallRoomMemberCallState.fromJson(Map<String, dynamic> json) {
|
||||||
call_id = json['m.call_id'];
|
call_id = json['m.call_id'];
|
||||||
if (json['m.foci'] != null) {
|
if (json['m.foci'] != null) {
|
||||||
foci = (json['m.foci'] as List<dynamic>).cast<String>();
|
foci = (json['m.foci'] as List<dynamic>).cast<String>();
|
||||||
|
|
@ -141,17 +147,12 @@ class IGroupCallRoomMemberCallState {
|
||||||
}
|
}
|
||||||
|
|
||||||
class IGroupCallRoomMemberState {
|
class IGroupCallRoomMemberState {
|
||||||
final DEFAULT_EXPIRE_TS = Duration(seconds: 300);
|
|
||||||
late int expireTs;
|
|
||||||
List<IGroupCallRoomMemberCallState> calls = [];
|
List<IGroupCallRoomMemberCallState> calls = [];
|
||||||
IGroupCallRoomMemberState.fromJson(MatrixEvent event) {
|
IGroupCallRoomMemberState.fromJson(MatrixEvent event) {
|
||||||
if (event.content['m.calls'] != null) {
|
if (event.content['m.calls'] != null) {
|
||||||
(event.content['m.calls'] as List<dynamic>).forEach(
|
(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!);
|
return room.unsafeGetUserFromMemoryOrFallback(client.userID!);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool callMemberStateIsExpired(MatrixEvent event) {
|
|
||||||
final callMemberState = IGroupCallRoomMemberState.fromJson(event);
|
|
||||||
return callMemberState.expireTs < DateTime.now().millisecondsSinceEpoch;
|
|
||||||
}
|
|
||||||
|
|
||||||
Event? getMemberStateEvent(String userId) {
|
Event? getMemberStateEvent(String userId) {
|
||||||
final event = room.getState(EventTypes.GroupCallMemberPrefix, userId);
|
final event = room.getState(EventTypes.GroupCallMemberPrefix, userId);
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
return callMemberStateIsExpired(event) ? null : event;
|
return voip.callMemberStateIsExpired(event, groupCallId) ? null : event;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -283,7 +279,7 @@ class GroupCall {
|
||||||
roomStates.sort((a, b) => a.originServerTs.compareTo(b.originServerTs));
|
roomStates.sort((a, b) => a.originServerTs.compareTo(b.originServerTs));
|
||||||
roomStates.forEach((value) {
|
roomStates.forEach((value) {
|
||||||
if (value.type == EventTypes.GroupCallMemberPrefix &&
|
if (value.type == EventTypes.GroupCallMemberPrefix &&
|
||||||
!callMemberStateIsExpired(value)) {
|
!voip.callMemberStateIsExpired(value, groupCallId)) {
|
||||||
events.add(value);
|
events.add(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -685,12 +681,17 @@ class GroupCall {
|
||||||
|
|
||||||
Future<void> sendMemberStateEvent() async {
|
Future<void> sendMemberStateEvent() async {
|
||||||
final deviceId = client.deviceID;
|
final deviceId = client.deviceID;
|
||||||
await updateMemberCallState(IGroupCallRoomMemberCallState.formJson({
|
await updateMemberCallState(
|
||||||
|
IGroupCallRoomMemberCallState.fromJson(
|
||||||
|
{
|
||||||
'm.call_id': groupCallId,
|
'm.call_id': groupCallId,
|
||||||
'm.devices': [
|
'm.devices': [
|
||||||
{
|
{
|
||||||
'device_id': deviceId,
|
'device_id': deviceId,
|
||||||
'session_id': client.groupCallSessionId,
|
'session_id': client.groupCallSessionId,
|
||||||
|
'expires_ts': DateTime.now()
|
||||||
|
.add(expireTsBumpDuration)
|
||||||
|
.millisecondsSinceEpoch,
|
||||||
'feeds': getLocalStreams()
|
'feeds': getLocalStreams()
|
||||||
.map((feed) => ({
|
.map((feed) => ({
|
||||||
'purpose': feed.purpose,
|
'purpose': feed.purpose,
|
||||||
|
|
@ -700,7 +701,9 @@ class GroupCall {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// TODO 'm.foci'
|
// TODO 'm.foci'
|
||||||
}));
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (resendMemberStateEventTimer != null) {
|
if (resendMemberStateEventTimer != null) {
|
||||||
resendMemberStateEventTimer!.cancel();
|
resendMemberStateEventTimer!.cancel();
|
||||||
|
|
@ -726,7 +729,6 @@ class GroupCall {
|
||||||
final localUserId = client.userID;
|
final localUserId = client.userID;
|
||||||
|
|
||||||
final currentStateEvent = getMemberStateEvent(localUserId!);
|
final currentStateEvent = getMemberStateEvent(localUserId!);
|
||||||
final eventContent = currentStateEvent?.content ?? {};
|
|
||||||
var calls = <IGroupCallRoomMemberCallState>[];
|
var calls = <IGroupCallRoomMemberCallState>[];
|
||||||
|
|
||||||
if (currentStateEvent != null) {
|
if (currentStateEvent != null) {
|
||||||
|
|
@ -750,9 +752,6 @@ class GroupCall {
|
||||||
}
|
}
|
||||||
final content = {
|
final content = {
|
||||||
'm.calls': calls.map((e) => e.toJson()).toList(),
|
'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(
|
await client.setRoomStateWithKey(
|
||||||
|
|
@ -1153,17 +1152,27 @@ class GroupCall {
|
||||||
statsReport
|
statsReport
|
||||||
.removeWhere((element) => !element.values.containsKey('audioLevel'));
|
.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
|
// https://www.w3.org/TR/webrtc-stats/#dom-rtcstatstype-media-source
|
||||||
// firefox does not seem to have this though. Works on chrome and android
|
// firefox does not seem to have this though. Works on chrome and android
|
||||||
audioLevelsMap[client.userID!] = statsReport
|
final ownAudioLevel = statsReport
|
||||||
.lastWhere((element) =>
|
.singleWhereOrNull((element) =>
|
||||||
element.type == 'media-source' &&
|
element.type == 'media-source' &&
|
||||||
element.values['kind'] == 'audio')
|
element.values['kind'] == 'audio')
|
||||||
.values['audioLevel'];
|
?.values['audioLevel'];
|
||||||
// works everywhere?
|
if (ownAudioLevel != null &&
|
||||||
audioLevelsMap[callFeed.userId] = statsReport
|
audioLevelsMap[client.userID] != ownAudioLevel) {
|
||||||
.lastWhere((element) => element.type == 'inbound-rtp')
|
audioLevelsMap[client.userID!] = ownAudioLevel;
|
||||||
.values['audioLevel'];
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double maxAudioLevel = double.negativeInfinity;
|
double maxAudioLevel = double.negativeInfinity;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:core';
|
import 'dart:core';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:sdp_transform/sdp_transform.dart' as sdp_transform;
|
import 'package:sdp_transform/sdp_transform.dart' as sdp_transform;
|
||||||
import 'package:webrtc_interface/webrtc_interface.dart';
|
import 'package:webrtc_interface/webrtc_interface.dart';
|
||||||
|
|
||||||
|
|
@ -828,6 +829,23 @@ class VoIP {
|
||||||
|
|
||||||
static const staleCallCheckerDuration = Duration(seconds: 30);
|
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
|
/// checks for stale calls in a room and sends `m.terminated` if all the
|
||||||
/// expires_ts are expired. Call when opening a room
|
/// expires_ts are expired. Call when opening a room
|
||||||
void startStaleCallsChecker(String roomId) async {
|
void startStaleCallsChecker(String roomId) async {
|
||||||
|
|
@ -851,28 +869,26 @@ class VoIP {
|
||||||
'found non terminated group call with id $groupCallId');
|
'found non terminated group call with id $groupCallId');
|
||||||
// call is not empty but check for stale participants (gone offline)
|
// call is not empty but check for stale participants (gone offline)
|
||||||
// with expire_ts
|
// with expire_ts
|
||||||
final Map<String, int> participants = {};
|
bool callExpired = true; // assume call is expired
|
||||||
final callMemberEvents = room.states.tryGetMap<String, Event>(
|
final callMemberEvents = room.states.tryGetMap<String, Event>(
|
||||||
EventTypes.GroupCallMemberPrefix);
|
EventTypes.GroupCallMemberPrefix);
|
||||||
|
|
||||||
if (callMemberEvents != null) {
|
if (callMemberEvents != null) {
|
||||||
callMemberEvents.forEach((userId, memberEvent) async {
|
for (var i = 0; i < callMemberEvents.length; i++) {
|
||||||
final callMemberEvent = groupCallEvent.room.getState(
|
final groupCallMemberEventMap =
|
||||||
EventTypes.GroupCallMemberPrefix,
|
callMemberEvents.entries.toList()[i];
|
||||||
userId,
|
|
||||||
);
|
|
||||||
if (callMemberEvent != null) {
|
|
||||||
final event =
|
|
||||||
IGroupCallRoomMemberState.fromJson(callMemberEvent);
|
|
||||||
participants[userId] = event.expireTs;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!participants.values.any((expire_ts) =>
|
final groupCallMemberEvent =
|
||||||
expire_ts > DateTime.now().millisecondsSinceEpoch)) {
|
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(
|
Logs().i(
|
||||||
'Group call with expired timestamps detected, terminating');
|
'Group call with only expired timestamps detected, terminating');
|
||||||
await sendGroupCallTerminateEvent(room, groupCallId);
|
await sendGroupCallTerminateEvent(room, groupCallId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue