in chat/protocols/matrix/lib/matrix-sdk/webrtc/callEventHandler.js [155:335]
async handleCallEvent(event) {
this.client.emit(_client.ClientEvent.ReceivedVoipEvent, event);
const content = event.getContent();
const callRoomId = event.getRoomId() || this.client.groupCallEventHandler.getGroupCallById(content.conf_id)?.room?.roomId;
const groupCallId = content.conf_id;
const type = event.getType();
const senderId = event.getSender();
let call = content.call_id ? this.calls.get(content.call_id) : undefined;
let opponentDeviceId;
let groupCall;
if (groupCallId) {
groupCall = this.client.groupCallEventHandler.getGroupCallById(groupCallId);
if (!groupCall) {
_logger.logger.warn(`CallEventHandler handleCallEvent() could not find a group call - ignoring event (groupCallId=${groupCallId}, type=${type})`);
return;
}
opponentDeviceId = content.device_id;
if (!opponentDeviceId) {
_logger.logger.warn(`CallEventHandler handleCallEvent() could not find a device id - ignoring event (senderId=${senderId})`);
groupCall.emit(_groupCall.GroupCallEvent.Error, new _groupCall.GroupCallUnknownDeviceError(senderId));
return;
}
if (content.dest_session_id !== this.client.getSessionId()) {
_logger.logger.warn("CallEventHandler handleCallEvent() call event does not match current session id - ignoring");
return;
}
}
const weSentTheEvent = senderId === this.client.credentials.userId && (opponentDeviceId === undefined || opponentDeviceId === this.client.getDeviceId());
if (!callRoomId) return;
if (type === _event.EventType.CallInvite) {
// ignore invites you send
if (weSentTheEvent) return;
// expired call
if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD) return;
// stale/old invite event
if (call && call.state === _call.CallState.Ended) return;
if (call) {
_logger.logger.warn(`CallEventHandler handleCallEvent() already has a call but got an invite - clobbering (callId=${content.call_id})`);
}
if (content.invitee && content.invitee !== this.client.getUserId()) {
return; // This invite was meant for another user in the room
}
const timeUntilTurnCresExpire = (this.client.getTurnServersExpiry() ?? 0) - Date.now();
_logger.logger.info("CallEventHandler handleCallEvent() current turn creds expire in " + timeUntilTurnCresExpire + " ms");
call = (0, _call.createNewMatrixCall)(this.client, callRoomId, {
forceTURN: this.client.forceTURN,
opponentDeviceId,
groupCallId,
opponentSessionId: content.sender_session_id
}) ?? undefined;
if (!call) {
_logger.logger.log(`CallEventHandler handleCallEvent() this client does not support WebRTC (callId=${content.call_id})`);
// don't hang up the call: there could be other clients
// connected that do support WebRTC and declining the
// the call on their behalf would be really annoying.
return;
}
call.callId = content.call_id;
const stats = groupCall?.getGroupCallStats();
if (stats) {
call.initStats(stats);
}
try {
await call.initWithInvite(event);
} catch (e) {
if (e instanceof _call.CallError) {
if (e.code === _groupCall.GroupCallErrorCode.UnknownDevice) {
groupCall?.emit(_groupCall.GroupCallEvent.Error, e);
} else {
_logger.logger.error(e);
}
}
}
this.calls.set(call.callId, call);
// if we stashed candidate events for that call ID, play them back now
if (this.candidateEventsByCall.get(call.callId)) {
for (const ev of this.candidateEventsByCall.get(call.callId)) {
call.onRemoteIceCandidatesReceived(ev);
}
}
// Were we trying to call that user (room)?
let existingCall;
for (const thisCall of this.calls.values()) {
const isCalling = [_call.CallState.WaitLocalMedia, _call.CallState.CreateOffer, _call.CallState.InviteSent].includes(thisCall.state);
if (call.roomId === thisCall.roomId && thisCall.direction === _call.CallDirection.Outbound && call.getOpponentMember()?.userId === thisCall.invitee && isCalling) {
existingCall = thisCall;
break;
}
}
if (existingCall) {
if (existingCall.callId > call.callId) {
_logger.logger.log(`CallEventHandler handleCallEvent() detected glare - answering incoming call and canceling outgoing call (incomingId=${call.callId}, outgoingId=${existingCall.callId})`);
existingCall.replacedBy(call);
} else {
_logger.logger.log(`CallEventHandler handleCallEvent() detected glare - hanging up incoming call (incomingId=${call.callId}, outgoingId=${existingCall.callId})`);
call.hangup(_call.CallErrorCode.Replaced, true);
}
} else {
this.client.emit(CallEventHandlerEvent.Incoming, call);
}
return;
} else if (type === _event.EventType.CallCandidates) {
if (weSentTheEvent) return;
if (!call) {
// store the candidates; we may get a call eventually.
if (!this.candidateEventsByCall.has(content.call_id)) {
this.candidateEventsByCall.set(content.call_id, []);
}
this.candidateEventsByCall.get(content.call_id).push(event);
} else {
call.onRemoteIceCandidatesReceived(event);
}
return;
} else if ([_event.EventType.CallHangup, _event.EventType.CallReject].includes(type)) {
// Note that we also observe our own hangups here so we can see
// if we've already rejected a call that would otherwise be valid
if (!call) {
// if not live, store the fact that the call has ended because
// we're probably getting events backwards so
// the hangup will come before the invite
call = (0, _call.createNewMatrixCall)(this.client, callRoomId, {
opponentDeviceId,
opponentSessionId: content.sender_session_id
}) ?? undefined;
if (call) {
call.callId = content.call_id;
call.initWithHangup(event);
this.calls.set(content.call_id, call);
}
} else {
if (call.state !== _call.CallState.Ended) {
if (type === _event.EventType.CallHangup) {
call.onHangupReceived(content);
} else {
call.onRejectReceived(content);
}
// @ts-expect-error typescript thinks the state can't be 'ended' because we're
// inside the if block where it wasn't, but it could have changed because
// on[Hangup|Reject]Received are side-effecty.
if (call.state === _call.CallState.Ended) this.calls.delete(content.call_id);
}
}
return;
}
// The following events need a call and a peer connection
if (!call || !call.hasPeerConnection) {
_logger.logger.info(`CallEventHandler handleCallEvent() discarding possible call event as we don't have a call (type=${type})`);
return;
}
// Ignore remote echo
if (event.getContent().party_id === call.ourPartyId) return;
switch (type) {
case _event.EventType.CallAnswer:
if (weSentTheEvent) {
if (call.state === _call.CallState.Ringing) {
call.onAnsweredElsewhere(content);
}
} else {
call.onAnswerReceived(event);
}
break;
case _event.EventType.CallSelectAnswer:
call.onSelectAnswerReceived(event);
break;
case _event.EventType.CallNegotiate:
call.onNegotiateReceived(event);
break;
case _event.EventType.CallAssertedIdentity:
case _event.EventType.CallAssertedIdentityPrefix:
call.onAssertedIdentityReceived(event);
break;
case _event.EventType.CallSDPStreamMetadataChanged:
case _event.EventType.CallSDPStreamMetadataChangedPrefix:
call.onSDPStreamMetadataChangedReceived(event);
break;
}
}