fix: do not proceed call if getUserMedia fails

fix: added a few missing awaits

fix: add a workaround for not having state updates for staleCallChecker till sync

chore: fix some logging
This commit is contained in:
td 2023-07-10 14:19:16 +05:30
parent 8f44c7f33f
commit 66a53786e7
No known key found for this signature in database
GPG Key ID: F6D9E9BF14C7D103
6 changed files with 88 additions and 73 deletions

View File

@ -358,7 +358,7 @@ class CallSession {
final CachedStreamController<CallSession> onCallReplaced = final CachedStreamController<CallSession> onCallReplaced =
CachedStreamController(); CachedStreamController();
final CachedStreamController<CallSession> onCallHangup = final CachedStreamController<CallSession> onCallHangupNotifierForGroupCalls =
CachedStreamController(); CachedStreamController();
final CachedStreamController<CallState> onCallStateChanged = final CachedStreamController<CallState> onCallStateChanged =
@ -435,6 +435,7 @@ class CallSession {
Timer? inviteTimer; Timer? inviteTimer;
Timer? ringingTimer; Timer? ringingTimer;
// outgoing call
Future<void> initOutboundCall(CallType type) async { Future<void> initOutboundCall(CallType type) async {
await _preparePeerConnection(); await _preparePeerConnection();
setCallState(CallState.kCreateOffer); setCallState(CallState.kCreateOffer);
@ -444,6 +445,7 @@ class CallSession {
} }
} }
// incoming call
Future<void> initWithInvite(CallType type, RTCSessionDescription offer, Future<void> initWithInvite(CallType type, RTCSessionDescription offer,
SDPStreamMetadata? metadata, int lifetime, bool isGroupCall) async { SDPStreamMetadata? metadata, int lifetime, bool isGroupCall) async {
if (!isGroupCall) { if (!isGroupCall) {
@ -491,6 +493,12 @@ class CallSession {
final stream = await _getUserMedia(type); final stream = await _getUserMedia(type);
if (stream != null) { if (stream != null) {
await addLocalStream(stream, SDPStreamMetadataPurpose.Usermedia); await addLocalStream(stream, SDPStreamMetadataPurpose.Usermedia);
} else {
// we don't have a localstream, call probably crashed
// for sanity
if (state == CallState.kEnded) {
return;
}
} }
} }
@ -597,10 +605,6 @@ class CallSession {
// Now we wait for the negotiationneeded event // Now we wait for the negotiationneeded event
} }
void initWithHangup() {
setCallState(CallState.kEnded);
}
Future<void> onAnswerReceived( Future<void> onAnswerReceived(
RTCSessionDescription answer, SDPStreamMetadata? metadata) async { RTCSessionDescription answer, SDPStreamMetadata? metadata) async {
if (metadata != null) { if (metadata != null) {
@ -658,9 +662,9 @@ class CallSession {
type: answer.type!); type: answer.type!);
await pc!.setLocalDescription(answer); await pc!.setLocalDescription(answer);
} }
} catch (e) { } catch (e, s) {
_getLocalOfferFailed(e); Logs().e('[VOIP] onNegotiateReceived => ', e, s);
Logs().e('[VOIP] onNegotiateReceived => ${e.toString()}'); await _getLocalOfferFailed(e);
return; return;
} }
@ -740,8 +744,8 @@ class CallSession {
if (pc != null && inviteOrAnswerSent && remotePartyId != null) { if (pc != null && inviteOrAnswerSent && remotePartyId != null) {
try { try {
await pc!.addCandidate(candidate); await pc!.addCandidate(candidate);
} catch (e) { } catch (e, s) {
Logs().e('[VOIP] onCandidatesReceived => ${e.toString()}'); Logs().e('[VOIP] onCandidatesReceived => ', e, s);
} }
} else { } else {
remoteCandidates.add(candidate); remoteCandidates.add(candidate);
@ -810,8 +814,8 @@ class CallSession {
await _removeStream(localScreenSharingStream!.stream!); await _removeStream(localScreenSharingStream!.stream!);
fireCallEvent(CallEvent.kFeedsChanged); fireCallEvent(CallEvent.kFeedsChanged);
return false; return false;
} catch (e) { } catch (e, s) {
Logs().e('[VOIP] stopping screen sharing track failed', e); Logs().e('[VOIP] stopping screen sharing track failed', e, s);
return false; return false;
} }
} }
@ -1130,12 +1134,11 @@ class CallSession {
/// Reject a call /// Reject a call
/// This used to be done by calling hangup, but is a separate method and protocol /// This used to be done by calling hangup, but is a separate method and protocol
/// event as of MSC2746. /// event as of MSC2746.
///
Future<void> reject({String? reason, bool shouldEmit = true}) async { Future<void> reject({String? reason, bool shouldEmit = true}) async {
// stop play ringtone
await voip.delegate.stopRingtone();
if (state != CallState.kRinging && state != CallState.kFledgling) { if (state != CallState.kRinging && state != CallState.kFledgling) {
Logs().e('[VOIP] Call must be in \'ringing|fledgling\' state to reject!'); Logs().e(
'[VOIP] Call must be in \'ringing|fledgling\' state to reject! (current state was: ${state.toString()}) Calling hangup instead');
await hangup(reason, shouldEmit);
return; return;
} }
Logs().d('[VOIP] Rejecting call: $callId'); Logs().d('[VOIP] Rejecting call: $callId');
@ -1146,12 +1149,9 @@ class CallSession {
} }
} }
Future<void> hangup([String? reason, bool suppressEvent = false]) async { Future<void> hangup([String? reason, bool shouldEmit = true]) async {
// stop play ringtone
await voip.delegate.stopRingtone();
await terminate( await terminate(
CallParty.kLocal, reason ?? CallErrorCode.UserHangup, !suppressEvent); CallParty.kLocal, reason ?? CallErrorCode.UserHangup, shouldEmit);
try { try {
final res = final res =
@ -1174,11 +1174,11 @@ class CallSession {
} }
Future<void> terminate( Future<void> terminate(
CallParty party, String reason, bool shouldEmit) async { CallParty party,
if (state == CallState.kEnded) { String reason,
return; bool shouldEmit,
} ) async {
Logs().d('[VOIP] terminating call');
inviteTimer?.cancel(); inviteTimer?.cancel();
inviteTimer = null; inviteTimer = null;
@ -1195,9 +1195,9 @@ class CallSession {
hangupParty = party; hangupParty = party;
hangupReason = reason; hangupReason = reason;
if (shouldEmit) { // don't see any reason to wrap this with shouldEmit atm,
setCallState(CallState.kEnded); // looks like a local state change only
} setCallState(CallState.kEnded);
if (!isGroupCall) { if (!isGroupCall) {
if (callId != voip.currentCID) return; if (callId != voip.currentCID) return;
@ -1209,7 +1209,7 @@ class CallSession {
await cleanUp(); await cleanUp();
if (shouldEmit) { if (shouldEmit) {
onCallHangup.add(this); onCallHangupNotifierForGroupCalls.add(this);
await voip.delegate.handleCallEnded(this); await voip.delegate.handleCallEnded(this);
fireCallEvent(CallEvent.kHangup); fireCallEvent(CallEvent.kHangup);
if ((party == CallParty.kRemote && missedCall)) { if ((party == CallParty.kRemote && missedCall)) {
@ -1273,7 +1273,6 @@ class CallSession {
// 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);
setCallState(CallState.kEnded);
return; return;
} }
inviteOrAnswerSent = true; inviteOrAnswerSent = true;
@ -1313,7 +1312,7 @@ class CallSession {
final offer = await pc!.createOffer({}); final offer = await pc!.createOffer({});
await _gotLocalOffer(offer); await _gotLocalOffer(offer);
} catch (e) { } catch (e) {
_getLocalOfferFailed(e); await _getLocalOfferFailed(e);
return; return;
} finally { } finally {
makingOffer = false; makingOffer = false;
@ -1377,9 +1376,9 @@ class CallSession {
} }
} }
void onAnsweredElsewhere() { Future<void> onAnsweredElsewhere() async {
Logs().d('Call ID $callId answered elsewhere'); Logs().d('Call ID $callId answered elsewhere');
terminate(CallParty.kRemote, CallErrorCode.AnsweredElsewhere, true); await terminate(CallParty.kRemote, CallErrorCode.AnsweredElsewhere, true);
} }
Future<void> cleanUp() async { Future<void> cleanUp() async {
@ -1388,8 +1387,8 @@ class CallSession {
await stream.dispose(); await stream.dispose();
} }
streams.clear(); streams.clear();
} catch (e) { } catch (e, s) {
Logs().e('[VOIP] cleaning up streams failed', e); Logs().e('[VOIP] cleaning up streams failed', e, s);
} }
try { try {
@ -1397,8 +1396,8 @@ class CallSession {
await pc!.close(); await pc!.close();
await pc!.dispose(); await pc!.dispose();
} }
} catch (e) { } catch (e, s) {
Logs().e('[VOIP] removing pc failed', e); Logs().e('[VOIP] removing pc failed', e, s);
} }
} }
@ -1468,7 +1467,7 @@ class CallSession {
try { try {
return await voip.delegate.mediaDevices.getUserMedia(mediaConstraints); return await voip.delegate.mediaDevices.getUserMedia(mediaConstraints);
} catch (e) { } catch (e) {
_getUserMediaFailed(e); await _getUserMediaFailed(e);
} }
return null; return null;
} }
@ -1481,7 +1480,7 @@ class CallSession {
try { try {
return await voip.delegate.mediaDevices.getDisplayMedia(mediaConstraints); return await voip.delegate.mediaDevices.getDisplayMedia(mediaConstraints);
} catch (e) { } catch (e) {
_getUserMediaFailed(e); await _getUserMediaFailed(e);
} }
return null; return null;
} }
@ -1614,15 +1613,15 @@ class CallSession {
} }
} }
void _getLocalOfferFailed(dynamic err) { Future<void> _getLocalOfferFailed(dynamic err) async {
Logs().e('Failed to get local offer ${err.toString()}'); Logs().e('Failed to get local offer ${err.toString()}');
fireCallEvent(CallEvent.kError); fireCallEvent(CallEvent.kError);
lastError = CallError( lastError = CallError(
CallErrorCode.LocalOfferFailed, 'Failed to get local offer!', err); CallErrorCode.LocalOfferFailed, 'Failed to get local offer!', err);
terminate(CallParty.kLocal, CallErrorCode.LocalOfferFailed, false); await terminate(CallParty.kLocal, CallErrorCode.LocalOfferFailed, false);
} }
void _getUserMediaFailed(dynamic err) { Future<void> _getUserMediaFailed(dynamic err) async {
if (state != CallState.kConnected) { if (state != CallState.kConnected) {
Logs().w('Failed to get user media - ending call ${err.toString()}'); Logs().w('Failed to get user media - ending call ${err.toString()}');
fireCallEvent(CallEvent.kError); fireCallEvent(CallEvent.kError);
@ -1630,11 +1629,11 @@ class CallSession {
CallErrorCode.NoUserMedia, CallErrorCode.NoUserMedia,
'Couldn\'t start capturing media! Is your microphone set up and does this app have permission?', 'Couldn\'t start capturing media! Is your microphone set up and does this app have permission?',
err); err);
terminate(CallParty.kLocal, CallErrorCode.NoUserMedia, false); await terminate(CallParty.kLocal, CallErrorCode.NoUserMedia, false);
} }
} }
void onSelectAnswerReceived(String? selectedPartyId) { Future<void> onSelectAnswerReceived(String? selectedPartyId) async {
if (direction != CallDirection.kIncoming) { if (direction != CallDirection.kIncoming) {
Logs().w('Got select_answer for an outbound call: ignoring'); Logs().w('Got select_answer for an outbound call: ignoring');
return; return;
@ -1649,7 +1648,7 @@ class CallSession {
Logs().w( Logs().w(
'Got select_answer for party ID $selectedPartyId: we are party ID $localPartyId.'); 'Got select_answer for party ID $selectedPartyId: we are party ID $localPartyId.');
// The other party has picked somebody else's answer // The other party has picked somebody else's answer
terminate(CallParty.kRemote, CallErrorCode.AnsweredElsewhere, true); await terminate(CallParty.kRemote, CallErrorCode.AnsweredElsewhere, true);
} }
} }

View File

@ -64,8 +64,9 @@ class ConnectionTester {
} }
return false; return false;
}); });
} catch (e) { } catch (e, s) {
Logs().e('[VOIP] ConnectionTester Error while testing TURN server: $e'); Logs()
.e('[VOIP] ConnectionTester Error while testing TURN server: ', e, s);
} }
dispose(); dispose();

