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

View File

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

View File

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

View File

@ -284,7 +284,7 @@ class VoIP {
await delegate.stopRingtone();
}
if (call.state == CallState.kRinging) {
call.onAnsweredElsewhere();
await call.onAnsweredElsewhere();
}
return;
}
@ -368,7 +368,7 @@ class VoIP {
}
await call.onRejectReceived(content['reason']);
} 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}');
return;
}
call.onSelectAnswerReceived(selectedPartyId);
await call.onSelectAnswerReceived(selectedPartyId);
}
}
@ -486,8 +486,8 @@ class VoIP {
}
await call.onNegotiateReceived(metadata,
RTCSessionDescription(description['sdp'], description['type']));
} catch (err) {
Logs().e('Failed to complete negotiation ${err.toString()}');
} catch (e, s) {
Logs().e('Failed to complete negotiation', e, s);
}
}
}
@ -498,8 +498,8 @@ class VoIP {
if (session['media'].indexWhere((e) => e['type'] == 'video') != -1) {
return CallType.kVideo;
}
} catch (err) {
Logs().e('Failed to getCallType ${err.toString()}');
} catch (e, s) {
Logs().e('Failed to getCallType', e, s);
}
return CallType.kVoice;

View File

@ -48,7 +48,7 @@ extension GroupCallUtils on Room {
if (staleGroupCallsTimer.tryGet(roomId) != null) {
staleGroupCallsTimer[roomId]!.cancel();
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 {
Logs().d('[VOIP] no stale call checker for room found');
}
@ -70,7 +70,22 @@ extension GroupCallUtils on Room {
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
@ -85,7 +100,7 @@ extension GroupCallUtils on Room {
}
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
await client.oneShotSync();
final copyGroupCallIds =
@ -97,7 +112,7 @@ extension GroupCallUtils on Room {
if (groupCallEvent.content.tryGet('m.intent') == 'm.room') return;
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)
// with expire_ts
bool callExpired = true; // assume call is expired
@ -118,7 +133,7 @@ extension GroupCallUtils on Room {
if (callExpired) {
Logs().i(
'Group call with only expired timestamps detected, terminating');
'[VOIP] Group call with only expired timestamps detected, terminating');
await sendGroupCallTerminateEvent(groupCallId);
}
}
@ -132,7 +147,7 @@ extension GroupCallUtils on Room {
final existingStateEvent =
getState(EventTypes.GroupCallPrefix, groupCallId);
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;
}
@ -144,8 +159,8 @@ extension GroupCallUtils on Room {
Logs().i('[VOIP] Group call $groupCallId was killed uwu');
return req;
} catch (e) {
Logs().e('killing stale call $groupCallId failed. reason: $e');
} catch (e, s) {
Logs().e('[VOIP] killing stale call $groupCallId failed', e, s);
return null;
}
}