fix: check negotiate party and call ids

chore: making some call naming schemes better
This commit is contained in:
td 2023-12-15 21:39:06 +05:30
parent 0bb1e3bef9
commit 520dfdbe3e
No known key found for this signature in database
GPG Key ID: 62A30523D4D6CE28
3 changed files with 80 additions and 45 deletions

View File

@ -30,18 +30,18 @@ import 'package:matrix/src/utils/cached_stream_controller.dart';
/// version 1 /// version 1
const String voipProtoVersion = '1'; const String voipProtoVersion = '1';
class Timeouts { class CallTimeouts {
/// The default life time for call events, in millisecond. /// The default life time for call events, in millisecond.
static const lifetimeMs = 10 * 1000; static const defaultCallEventLifetime = Duration(seconds: 10);
/// The length of time a call can be ringing for. /// The length of time a call can be ringing for.
static const callTimeoutSec = 60; static const callInviteLifetime = Duration(seconds: 60);
/// The delay for ice gathering. /// The delay for ice gathering.
static const iceGatheringDelayMs = 200; static const iceGatheringDelay = Duration(milliseconds: 200);
/// Delay before createOffer. /// Delay before createOffer.
static const delayBeforeOfferMs = 100; static const delayBeforeOffer = Duration(milliseconds: 100);
} }
extension RTCIceCandidateExt on RTCIceCandidate { extension RTCIceCandidateExt on RTCIceCandidate {
@ -504,7 +504,7 @@ class CallSession {
setCallState(CallState.kRinging); setCallState(CallState.kRinging);
ringingTimer = Timer(Duration(seconds: 30), () { ringingTimer = Timer(CallTimeouts.callInviteLifetime, () {
if (state == CallState.kRinging) { if (state == CallState.kRinging) {
Logs().v('[VOIP] Call invite has expired. Hanging up.'); Logs().v('[VOIP] Call invite has expired. Hanging up.');
hangupParty = CallParty.kRemote; // effectively hangupParty = CallParty.kRemote; // effectively
@ -621,8 +621,7 @@ class CallSession {
} }
/// Send select_answer event. /// Send select_answer event.
await sendSelectCallAnswer( await sendSelectCallAnswer(opts.room, callId, localPartyId, remotePartyId!);
opts.room, callId, Timeouts.lifetimeMs, localPartyId, remotePartyId!);
} }
Future<void> onNegotiateReceived( Future<void> onNegotiateReceived(
@ -659,7 +658,11 @@ class CallSession {
} }
await sendCallNegotiate( await sendCallNegotiate(
room, callId, Timeouts.lifetimeMs, localPartyId, answer.sdp!, room,
callId,
CallTimeouts.defaultCallEventLifetime.inMilliseconds,
localPartyId,
answer.sdp!,
type: answer.type!); type: answer.type!);
await pc!.setLocalDescription(answer); await pc!.setLocalDescription(answer);
} }
@ -983,7 +986,7 @@ class CallSession {
if (localUserMediaStream != null && localUserMediaStream!.stream != null) { if (localUserMediaStream != null && localUserMediaStream!.stream != null) {
final stream = await _getUserMedia(CallType.kVideo); final stream = await _getUserMedia(CallType.kVideo);
if (stream != null) { if (stream != null) {
Logs().e('[VOIP] running replaceTracks() on stream: ${stream.id}'); Logs().d('[VOIP] running replaceTracks() on stream: ${stream.id}');
_setTracksEnabled(stream.getVideoTracks(), true); _setTracksEnabled(stream.getVideoTracks(), true);
// replace local tracks // replace local tracks
for (final track in localUserMediaStream!.stream!.getTracks()) { for (final track in localUserMediaStream!.stream!.getTracks()) {
@ -1145,8 +1148,7 @@ class CallSession {
Logs().d('[VOIP] Rejecting call: $callId'); Logs().d('[VOIP] Rejecting call: $callId');
await terminate(CallParty.kLocal, CallErrorCode.UserHangup, shouldEmit); await terminate(CallParty.kLocal, CallErrorCode.UserHangup, shouldEmit);
if (shouldEmit) { if (shouldEmit) {
await sendCallReject( await sendCallReject(room, callId, localPartyId, reason);
room, callId, Timeouts.lifetimeMs, localPartyId, reason);
} }
} }
@ -1257,8 +1259,7 @@ class CallSession {
if (pc!.iceGatheringState == if (pc!.iceGatheringState ==
RTCIceGatheringState.RTCIceGatheringStateGathering) { RTCIceGatheringState.RTCIceGatheringStateGathering) {
// Allow a short time for initial candidates to be gathered // Allow a short time for initial candidates to be gathered
await Future.delayed( await Future.delayed(CallTimeouts.iceGatheringDelay);
Duration(milliseconds: Timeouts.iceGatheringDelayMs));
} }
if (callHasEnded) return; if (callHasEnded) return;
@ -1271,8 +1272,14 @@ class CallSession {
Logs().d('[glare] new invite sent about to be called'); Logs().d('[glare] new invite sent about to be called');
await sendInviteToCall( await sendInviteToCall(
room, callId, Timeouts.lifetimeMs, localPartyId, null, offer.sdp!, room,
capabilities: callCapabilities, metadata: metadata); callId,
CallTimeouts.callInviteLifetime.inMilliseconds,
localPartyId,
null,
offer.sdp!,
capabilities: callCapabilities,
metadata: metadata);
// just incase we ended the call but already sent the invite // just incase we ended the call but already sent the invite
if (state == CallState.kEnded) { if (state == CallState.kEnded) {
await hangup(CallErrorCode.Replaced, false); await hangup(CallErrorCode.Replaced, false);
@ -1287,7 +1294,7 @@ class CallSession {
setCallState(CallState.kInviteSent); setCallState(CallState.kInviteSent);
inviteTimer = Timer(Duration(seconds: Timeouts.callTimeoutSec), () { inviteTimer = Timer(CallTimeouts.callInviteLifetime, () {
if (state == CallState.kInviteSent) { if (state == CallState.kInviteSent) {
hangup(CallErrorCode.InviteTimeout); hangup(CallErrorCode.InviteTimeout);
} }
@ -1296,7 +1303,11 @@ class CallSession {
}); });
} else { } else {
await sendCallNegotiate( await sendCallNegotiate(
room, callId, Timeouts.lifetimeMs, localPartyId, offer.sdp!, room,
callId,
CallTimeouts.defaultCallEventLifetime.inMilliseconds,
localPartyId,
offer.sdp!,
type: offer.type!, type: offer.type!,
capabilities: callCapabilities, capabilities: callCapabilities,
metadata: metadata); metadata: metadata);
@ -1311,7 +1322,7 @@ class CallSession {
// onNegotiationNeeded, which causes creatOffer to only include // onNegotiationNeeded, which causes creatOffer to only include
// audio m-line, add delay and wait for video track to be added, // audio m-line, add delay and wait for video track to be added,
// then createOffer can get audio/video m-line correctly. // then createOffer can get audio/video m-line correctly.
await Future.delayed(Duration(milliseconds: Timeouts.delayBeforeOfferMs)); await Future.delayed(CallTimeouts.delayBeforeOffer);
final offer = await pc!.createOffer({}); final offer = await pc!.createOffer({});
await _gotLocalOffer(offer); await _gotLocalOffer(offer);
} catch (e) { } catch (e) {
@ -1703,8 +1714,8 @@ class CallSession {
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 1. /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
/// [party_id] The party ID for call, Can be set to client.deviceId. /// [party_id] The party ID for call, Can be set to client.deviceId.
/// [selected_party_id] The party ID for the selected answer. /// [selected_party_id] The party ID for the selected answer.
Future<String?> sendSelectCallAnswer(Room room, String callId, int lifetime, Future<String?> sendSelectCallAnswer(
String party_id, String selected_party_id, Room room, String callId, String party_id, String selected_party_id,
{String version = voipProtoVersion, String? txid}) async { {String version = voipProtoVersion, String? txid}) async {
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
@ -1713,7 +1724,6 @@ class CallSession {
'party_id': party_id, 'party_id': party_id,
if (groupCallId != null) 'conf_id': groupCallId, if (groupCallId != null) 'conf_id': groupCallId,
'version': version, 'version': version,
'lifetime': lifetime,
'selected_party_id': selected_party_id, 'selected_party_id': selected_party_id,
}; };
@ -1730,7 +1740,7 @@ class CallSession {
/// [version] is the version of the VoIP specification this message adheres to. This specification is version 1. /// [version] is the version of the VoIP specification this message adheres to. This specification is version 1.
/// [party_id] The party ID for call, Can be set to client.deviceId. /// [party_id] The party ID for call, Can be set to client.deviceId.
Future<String?> sendCallReject( Future<String?> sendCallReject(
Room room, String callId, int lifetime, String party_id, String? reason, Room room, String callId, String party_id, String? reason,
{String version = voipProtoVersion, String? txid}) async { {String version = voipProtoVersion, String? txid}) async {
txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}'; txid ??= 'txid${DateTime.now().millisecondsSinceEpoch}';
@ -1740,7 +1750,6 @@ class CallSession {
if (groupCallId != null) 'conf_id': groupCallId, if (groupCallId != null) 'conf_id': groupCallId,
if (reason != null) 'reason': reason, if (reason != null) 'reason': reason,
'version': version, 'version': version,
'lifetime': lifetime,
}; };
return await _sendContent( return await _sendContent(
@ -1972,6 +1981,9 @@ class CallSession {
}) async { }) async {
txid ??= client.generateUniqueTransactionId(); txid ??= client.generateUniqueTransactionId();
final mustEncrypt = room.encrypted && client.encryptionEnabled; final mustEncrypt = room.encrypted && client.encryptionEnabled;
// opponentDeviceId is only set for a few events during group calls,
// therefore only group calls use to-device messages for some events
if (opponentDeviceId != null) { if (opponentDeviceId != null) {
final toDeviceSeq = this.toDeviceSeq++; final toDeviceSeq = this.toDeviceSeq++;

View File

@ -192,7 +192,7 @@ class GroupCall {
WrappedMediaStream? localUserMediaStream; WrappedMediaStream? localUserMediaStream;
WrappedMediaStream? localScreenshareStream; WrappedMediaStream? localScreenshareStream;
String? localDesktopCapturerSourceId; String? localDesktopCapturerSourceId;
List<CallSession> calls = []; List<CallSession> callSessions = [];
List<User> participants = []; List<User> participants = [];
List<WrappedMediaStream> userMediaStreams = []; List<WrappedMediaStream> userMediaStreams = [];
List<WrappedMediaStream> screenshareStreams = []; List<WrappedMediaStream> screenshareStreams = [];
@ -404,7 +404,7 @@ class GroupCall {
final stream = final stream =
await voip.delegate.mediaDevices.getUserMedia({'audio': true}); await voip.delegate.mediaDevices.getUserMedia({'audio': true});
final audioTrack = stream.getAudioTracks().first; final audioTrack = stream.getAudioTracks().first;
for (final call in calls) { for (final call in callSessions) {
await call.updateAudioDevice(audioTrack); await call.updateAudioDevice(audioTrack);
} }
} }
@ -441,7 +441,7 @@ class GroupCall {
_callSubscription = voip.onIncomingCall.stream.listen(onIncomingCall); _callSubscription = voip.onIncomingCall.stream.listen(onIncomingCall);
for (final call in calls) { for (final call in callSessions) {
await onIncomingCall(call); await onIncomingCall(call);
} }
@ -478,7 +478,7 @@ class GroupCall {
await removeMemberStateEvent(); await removeMemberStateEvent();
final callsCopy = calls.toList(); final callsCopy = callSessions.toList();
for (final call in callsCopy) { for (final call in callsCopy) {
await removeCall(call, CallErrorCode.UserHangup); await removeCall(call, CallErrorCode.UserHangup);
@ -553,7 +553,7 @@ class GroupCall {
setTracksEnabled(localUserMediaStream!.stream!.getAudioTracks(), !muted); setTracksEnabled(localUserMediaStream!.stream!.getAudioTracks(), !muted);
} }
for (final call in calls) { for (final call in callSessions) {
await call.setMicrophoneMuted(muted); await call.setMicrophoneMuted(muted);
} }
@ -571,7 +571,7 @@ class GroupCall {
setTracksEnabled(localUserMediaStream!.stream!.getVideoTracks(), !muted); setTracksEnabled(localUserMediaStream!.stream!.getVideoTracks(), !muted);
} }
for (final call in calls) { for (final call in callSessions) {
await call.setLocalVideoMuted(muted); await call.setLocalVideoMuted(muted);
} }
@ -620,7 +620,7 @@ class GroupCall {
await localScreenshareStream!.initialize(); await localScreenshareStream!.initialize();
onGroupCallEvent.add(GroupCallEvent.LocalScreenshareStateChanged); onGroupCallEvent.add(GroupCallEvent.LocalScreenshareStateChanged);
for (final call in calls) { for (final call in callSessions) {
await call.addLocalStream( await call.addLocalStream(
await localScreenshareStream!.stream!.clone(), await localScreenshareStream!.stream!.clone(),
localScreenshareStream!.purpose); localScreenshareStream!.purpose);
@ -637,7 +637,7 @@ class GroupCall {
return false; return false;
} }
} else { } else {
for (final call in calls) { for (final call in callSessions) {
await call.removeLocalStream(call.localScreenSharingStream!); await call.removeLocalStream(call.localScreenSharingStream!);
} }
@ -935,7 +935,7 @@ class GroupCall {
} }
CallSession? getCallByUserId(String userId) { CallSession? getCallByUserId(String userId) {
final value = calls.where((item) => item.remoteUser!.id == userId); final value = callSessions.where((item) => item.remoteUser!.id == userId);
if (value.isNotEmpty) { if (value.isNotEmpty) {
return value.first; return value.first;
} }
@ -943,7 +943,7 @@ class GroupCall {
} }
Future<void> addCall(CallSession call) async { Future<void> addCall(CallSession call) async {
calls.add(call); callSessions.add(call);
await initCall(call); await initCall(call);
onGroupCallEvent.add(GroupCallEvent.CallsChanged); onGroupCallEvent.add(GroupCallEvent.CallsChanged);
} }
@ -951,14 +951,14 @@ class GroupCall {
Future<void> replaceCall( Future<void> replaceCall(
CallSession existingCall, CallSession replacementCall) async { CallSession existingCall, CallSession replacementCall) async {
final existingCallIndex = final existingCallIndex =
calls.indexWhere((element) => element == existingCall); callSessions.indexWhere((element) => element == existingCall);
if (existingCallIndex == -1) { if (existingCallIndex == -1) {
throw Exception('Couldn\'t find call to replace'); throw Exception('Couldn\'t find call to replace');
} }
calls.removeAt(existingCallIndex); callSessions.removeAt(existingCallIndex);
calls.add(replacementCall); callSessions.add(replacementCall);
await disposeCall(existingCall, CallErrorCode.Replaced); await disposeCall(existingCall, CallErrorCode.Replaced);
await initCall(replacementCall); await initCall(replacementCall);
@ -970,7 +970,7 @@ class GroupCall {
Future<void> removeCall(CallSession call, String hangupReason) async { Future<void> removeCall(CallSession call, String hangupReason) async {
await disposeCall(call, hangupReason); await disposeCall(call, hangupReason);
calls.removeWhere((element) => call.callId == element.callId); callSessions.removeWhere((element) => call.callId == element.callId);
onGroupCallEvent.add(GroupCallEvent.CallsChanged); onGroupCallEvent.add(GroupCallEvent.CallsChanged);
} }
@ -1287,7 +1287,7 @@ class GroupCall {
onGroupCallEvent.add(GroupCallEvent.ParticipantsChanged); onGroupCallEvent.add(GroupCallEvent.ParticipantsChanged);
final callsCopylist = List.from(calls); final callsCopylist = List.from(callSessions);
for (final call in callsCopylist) { for (final call in callsCopylist) {
await call.updateMuteStatus(); await call.updateMuteStatus();
@ -1305,7 +1305,7 @@ class GroupCall {
onGroupCallEvent.add(GroupCallEvent.ParticipantsChanged); onGroupCallEvent.add(GroupCallEvent.ParticipantsChanged);
final callsCopylist = List.from(calls); final callsCopylist = List.from(callSessions);
for (final call in callsCopylist) { for (final call in callsCopylist) {
await call.updateMuteStatus(); await call.updateMuteStatus();

View File

@ -176,7 +176,11 @@ class VoIP {
final String partyId = content['party_id']; final String partyId = content['party_id'];
final int lifetime = content['lifetime']; final int lifetime = content['lifetime'];
final String? confId = content['conf_id']; final String? confId = content['conf_id'];
// msc3401 group call invites send deviceId and senderSessionId in to device messages
final String? deviceId = content['device_id']; final String? deviceId = content['device_id'];
final String? senderSessionId = content['sender_session_id'];
final call = calls[callId]; final call = calls[callId];
Logs().d( Logs().d(
@ -232,7 +236,7 @@ class VoIP {
newCall.remotePartyId = partyId; newCall.remotePartyId = partyId;
newCall.remoteUser = await room.requestUser(senderId); newCall.remoteUser = await room.requestUser(senderId);
newCall.opponentDeviceId = deviceId; newCall.opponentDeviceId = deviceId;
newCall.opponentSessionId = content['sender_session_id']; newCall.opponentSessionId = senderSessionId;
if (!delegate.canHandleNewCall && if (!delegate.canHandleNewCall &&
(confId == null || confId != currentGroupCID)) { (confId == null || confId != currentGroupCID)) {
Logs().v( Logs().v(
@ -263,16 +267,22 @@ class VoIP {
// initWithInvite, we might set it to callId even after it was reset to null // initWithInvite, we might set it to callId even after it was reset to null
// by terminate. // by terminate.
currentCID = callId; currentCID = callId;
try {
await newCall.initWithInvite( await newCall.initWithInvite(
callType, offer, sdpStreamMetadata, lifetime, confId != null); callType, offer, sdpStreamMetadata, lifetime, confId != null);
} catch (e, s) {
Logs().e('[VOIP] initWithInvite failed', e, s);
}
// Popup CallingPage for incoming call. // Popup CallingPage for incoming call.
if (confId == null && !newCall.callHasEnded) { if (confId == null && !newCall.callHasEnded) {
await delegate.handleNewCall(newCall); await delegate.handleNewCall(newCall);
} }
onIncomingCall.add(newCall); if (confId != null) {
// the stream is used to monitor incoming peer calls in a mesh call
onIncomingCall.add(newCall);
}
} }
Future<void> onCallAnswer( Future<void> onCallAnswer(
@ -494,6 +504,19 @@ class VoIP {
'Ignoring call negotiation for room $roomId claiming to be for call in room ${call.room.id}'); 'Ignoring call negotiation for room $roomId claiming to be for call in room ${call.room.id}');
return; return;
} }
if (content['party_id'] != call.remotePartyId) {
Logs().w('Ignoring call negotiation, wrong partyId detected');
return;
}
if (content['party_id'] == call.localPartyId) {
Logs().w('Ignoring call negotiation echo');
return;
}
// ideally you also check the lifetime here and discard negotiation events
// if age of the event was older than the lifetime but as to device events
// do not have a unsigned age nor a origin_server_ts there's no easy way to
// override this one function atm
final description = content['description']; final description = content['description'];
try { try {