View File

@ -340,8 +340,8 @@ class GroupCall {
}; };
try { try {
return await voip.delegate.mediaDevices.getDisplayMedia(mediaConstraints); return await voip.delegate.mediaDevices.getDisplayMedia(mediaConstraints);
} catch (e) { } catch (e, s) {
setState(GroupCallState.LocalCallFeedUninitialized); Logs().e('[VOIP] _getDisplayMedia failed because,', e, s);
} }
return Null as MediaStream; return Null as MediaStream;
} }
@ -627,10 +627,10 @@ class GroupCall {
await sendMemberStateEvent(); await sendMemberStateEvent();
return true; return true;
} catch (error) { } catch (e, s) {
Logs().e('Enabling screensharing error', error); Logs().e('Enabling screensharing error', e, s);
lastError = GroupCallError(GroupCallErrorCode.NoUserMedia, lastError = GroupCallError(GroupCallErrorCode.NoUserMedia,
'Failed to get screen-sharing stream: ', error); 'Failed to get screen-sharing stream: ', e);
onGroupCallEvent.add(GroupCallEvent.Error); onGroupCallEvent.add(GroupCallEvent.Error);
return false; return false;
} }
@ -994,7 +994,7 @@ class GroupCall {
await onStreamsChanged(call); await onStreamsChanged(call);
}); });
call.onCallHangup.stream.listen((event) async { call.onCallHangupNotifierForGroupCalls.stream.listen((event) async {
await onCallHangup(call); await onCallHangup(call);
}); });

View File

@ -10,14 +10,14 @@ Future<void> stopMediaStream(MediaStream? stream) async {
for (final track in stream.getTracks()) { for (final track in stream.getTracks()) {
try { try {
await track.stop(); await track.stop();
} catch (e) { } catch (e, s) {
Logs().e('[VOIP] stopping track ${track.id} failed', e); Logs().e('[VOIP] stopping track ${track.id} failed', e, s);
} }
} }
try { try {
await stream.dispose(); await stream.dispose();
} catch (e) { } catch (e, s) {
Logs().e('[VOIP] disposing stream ${stream.id} failed', e); Logs().e('[VOIP] disposing stream ${stream.id} failed', e, s);
} }
} }
} }

View File

@ -284,7 +284,7 @@ class VoIP {
await delegate.stopRingtone(); await delegate.stopRingtone();
} }
if (call.state == CallState.kRinging) { if (call.state == CallState.kRinging) {
call.onAnsweredElsewhere(); await call.onAnsweredElsewhere();
} }
return; return;
} }
@ -368,7 +368,7 @@ class VoIP {
} }
await call.onRejectReceived(content['reason']); await call.onRejectReceived(content['reason']);
} else { } else {
Logs().v('[VOIP] onCallHangup: Session [$callId] not found!'); Logs().v('[VOIP] onCallReject: Session [$callId] not found!');
} }
} }
@ -408,7 +408,7 @@ class VoIP {
'Ignoring call select answer for room $roomId claiming to be for call in room ${call.room.id}'); 'Ignoring call select answer for room $roomId claiming to be for call in room ${call.room.id}');
return; return;
} }
call.onSelectAnswerReceived(selectedPartyId); await call.onSelectAnswerReceived(selectedPartyId);
} }
} }
@ -486,8 +486,8 @@ class VoIP {
} }
await call.onNegotiateReceived(metadata, await call.onNegotiateReceived(metadata,
RTCSessionDescription(description['sdp'], description['type'])); RTCSessionDescription(description['sdp'], description['type']));
} catch (err) { } catch (e, s) {
Logs().e('Failed to complete negotiation ${err.toString()}'); Logs().e('Failed to complete negotiation', e, s);
} }
} }
} }
@ -498,8 +498,8 @@ class VoIP {
if (session['media'].indexWhere((e) => e['type'] == 'video') != -1) { if (session['media'].indexWhere((e) => e['type'] == 'video') != -1) {
return CallType.kVideo; return CallType.kVideo;
} }
} catch (err) { } catch (e, s) {
Logs().e('Failed to getCallType ${err.toString()}'); Logs().e('Failed to getCallType', e, s);
} }
return CallType.kVoice; return CallType.kVoice;

View File

@ -48,7 +48,7 @@ extension GroupCallUtils on Room {
if (staleGroupCallsTimer.tryGet(roomId) != null) { if (staleGroupCallsTimer.tryGet(roomId) != null) {
staleGroupCallsTimer[roomId]!.cancel(); staleGroupCallsTimer[roomId]!.cancel();
staleGroupCallsTimer.remove(roomId); staleGroupCallsTimer.remove(roomId);
Logs().d('stopped stale group calls checker for room $id'); Logs().d('[VOIP] stopped stale group calls checker for room $id');
} else { } else {
Logs().d('[VOIP] no stale call checker for room found'); Logs().d('[VOIP] no stale call checker for room found');
} }
@ -70,7 +70,22 @@ extension GroupCallUtils on Room {
device.expires_ts! < DateTime.now().millisecondsSinceEpoch); device.expires_ts! < DateTime.now().millisecondsSinceEpoch);
} }
} }
return true;
// Last 30 seconds to get yourself together.
// This saves us from accidentally killing calls which were just created and
// whose state event we haven't recieved yet in sync.
// (option 2 was local echo member state events, but reverting them if anything
// fails sounds pain)
final expiredfr = groupCallMemberStateEvent.originServerTs
.add(staleCallCheckerDuration)
.millisecondsSinceEpoch <
DateTime.now().millisecondsSinceEpoch;
if (!expiredfr) {
Logs().d(
'[VOIP] Last 30 seconds for state event from ${groupCallMemberStateEvent.senderId}');
}
return expiredfr;
} }
/// 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
@ -85,7 +100,7 @@ extension GroupCallUtils on Room {
} }
Future<void> singleShotStaleCallCheckerOnRoom() async { Future<void> singleShotStaleCallCheckerOnRoom() async {
Logs().d('checking for stale group calls in room $id'); Logs().d('[VOIP] checking for stale group calls in room $id');
// make sure we have all the to-device messages we are supposed to have // make sure we have all the to-device messages we are supposed to have
await client.oneShotSync(); await client.oneShotSync();
final copyGroupCallIds = final copyGroupCallIds =
@ -97,7 +112,7 @@ extension GroupCallUtils on Room {
if (groupCallEvent.content.tryGet('m.intent') == 'm.room') return; if (groupCallEvent.content.tryGet('m.intent') == 'm.room') return;
if (!groupCallEvent.content.containsKey('m.terminated')) { if (!groupCallEvent.content.containsKey('m.terminated')) {
Logs().i('found non terminated group call with id $groupCallId'); Logs().i('[VOIP] 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
bool callExpired = true; // assume call is expired bool callExpired = true; // assume call is expired
@ -118,7 +133,7 @@ extension GroupCallUtils on Room {
if (callExpired) { if (callExpired) {
Logs().i( Logs().i(
'Group call with only expired timestamps detected, terminating'); '[VOIP] Group call with only expired timestamps detected, terminating');
await sendGroupCallTerminateEvent(groupCallId); await sendGroupCallTerminateEvent(groupCallId);
} }
} }
@ -132,7 +147,7 @@ extension GroupCallUtils on Room {
final existingStateEvent = final existingStateEvent =
getState(EventTypes.GroupCallPrefix, groupCallId); getState(EventTypes.GroupCallPrefix, groupCallId);
if (existingStateEvent == null) { if (existingStateEvent == null) {
Logs().e('could not find group call with id $groupCallId'); Logs().e('[VOIP] could not find group call with id $groupCallId');
return null; return null;
} }
@ -144,8 +159,8 @@ extension GroupCallUtils on Room {
Logs().i('[VOIP] Group call $groupCallId was killed uwu'); Logs().i('[VOIP] Group call $groupCallId was killed uwu');
return req; return req;
} catch (e) { } catch (e, s) {
Logs().e('killing stale call $groupCallId failed. reason: $e'); Logs().e('[VOIP] killing stale call $groupCallId failed', e, s);
return null; return null;
} }
} }