1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-23 17:02:25 +03:00

Improve TypeScript in MatrixCall

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner
2021-08-28 09:27:47 +02:00
parent b97e3113d1
commit 411fc47f28
2 changed files with 129 additions and 132 deletions

View File

@@ -31,14 +31,21 @@ import { randomString } from '../randomstring';
import { import {
MCallReplacesEvent, MCallReplacesEvent,
MCallAnswer, MCallAnswer,
MCallOfferNegotiate, MCallInviteNegotiate,
CallCapabilities, CallCapabilities,
SDPStreamMetadataPurpose, SDPStreamMetadataPurpose,
SDPStreamMetadata, SDPStreamMetadata,
SDPStreamMetadataKey, SDPStreamMetadataKey,
MCallSDPStreamMetadataChanged, MCallSDPStreamMetadataChanged,
MCallSelectAnswer,
MCAllAssertedIdentity,
MCallCandidates,
MCallBase,
MCallHangupReject,
} from './callEventTypes'; } from './callEventTypes';
import { CallFeed } from './callFeed'; import { CallFeed } from './callFeed';
import { MatrixClient } from "../client";
import { ISendEventResponse } from "../@types/requests";
// events: hangup, error(err), replaced(call), state(state, oldState) // events: hangup, error(err), replaced(call), state(state, oldState)
@@ -261,30 +268,33 @@ function genCallID(): string {
* @param {MatrixClient} opts.client The Matrix Client instance to send events to. * @param {MatrixClient} opts.client The Matrix Client instance to send events to.
*/ */
export class MatrixCall extends EventEmitter { export class MatrixCall extends EventEmitter {
roomId: string; public roomId: string;
type: CallType; public type: CallType = null;
callId: string; public callId: string;
state: CallState; public state = CallState.Fledgling;
hangupParty: CallParty; public hangupParty: CallParty;
hangupReason: string; public hangupReason: string;
direction: CallDirection; public direction: CallDirection;
ourPartyId: string; public ourPartyId: string;
private client: any; // Fix when client is TSified private client: MatrixClient;
private forceTURN: boolean; private forceTURN: boolean;
private turnServers: Array<TurnServer>; private turnServers: Array<TurnServer>;
private candidateSendQueue: Array<RTCIceCandidate>; // A queue for candidates waiting to go out.
private candidateSendTries: number; // We try to amalgamate candidates into a single candidate message where
private sentEndOfCandidates: boolean; // possible
private candidateSendQueue: Array<RTCIceCandidate> = [];
private candidateSendTries = 0;
private sentEndOfCandidates = false;
private peerConn: RTCPeerConnection; private peerConn: RTCPeerConnection;
private feeds: Array<CallFeed>; private feeds: Array<CallFeed> = [];
private usermediaSenders: Array<RTCRtpSender>; private usermediaSenders: Array<RTCRtpSender> = [];
private screensharingSenders: Array<RTCRtpSender>; private screensharingSenders: Array<RTCRtpSender> = [];
private inviteOrAnswerSent: boolean; private inviteOrAnswerSent = false;
private waitForLocalAVStream: boolean; private waitForLocalAVStream: boolean;
private successor: MatrixCall; private successor: MatrixCall;
private opponentMember: RoomMember; private opponentMember: RoomMember;
private opponentVersion: number; private opponentVersion: number | string;
// The party ID of the other side: undefined if we haven't chosen a partner // The party ID of the other side: undefined if we haven't chosen a partner
// yet, null if we have but they didn't send a party ID. // yet, null if we have but they didn't send a party ID.
private opponentPartyId: string; private opponentPartyId: string;
@@ -293,7 +303,7 @@ export class MatrixCall extends EventEmitter {
// The logic of when & if a call is on hold is nontrivial and explained in is*OnHold // The logic of when & if a call is on hold is nontrivial and explained in is*OnHold
// This flag represents whether we want the other party to be on hold // This flag represents whether we want the other party to be on hold
private remoteOnHold; private remoteOnHold = false;
// the stats for the call at the point it ended. We can't get these after we // the stats for the call at the point it ended. We can't get these after we
// tear the call down, so we just grab a snapshot before we stop the call. // tear the call down, so we just grab a snapshot before we stop the call.
@@ -301,7 +311,7 @@ export class MatrixCall extends EventEmitter {
private callStatsAtEnd: any[]; private callStatsAtEnd: any[];
// Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example // Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example
private makingOffer: boolean; private makingOffer = false;
private ignoreOffer: boolean; private ignoreOffer: boolean;
// If candidates arrive before we've picked an opponent (which, in particular, // If candidates arrive before we've picked an opponent (which, in particular,
@@ -317,7 +327,6 @@ export class MatrixCall extends EventEmitter {
super(); super();
this.roomId = opts.roomId; this.roomId = opts.roomId;
this.client = opts.client; this.client = opts.client;
this.type = null;
this.forceTURN = opts.forceTURN; this.forceTURN = opts.forceTURN;
this.ourPartyId = this.client.deviceId; this.ourPartyId = this.client.deviceId;
// Array of Objects with urls, username, credential keys // Array of Objects with urls, username, credential keys
@@ -330,33 +339,14 @@ export class MatrixCall extends EventEmitter {
for (const server of this.turnServers) { for (const server of this.turnServers) {
utils.checkObjectHasKeys(server, ["urls"]); utils.checkObjectHasKeys(server, ["urls"]);
} }
this.callId = genCallID(); this.callId = genCallID();
this.state = CallState.Fledgling;
// A queue for candidates waiting to go out.
// We try to amalgamate candidates into a single candidate message where
// possible
this.candidateSendQueue = [];
this.candidateSendTries = 0;
this.sentEndOfCandidates = false;
this.inviteOrAnswerSent = false;
this.makingOffer = false;
this.remoteOnHold = false;
this.feeds = [];
this.usermediaSenders = [];
this.screensharingSenders = [];
} }
/** /**
* Place a voice call to this room. * Place a voice call to this room.
* @throws If you have not specified a listener for 'error' events. * @throws If you have not specified a listener for 'error' events.
*/ */
async placeVoiceCall() { async placeVoiceCall(): Promise<void> {
logger.debug("placeVoiceCall"); logger.debug("placeVoiceCall");
this.checkForErrorListener(); this.checkForErrorListener();
const constraints = getUserMediaContraints(ConstraintsType.Audio); const constraints = getUserMediaContraints(ConstraintsType.Audio);
@@ -368,7 +358,7 @@ export class MatrixCall extends EventEmitter {
* Place a video call to this room. * Place a video call to this room.
* @throws If you have not specified a listener for 'error' events. * @throws If you have not specified a listener for 'error' events.
*/ */
async placeVideoCall() { async placeVideoCall(): Promise<void> {
logger.debug("placeVideoCall"); logger.debug("placeVideoCall");
this.checkForErrorListener(); this.checkForErrorListener();
const constraints = getUserMediaContraints(ConstraintsType.Video); const constraints = getUserMediaContraints(ConstraintsType.Video);
@@ -376,11 +366,11 @@ export class MatrixCall extends EventEmitter {
await this.placeCallWithConstraints(constraints); await this.placeCallWithConstraints(constraints);
} }
public getOpponentMember() { public getOpponentMember(): RoomMember {
return this.opponentMember; return this.opponentMember;
} }
public opponentCanBeTransferred() { public opponentCanBeTransferred(): boolean {
return Boolean(this.opponentCaps && this.opponentCaps["m.call.transferee"]); return Boolean(this.opponentCaps && this.opponentCaps["m.call.transferee"]);
} }
@@ -462,7 +452,7 @@ export class MatrixCall extends EventEmitter {
return !this.feeds.some((feed) => !feed.isLocal()); return !this.feeds.some((feed) => !feed.isLocal());
} }
private pushRemoteFeed(stream: MediaStream) { private pushRemoteFeed(stream: MediaStream): void {
// Fallback to old behavior if the other side doesn't support SDPStreamMetadata // Fallback to old behavior if the other side doesn't support SDPStreamMetadata
if (!this.opponentSupportsSDPStreamMetadata()) { if (!this.opponentSupportsSDPStreamMetadata()) {
this.pushRemoteFeedWithoutMetadata(stream); this.pushRemoteFeedWithoutMetadata(stream);
@@ -495,7 +485,7 @@ export class MatrixCall extends EventEmitter {
/** /**
* This method is used ONLY if the other client doesn't support sending SDPStreamMetadata * This method is used ONLY if the other client doesn't support sending SDPStreamMetadata
*/ */
private pushRemoteFeedWithoutMetadata(stream: MediaStream) { private pushRemoteFeedWithoutMetadata(stream: MediaStream): void {
const userId = this.getOpponentMember().userId; const userId = this.getOpponentMember().userId;
// We can guess the purpose here since the other client can only send one stream // We can guess the purpose here since the other client can only send one stream
const purpose = SDPStreamMetadataPurpose.Usermedia; const purpose = SDPStreamMetadataPurpose.Usermedia;
@@ -523,7 +513,7 @@ export class MatrixCall extends EventEmitter {
logger.info(`Pushed remote stream (id="${stream.id}", active="${stream.active}")`); logger.info(`Pushed remote stream (id="${stream.id}", active="${stream.active}")`);
} }
private pushLocalFeed(stream: MediaStream, purpose: SDPStreamMetadataPurpose, addToPeerConnection = true) { private pushLocalFeed(stream: MediaStream, purpose: SDPStreamMetadataPurpose, addToPeerConnection = true): void {
const userId = this.client.getUserId(); const userId = this.client.getUserId();
// We try to replace an existing feed if there already is one with the same purpose // We try to replace an existing feed if there already is one with the same purpose
@@ -562,12 +552,12 @@ export class MatrixCall extends EventEmitter {
logger.info(`Pushed local stream (id="${stream.id}", active="${stream.active}", purpose="${purpose}")`); logger.info(`Pushed local stream (id="${stream.id}", active="${stream.active}", purpose="${purpose}")`);
} }
private deleteAllFeeds() { private deleteAllFeeds(): void {
this.feeds = []; this.feeds = [];
this.emit(CallEvent.FeedsChanged, this.feeds); this.emit(CallEvent.FeedsChanged, this.feeds);
} }
private deleteFeedByStream(stream: MediaStream) { private deleteFeedByStream(stream: MediaStream): void {
logger.debug(`Removing feed with stream id ${stream.id}`); logger.debug(`Removing feed with stream id ${stream.id}`);
const feed = this.getFeedByStreamId(stream.id); const feed = this.getFeedByStreamId(stream.id);
@@ -607,8 +597,8 @@ export class MatrixCall extends EventEmitter {
* Configure this call from an invite event. Used by MatrixClient. * Configure this call from an invite event. Used by MatrixClient.
* @param {MatrixEvent} event The m.call.invite event * @param {MatrixEvent} event The m.call.invite event
*/ */
async initWithInvite(event: MatrixEvent) { public async initWithInvite(event: MatrixEvent): Promise<void> {
const invite = event.getContent(); const invite = event.getContent<MCallInviteNegotiate>();
this.direction = CallDirection.Inbound; this.direction = CallDirection.Inbound;
// make sure we have valid turn creds. Unless something's gone wrong, it should // make sure we have valid turn creds. Unless something's gone wrong, it should
@@ -674,7 +664,7 @@ export class MatrixCall extends EventEmitter {
* Configure this call from a hangup or reject event. Used by MatrixClient. * Configure this call from a hangup or reject event. Used by MatrixClient.
* @param {MatrixEvent} event The m.call.hangup event * @param {MatrixEvent} event The m.call.hangup event
*/ */
initWithHangup(event: MatrixEvent) { public initWithHangup(event: MatrixEvent): void {
// perverse as it may seem, sometimes we want to instantiate a call with a // perverse as it may seem, sometimes we want to instantiate a call with a
// hangup message (because when getting the state of the room on load, events // hangup message (because when getting the state of the room on load, events
// come in reverse order and we want to remember that a call has been hung up) // come in reverse order and we want to remember that a call has been hung up)
@@ -684,7 +674,7 @@ export class MatrixCall extends EventEmitter {
/** /**
* Answer a call. * Answer a call.
*/ */
async answer() { public async answer(): Promise<void> {
if (this.inviteOrAnswerSent) { if (this.inviteOrAnswerSent) {
return; return;
} }
@@ -719,7 +709,7 @@ export class MatrixCall extends EventEmitter {
* MatrixClient. * MatrixClient.
* @param {MatrixCall} newCall The new call. * @param {MatrixCall} newCall The new call.
*/ */
replacedBy(newCall: MatrixCall) { public replacedBy(newCall: MatrixCall): void {
if (this.state === CallState.WaitLocalMedia) { if (this.state === CallState.WaitLocalMedia) {
logger.debug("Telling new call to wait for local media"); logger.debug("Telling new call to wait for local media");
newCall.waitForLocalAVStream = true; newCall.waitForLocalAVStream = true;
@@ -737,7 +727,7 @@ export class MatrixCall extends EventEmitter {
* @param {string} reason The reason why the call is being hung up. * @param {string} reason The reason why the call is being hung up.
* @param {boolean} suppressEvent True to suppress emitting an event. * @param {boolean} suppressEvent True to suppress emitting an event.
*/ */
hangup(reason: CallErrorCode, suppressEvent: boolean) { public hangup(reason: CallErrorCode, suppressEvent: boolean): void {
if (this.callHasEnded()) return; if (this.callHasEnded()) return;
logger.debug("Ending call " + this.callId); logger.debug("Ending call " + this.callId);
@@ -757,7 +747,7 @@ export class MatrixCall extends EventEmitter {
* 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.
*/ */
reject() { public reject(): void {
if (this.state !== CallState.Ringing) { if (this.state !== CallState.Ringing) {
throw Error("Call must be in 'ringing' state to reject!"); throw Error("Call must be in 'ringing' state to reject!");
} }
@@ -800,7 +790,7 @@ export class MatrixCall extends EventEmitter {
public async setScreensharingEnabled( public async setScreensharingEnabled(
enabled: boolean, enabled: boolean,
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>, selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
) { ): Promise<boolean> {
// Skip if there is nothing to do // Skip if there is nothing to do
if (enabled && this.isScreensharing()) { if (enabled && this.isScreensharing()) {
logger.warn(`There is already a screensharing stream - there is nothing to do!`); logger.warn(`There is already a screensharing stream - there is nothing to do!`);
@@ -850,7 +840,7 @@ export class MatrixCall extends EventEmitter {
private async setScreensharingEnabledWithoutMetadataSupport( private async setScreensharingEnabledWithoutMetadataSupport(
enabled: boolean, enabled: boolean,
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>, selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
) { ): Promise<boolean> {
logger.debug(`Set screensharing enabled? ${enabled} using replaceTrack()`); logger.debug(`Set screensharing enabled? ${enabled} using replaceTrack()`);
if (enabled) { if (enabled) {
try { try {
@@ -896,7 +886,7 @@ export class MatrixCall extends EventEmitter {
* Set whether our outbound video should be muted or not. * Set whether our outbound video should be muted or not.
* @param {boolean} muted True to mute the outbound video. * @param {boolean} muted True to mute the outbound video.
*/ */
setLocalVideoMuted(muted: boolean) { public setLocalVideoMuted(muted: boolean): void {
this.localUsermediaFeed?.setVideoMuted(muted); this.localUsermediaFeed?.setVideoMuted(muted);
this.updateMuteStatus(); this.updateMuteStatus();
} }
@@ -910,7 +900,7 @@ export class MatrixCall extends EventEmitter {
* @return {Boolean} True if the local preview video is muted, else false * @return {Boolean} True if the local preview video is muted, else false
* (including if the call is not set up yet). * (including if the call is not set up yet).
*/ */
isLocalVideoMuted(): boolean { public isLocalVideoMuted(): boolean {
return this.localUsermediaFeed?.isVideoMuted(); return this.localUsermediaFeed?.isVideoMuted();
} }
@@ -918,7 +908,7 @@ export class MatrixCall extends EventEmitter {
* Set whether the microphone should be muted or not. * Set whether the microphone should be muted or not.
* @param {boolean} muted True to mute the mic. * @param {boolean} muted True to mute the mic.
*/ */
setMicrophoneMuted(muted: boolean) { public setMicrophoneMuted(muted: boolean): void {
this.localUsermediaFeed?.setAudioMuted(muted); this.localUsermediaFeed?.setAudioMuted(muted);
this.updateMuteStatus(); this.updateMuteStatus();
} }
@@ -932,7 +922,7 @@ export class MatrixCall extends EventEmitter {
* @return {Boolean} True if the mic is muted, else false (including if the call * @return {Boolean} True if the mic is muted, else false (including if the call
* is not set up yet). * is not set up yet).
*/ */
isMicrophoneMuted(): boolean { public isMicrophoneMuted(): boolean {
return this.localUsermediaFeed?.isAudioMuted(); return this.localUsermediaFeed?.isAudioMuted();
} }
@@ -940,11 +930,11 @@ export class MatrixCall extends EventEmitter {
* @returns true if we have put the party on the other side of the call on hold * @returns true if we have put the party on the other side of the call on hold
* (that is, we are signalling to them that we are not listening) * (that is, we are signalling to them that we are not listening)
*/ */
isRemoteOnHold(): boolean { public isRemoteOnHold(): boolean {
return this.remoteOnHold; return this.remoteOnHold;
} }
setRemoteOnHold(onHold: boolean) { public setRemoteOnHold(onHold: boolean): void {
if (this.isRemoteOnHold() === onHold) return; if (this.isRemoteOnHold() === onHold) return;
this.remoteOnHold = onHold; this.remoteOnHold = onHold;
@@ -964,7 +954,7 @@ export class MatrixCall extends EventEmitter {
* they cannot hear us). * they cannot hear us).
* @returns true if the other party has put us on hold * @returns true if the other party has put us on hold
*/ */
isLocalOnHold(): boolean { public isLocalOnHold(): boolean {
if (this.state !== CallState.Connected) return false; if (this.state !== CallState.Connected) return false;
let callOnHold = true; let callOnHold = true;
@@ -984,7 +974,7 @@ export class MatrixCall extends EventEmitter {
* Sends a DTMF digit to the other party * Sends a DTMF digit to the other party
* @param digit The digit (nb. string - '#' and '*' are dtmf too) * @param digit The digit (nb. string - '#' and '*' are dtmf too)
*/ */
sendDtmfDigit(digit: string) { public sendDtmfDigit(digit: string): void {
for (const sender of this.peerConn.getSenders()) { for (const sender of this.peerConn.getSenders()) {
if (sender.track.kind === 'audio' && sender.dtmf) { if (sender.track.kind === 'audio' && sender.dtmf) {
sender.dtmf.insertDTMF(digit); sender.dtmf.insertDTMF(digit);
@@ -995,7 +985,7 @@ export class MatrixCall extends EventEmitter {
throw new Error("Unable to find a track to send DTMF on"); throw new Error("Unable to find a track to send DTMF on");
} }
private updateMuteStatus() { private updateMuteStatus(): void {
this.sendVoipEvent(EventType.CallSDPStreamMetadataChangedPrefix, { this.sendVoipEvent(EventType.CallSDPStreamMetadataChangedPrefix, {
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(), [SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(),
}); });
@@ -1011,7 +1001,7 @@ export class MatrixCall extends EventEmitter {
* Internal * Internal
* @param {Object} stream * @param {Object} stream
*/ */
private gotUserMediaForInvite = async (stream: MediaStream) => { private gotUserMediaForInvite = async (stream: MediaStream): Promise<void> => {
if (this.successor) { if (this.successor) {
this.successor.gotUserMediaForAnswer(stream); this.successor.gotUserMediaForAnswer(stream);
return; return;
@@ -1028,7 +1018,7 @@ export class MatrixCall extends EventEmitter {
// Now we wait for the negotiationneeded event // Now we wait for the negotiationneeded event
}; };
private async sendAnswer() { private async sendAnswer(): Promise<void> {
const answerContent = { const answerContent = {
answer: { answer: {
sdp: this.peerConn.localDescription.sdp, sdp: this.peerConn.localDescription.sdp,
@@ -1075,7 +1065,7 @@ export class MatrixCall extends EventEmitter {
this.sendCandidateQueue(); this.sendCandidateQueue();
} }
private gotUserMediaForAnswer = async (stream: MediaStream) => { private gotUserMediaForAnswer = async (stream: MediaStream): Promise<void> => {
if (this.callHasEnded()) { if (this.callHasEnded()) {
return; return;
} }
@@ -1114,7 +1104,7 @@ export class MatrixCall extends EventEmitter {
* Internal * Internal
* @param {Object} event * @param {Object} event
*/ */
private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent) => { private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent): Promise<void> => {
if (event.candidate) { if (event.candidate) {
logger.debug( logger.debug(
"Call " + this.callId + " got local ICE " + event.candidate.sdpMid + " candidate: " + "Call " + this.callId + " got local ICE " + event.candidate.sdpMid + " candidate: " +
@@ -1133,7 +1123,7 @@ export class MatrixCall extends EventEmitter {
} }
}; };
private onIceGatheringStateChange = (event: Event) => { private onIceGatheringStateChange = (event: Event): void => {
logger.debug("ice gathering state changed to " + this.peerConn.iceGatheringState); logger.debug("ice gathering state changed to " + this.peerConn.iceGatheringState);
if (this.peerConn.iceGatheringState === 'complete' && !this.sentEndOfCandidates) { if (this.peerConn.iceGatheringState === 'complete' && !this.sentEndOfCandidates) {
// If we didn't get an empty-string candidate to signal the end of candidates, // If we didn't get an empty-string candidate to signal the end of candidates,
@@ -1151,19 +1141,20 @@ export class MatrixCall extends EventEmitter {
} }
}; };
async onRemoteIceCandidatesReceived(ev: MatrixEvent) { public async onRemoteIceCandidatesReceived(ev: MatrixEvent): Promise<void> {
if (this.callHasEnded()) { if (this.callHasEnded()) {
//debuglog("Ignoring remote ICE candidate because call has ended"); //debuglog("Ignoring remote ICE candidate because call has ended");
return; return;
} }
const candidates = ev.getContent().candidates; const content = ev.getContent<MCallCandidates>();
const candidates = content.candidates;
if (!candidates) { if (!candidates) {
logger.info("Ignoring candidates event with no candidates!"); logger.info("Ignoring candidates event with no candidates!");
return; return;
} }
const fromPartyId = ev.getContent().version === 0 ? null : ev.getContent().party_id || null; const fromPartyId = content.version === 0 ? null : content.party_id || null;
if (this.opponentPartyId === undefined) { if (this.opponentPartyId === undefined) {
// we haven't picked an opponent yet so save the candidates // we haven't picked an opponent yet so save the candidates
@@ -1174,9 +1165,9 @@ export class MatrixCall extends EventEmitter {
return; return;
} }
if (!this.partyIdMatches(ev.getContent())) { if (!this.partyIdMatches(content)) {
logger.info( logger.info(
`Ignoring candidates from party ID ${ev.getContent().party_id}: ` + `Ignoring candidates from party ID ${content.party_id}: ` +
`we have chosen party ID ${this.opponentPartyId}`, `we have chosen party ID ${this.opponentPartyId}`,
); );
@@ -1190,8 +1181,9 @@ export class MatrixCall extends EventEmitter {
* Used by MatrixClient. * Used by MatrixClient.
* @param {Object} msg * @param {Object} msg
*/ */
async onAnswerReceived(event: MatrixEvent) { public async onAnswerReceived(event: MatrixEvent): Promise<void> {
logger.debug(`Got answer for call ID ${this.callId} from party ID ${event.getContent().party_id}`); const content = event.getContent<MCallAnswer>();
logger.debug(`Got answer for call ID ${this.callId} from party ID ${content.party_id}`);
if (this.callHasEnded()) { if (this.callHasEnded()) {
logger.debug(`Ignoring answer because call ID ${this.callId} has ended`); logger.debug(`Ignoring answer because call ID ${this.callId} has ended`);
@@ -1200,7 +1192,7 @@ export class MatrixCall extends EventEmitter {
if (this.opponentPartyId !== undefined) { if (this.opponentPartyId !== undefined) {
logger.info( logger.info(
`Ignoring answer from party ID ${event.getContent().party_id}: ` + `Ignoring answer from party ID ${content.party_id}: ` +
`we already have an answer/reject from ${this.opponentPartyId}`, `we already have an answer/reject from ${this.opponentPartyId}`,
); );
return; return;
@@ -1211,7 +1203,7 @@ export class MatrixCall extends EventEmitter {
this.setState(CallState.Connecting); this.setState(CallState.Connecting);
const sdpStreamMetadata = event.getContent()[SDPStreamMetadataKey]; const sdpStreamMetadata = content[SDPStreamMetadataKey];
if (sdpStreamMetadata) { if (sdpStreamMetadata) {
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata); this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
} else { } else {
@@ -1219,7 +1211,7 @@ export class MatrixCall extends EventEmitter {
} }
try { try {
await this.peerConn.setRemoteDescription(event.getContent().answer); await this.peerConn.setRemoteDescription(content.answer);
} catch (e) { } catch (e) {
logger.debug("Failed to set remote description", e); logger.debug("Failed to set remote description", e);
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false); this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
@@ -1242,13 +1234,13 @@ export class MatrixCall extends EventEmitter {
} }
} }
async onSelectAnswerReceived(event: MatrixEvent) { public async onSelectAnswerReceived(event: MatrixEvent): Promise<void> {
if (this.direction !== CallDirection.Inbound) { if (this.direction !== CallDirection.Inbound) {
logger.warn("Got select_answer for an outbound call: ignoring"); logger.warn("Got select_answer for an outbound call: ignoring");
return; return;
} }
const selectedPartyId = event.getContent().selected_party_id; const selectedPartyId = event.getContent<MCallSelectAnswer>().selected_party_id;
if (selectedPartyId === undefined || selectedPartyId === null) { if (selectedPartyId === undefined || selectedPartyId === null) {
logger.warn("Got nonsensical select_answer with null/undefined selected_party_id: ignoring"); logger.warn("Got nonsensical select_answer with null/undefined selected_party_id: ignoring");
@@ -1262,8 +1254,9 @@ export class MatrixCall extends EventEmitter {
} }
} }
async onNegotiateReceived(event: MatrixEvent) { public async onNegotiateReceived(event: MatrixEvent): Promise<void> {
const description = event.getContent().description; const content = event.getContent<MCallInviteNegotiate>();
const description = content.description;
if (!description || !description.sdp || !description.type) { if (!description || !description.sdp || !description.type) {
logger.info("Ignoring invalid m.call.negotiate event"); logger.info("Ignoring invalid m.call.negotiate event");
return; return;
@@ -1288,7 +1281,7 @@ export class MatrixCall extends EventEmitter {
const prevLocalOnHold = this.isLocalOnHold(); const prevLocalOnHold = this.isLocalOnHold();
const sdpStreamMetadata = event.getContent()[SDPStreamMetadataKey]; const sdpStreamMetadata = content[SDPStreamMetadataKey];
if (sdpStreamMetadata) { if (sdpStreamMetadata) {
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata); this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
} else { } else {
@@ -1336,12 +1329,13 @@ export class MatrixCall extends EventEmitter {
this.updateRemoteSDPStreamMetadata(metadata); this.updateRemoteSDPStreamMetadata(metadata);
} }
async onAssertedIdentityReceived(event: MatrixEvent) { public async onAssertedIdentityReceived(event: MatrixEvent): Promise<void> {
if (!event.getContent().asserted_identity) return; const content = event.getContent<MCAllAssertedIdentity>();
if (!content.asserted_identity) return;
this.remoteAssertedIdentity = { this.remoteAssertedIdentity = {
id: event.getContent().asserted_identity.id, id: content.asserted_identity.id,
displayName: event.getContent().asserted_identity.display_name, displayName: content.asserted_identity.display_name,
}; };
this.emit(CallEvent.AssertedIdentityChanged); this.emit(CallEvent.AssertedIdentityChanged);
} }
@@ -1353,7 +1347,7 @@ export class MatrixCall extends EventEmitter {
return this.state === CallState.Ended; return this.state === CallState.Ended;
} }
private gotLocalOffer = async (description: RTCSessionDescriptionInit) => { private gotLocalOffer = async (description: RTCSessionDescriptionInit): Promise<void> => {
logger.debug("Created offer: ", description); logger.debug("Created offer: ", description);
if (this.callHasEnded()) { if (this.callHasEnded()) {
@@ -1383,7 +1377,7 @@ export class MatrixCall extends EventEmitter {
const content = { const content = {
lifetime: CALL_TIMEOUT_MS, lifetime: CALL_TIMEOUT_MS,
} as MCallOfferNegotiate; } as MCallInviteNegotiate;
// clunky because TypeScript can't follow the types through if we use an expression as the key // clunky because TypeScript can't follow the types through if we use an expression as the key
if (this.state === CallState.CreateOffer) { if (this.state === CallState.CreateOffer) {
@@ -1442,7 +1436,7 @@ export class MatrixCall extends EventEmitter {
} }
}; };
private getLocalOfferFailed = (err: Error) => { private getLocalOfferFailed = (err: Error): void => {
logger.error("Failed to get local offer", err); logger.error("Failed to get local offer", err);
this.emit( this.emit(
@@ -1455,7 +1449,7 @@ export class MatrixCall extends EventEmitter {
this.terminate(CallParty.Local, CallErrorCode.LocalOfferFailed, false); this.terminate(CallParty.Local, CallErrorCode.LocalOfferFailed, false);
}; };
private getUserMediaFailed = (err: Error) => { private getUserMediaFailed = (err: Error): void => {
if (this.successor) { if (this.successor) {
this.successor.getUserMediaFailed(err); this.successor.getUserMediaFailed(err);
return; return;
@@ -1474,7 +1468,7 @@ export class MatrixCall extends EventEmitter {
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false); this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
}; };
onIceConnectionStateChanged = () => { private onIceConnectionStateChanged = (): void => {
if (this.callHasEnded()) { if (this.callHasEnded()) {
return; // because ICE can still complete as we're ending the call return; // because ICE can still complete as we're ending the call
} }
@@ -1490,14 +1484,14 @@ export class MatrixCall extends EventEmitter {
} }
}; };
private onSignallingStateChanged = () => { private onSignallingStateChanged = (): void => {
logger.debug( logger.debug(
"call " + this.callId + ": Signalling state changed to: " + "call " + this.callId + ": Signalling state changed to: " +
this.peerConn.signalingState, this.peerConn.signalingState,
); );
}; };
private onTrack = (ev: RTCTrackEvent) => { private onTrack = (ev: RTCTrackEvent): void => {
if (ev.streams.length === 0) { if (ev.streams.length === 0) {
logger.warn(`Streamless ${ev.track.kind} found: ignoring.`); logger.warn(`Streamless ${ev.track.kind} found: ignoring.`);
return; return;
@@ -1521,7 +1515,7 @@ export class MatrixCall extends EventEmitter {
* [96685:23:0518/162603.933377:ERROR:webrtc_video_engine.cc(1171)] GetChangedRecvParameters called without any video codecs. * [96685:23:0518/162603.933377:ERROR:webrtc_video_engine.cc(1171)] GetChangedRecvParameters called without any video codecs.
* [96685:23:0518/162603.933430:ERROR:sdp_offer_answer.cc(4302)] Failed to set local video description recv parameters for m-section with mid='2'. (INVALID_PARAMETER) * [96685:23:0518/162603.933430:ERROR:sdp_offer_answer.cc(4302)] Failed to set local video description recv parameters for m-section with mid='2'. (INVALID_PARAMETER)
*/ */
private getRidOfRTXCodecs() { private getRidOfRTXCodecs(): void {
// RTCRtpReceiver.getCapabilities and RTCRtpSender.getCapabilities don't seem to be supported on FF // RTCRtpReceiver.getCapabilities and RTCRtpSender.getCapabilities don't seem to be supported on FF
if (!RTCRtpReceiver.getCapabilities || !RTCRtpSender.getCapabilities) return; if (!RTCRtpReceiver.getCapabilities || !RTCRtpSender.getCapabilities) return;
@@ -1549,7 +1543,7 @@ export class MatrixCall extends EventEmitter {
} }
} }
onNegotiationNeeded = async () => { private onNegotiationNeeded = async (): Promise<void> => {
logger.info("Negotiation is needed!"); logger.info("Negotiation is needed!");
if (this.state !== CallState.CreateOffer && this.opponentVersion === 0) { if (this.state !== CallState.CreateOffer && this.opponentVersion === 0) {
@@ -1570,7 +1564,7 @@ export class MatrixCall extends EventEmitter {
} }
}; };
onHangupReceived = (msg) => { public onHangupReceived = (msg: MCallHangupReject): void => {
logger.debug("Hangup received for call ID " + this.callId); logger.debug("Hangup received for call ID " + this.callId);
// party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen // party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen
@@ -1583,7 +1577,7 @@ export class MatrixCall extends EventEmitter {
} }
}; };
onRejectReceived = (msg) => { public onRejectReceived = (msg: MCallHangupReject): void => {
logger.debug("Reject received for call ID " + this.callId); logger.debug("Reject received for call ID " + this.callId);
// No need to check party_id for reject because if we'd received either // No need to check party_id for reject because if we'd received either
@@ -1605,12 +1599,12 @@ export class MatrixCall extends EventEmitter {
} }
}; };
onAnsweredElsewhere = (msg) => { public onAnsweredElsewhere = (msg: MCallAnswer): void => {
logger.debug("Call ID " + this.callId + " answered elsewhere"); logger.debug("Call ID " + this.callId + " answered elsewhere");
this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true); this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true);
}; };
setState(state: CallState) { private setState(state: CallState): void {
const oldState = this.state; const oldState = this.state;
this.state = state; this.state = state;
this.emit(CallEvent.State, state, oldState); this.emit(CallEvent.State, state, oldState);
@@ -1622,7 +1616,7 @@ export class MatrixCall extends EventEmitter {
* @param {Object} content * @param {Object} content
* @return {Promise} * @return {Promise}
*/ */
private sendVoipEvent(eventType: string, content: object) { private sendVoipEvent(eventType: string, content: object): Promise<ISendEventResponse> {
return this.client.sendEvent(this.roomId, eventType, Object.assign({}, content, { return this.client.sendEvent(this.roomId, eventType, Object.assign({}, content, {
version: VOIP_PROTO_VERSION, version: VOIP_PROTO_VERSION,
call_id: this.callId, call_id: this.callId,
@@ -1630,7 +1624,7 @@ export class MatrixCall extends EventEmitter {
})); }));
} }
queueCandidate(content: RTCIceCandidate) { private queueCandidate(content: RTCIceCandidate): void {
// We partially de-trickle candidates by waiting for `delay` before sending them // We partially de-trickle candidates by waiting for `delay` before sending them
// amalgamated, in order to avoid sending too many m.call.candidates events and hitting // amalgamated, in order to avoid sending too many m.call.candidates events and hitting
// rate limits in Matrix. // rate limits in Matrix.
@@ -1664,7 +1658,7 @@ export class MatrixCall extends EventEmitter {
/* /*
* Transfers this call to another user * Transfers this call to another user
*/ */
async transfer(targetUserId: string) { public async transfer(targetUserId: string): Promise<void> {
// Fetch the target user's global profile info: their room avatar / displayname // Fetch the target user's global profile info: their room avatar / displayname
// could be different in whatever room we share with them. // could be different in whatever room we share with them.
const profileInfo = await this.client.getProfileInfo(targetUserId); const profileInfo = await this.client.getProfileInfo(targetUserId);
@@ -1675,7 +1669,7 @@ export class MatrixCall extends EventEmitter {
replacement_id: genCallID(), replacement_id: genCallID(),
target_user: { target_user: {
id: targetUserId, id: targetUserId,
display_name: profileInfo.display_name, display_name: profileInfo.displayname,
avatar_url: profileInfo.avatar_url, avatar_url: profileInfo.avatar_url,
}, },
create_call: replacementId, create_call: replacementId,
@@ -1690,7 +1684,7 @@ export class MatrixCall extends EventEmitter {
* Transfers this call to the target call, effectively 'joining' the * Transfers this call to the target call, effectively 'joining' the
* two calls (so the remote parties on each call are connected together). * two calls (so the remote parties on each call are connected together).
*/ */
async transferToCall(transferTargetCall?: MatrixCall) { public async transferToCall(transferTargetCall?: MatrixCall): Promise<void> {
const targetProfileInfo = await this.client.getProfileInfo(transferTargetCall.getOpponentMember().userId); const targetProfileInfo = await this.client.getProfileInfo(transferTargetCall.getOpponentMember().userId);
const transfereeProfileInfo = await this.client.getProfileInfo(this.getOpponentMember().userId); const transfereeProfileInfo = await this.client.getProfileInfo(this.getOpponentMember().userId);
@@ -1702,7 +1696,7 @@ export class MatrixCall extends EventEmitter {
replacement_id: genCallID(), replacement_id: genCallID(),
target_user: { target_user: {
id: this.getOpponentMember().userId, id: this.getOpponentMember().userId,
display_name: transfereeProfileInfo.display_name, display_name: transfereeProfileInfo.displayname,
avatar_url: transfereeProfileInfo.avatar_url, avatar_url: transfereeProfileInfo.avatar_url,
}, },
await_call: newCallId, await_call: newCallId,
@@ -1714,7 +1708,7 @@ export class MatrixCall extends EventEmitter {
replacement_id: genCallID(), replacement_id: genCallID(),
target_user: { target_user: {
id: transferTargetCall.getOpponentMember().userId, id: transferTargetCall.getOpponentMember().userId,
display_name: targetProfileInfo.display_name, display_name: targetProfileInfo.displayname,
avatar_url: targetProfileInfo.avatar_url, avatar_url: targetProfileInfo.avatar_url,
}, },
create_call: newCallId, create_call: newCallId,
@@ -1726,7 +1720,7 @@ export class MatrixCall extends EventEmitter {
await transferTargetCall.terminate(CallParty.Local, CallErrorCode.Transfered, true); await transferTargetCall.terminate(CallParty.Local, CallErrorCode.Transfered, true);
} }
private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean) { private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean): Promise<void> {
if (this.callHasEnded()) return; if (this.callHasEnded()) return;
this.callStatsAtEnd = await this.collectCallStats(); this.callStatsAtEnd = await this.collectCallStats();
@@ -1752,7 +1746,7 @@ export class MatrixCall extends EventEmitter {
} }
} }
private stopAllMedia() { private stopAllMedia(): void {
logger.debug(`stopAllMedia (stream=${this.localUsermediaStream})`); logger.debug(`stopAllMedia (stream=${this.localUsermediaStream})`);
for (const feed of this.feeds) { for (const feed of this.feeds) {
@@ -1762,7 +1756,7 @@ export class MatrixCall extends EventEmitter {
} }
} }
private checkForErrorListener() { private checkForErrorListener(): void {
if (this.listeners("error").length === 0) { if (this.listeners("error").length === 0) {
throw new Error( throw new Error(
"You MUST attach an error listener using call.on('error', function() {})", "You MUST attach an error listener using call.on('error', function() {})",
@@ -1770,7 +1764,7 @@ export class MatrixCall extends EventEmitter {
} }
} }
private async sendCandidateQueue() { private async sendCandidateQueue(): Promise<void> {
if (this.candidateSendQueue.length === 0) { if (this.candidateSendQueue.length === 0) {
return; return;
} }
@@ -1819,7 +1813,7 @@ export class MatrixCall extends EventEmitter {
} }
} }
private async placeCallWithConstraints(constraints: MediaStreamConstraints) { private async placeCallWithConstraints(constraints: MediaStreamConstraints): Promise<void> {
logger.log("Getting user media with constraints", constraints); logger.log("Getting user media with constraints", constraints);
// XXX Find a better way to do this // XXX Find a better way to do this
this.client.callEventHandler.calls.set(this.callId, this); this.client.callEventHandler.calls.set(this.callId, this);
@@ -1864,7 +1858,7 @@ export class MatrixCall extends EventEmitter {
return pc; return pc;
} }
private partyIdMatches(msg): boolean { private partyIdMatches(msg: MCallBase): boolean {
// They must either match or both be absent (in which case opponentPartyId will be null) // They must either match or both be absent (in which case opponentPartyId will be null)
// Also we ignore party IDs on the invite/offer if the version is 0, so we must do the same // Also we ignore party IDs on the invite/offer if the version is 0, so we must do the same
// here and use null if the version is 0 (woe betide any opponent sending messages in the // here and use null if the version is 0 (woe betide any opponent sending messages in the
@@ -1875,9 +1869,9 @@ export class MatrixCall extends EventEmitter {
// Commits to an opponent for the call // Commits to an opponent for the call
// ev: An invite or answer event // ev: An invite or answer event
private chooseOpponent(ev: MatrixEvent) { private chooseOpponent(ev: MatrixEvent): void {
// I choo-choo-choose you // I choo-choo-choose you
const msg = ev.getContent(); const msg = ev.getContent<MCallInviteNegotiate | MCallAnswer>();
logger.debug(`Choosing party ID ${msg.party_id} for call ID ${this.callId}`); logger.debug(`Choosing party ID ${msg.party_id} for call ID ${this.callId}`);
@@ -1892,11 +1886,11 @@ export class MatrixCall extends EventEmitter {
// party ID // party ID
this.opponentPartyId = msg.party_id || null; this.opponentPartyId = msg.party_id || null;
} }
this.opponentCaps = msg.capabilities || {}; this.opponentCaps = msg.capabilities || {} as CallCapabilities;
this.opponentMember = ev.sender; this.opponentMember = ev.sender;
} }
private async addBufferedIceCandidates() { private async addBufferedIceCandidates(): Promise<void> {
const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId); const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId);
if (bufferedCandidates) { if (bufferedCandidates) {
logger.info(`Adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`); logger.info(`Adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`);
@@ -1905,7 +1899,7 @@ export class MatrixCall extends EventEmitter {
this.remoteCandidateBuffer = null; this.remoteCandidateBuffer = null;
} }
private async addIceCandidates(candidates: RTCIceCandidate[]) { private async addIceCandidates(candidates: RTCIceCandidate[]): Promise<void> {
for (const candidate of candidates) { for (const candidate of candidates) {
if ( if (
(candidate.sdpMid === null || candidate.sdpMid === undefined) && (candidate.sdpMid === null || candidate.sdpMid === undefined) &&
@@ -1927,7 +1921,7 @@ export class MatrixCall extends EventEmitter {
} }
} }
public get hasPeerConnection() { public get hasPeerConnection(): boolean {
return Boolean(this.peerConn); return Boolean(this.peerConn);
} }
} }
@@ -1949,13 +1943,13 @@ async function getScreensharingStream(
} }
} }
function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) { function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean): void {
for (let i = 0; i < tracks.length; i++) { for (let i = 0; i < tracks.length; i++) {
tracks[i].enabled = enabled; tracks[i].enabled = enabled;
} }
} }
function getUserMediaContraints(type: ConstraintsType) { function getUserMediaContraints(type: ConstraintsType): MediaStreamConstraints {
const isWebkit = !!navigator.webkitGetUserMedia; const isWebkit = !!navigator.webkitGetUserMedia;
switch (type) { switch (type) {
@@ -1986,7 +1980,9 @@ function getUserMediaContraints(type: ConstraintsType) {
} }
} }
async function getScreenshareContraints(selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>) { async function getScreenshareContraints(
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
): Promise<DesktopCapturerConstraints> {
if (window.electron?.getDesktopCapturerSources && selectDesktopCapturerSource) { if (window.electron?.getDesktopCapturerSources && selectDesktopCapturerSource) {
// We have access to getDesktopCapturerSources() // We have access to getDesktopCapturerSources()
logger.debug("Electron getDesktopCapturerSources() is available..."); logger.debug("Electron getDesktopCapturerSources() is available...");
@@ -2020,14 +2016,14 @@ let videoInput: string;
* @param {string=} deviceId the identifier for the device * @param {string=} deviceId the identifier for the device
* undefined treated as unset * undefined treated as unset
*/ */
export function setAudioInput(deviceId: string) { audioInput = deviceId; } export function setAudioInput(deviceId: string): void { audioInput = deviceId; }
/** /**
* Set a video input device to use for MatrixCalls * Set a video input device to use for MatrixCalls
* @function * @function
* @param {string=} deviceId the identifier for the device * @param {string=} deviceId the identifier for the device
* undefined treated as unset * undefined treated as unset
*/ */
export function setVideoInput(deviceId: string) { videoInput = deviceId; } export function setVideoInput(deviceId: string): void { videoInput = deviceId; }
/** /**
* DEPRECATED * DEPRECATED
@@ -2042,7 +2038,7 @@ export function setVideoInput(deviceId: string) { videoInput = deviceId; }
* since it's only possible to set this option on outbound calls. * since it's only possible to set this option on outbound calls.
* @return {MatrixCall} the call or null if the browser doesn't support calling. * @return {MatrixCall} the call or null if the browser doesn't support calling.
*/ */
export function createNewMatrixCall(client: any, roomId: string, options?: CallOpts) { export function createNewMatrixCall(client: any, roomId: string, options?: CallOpts): MatrixCall {
// typeof prevents Node from erroring on an undefined reference // typeof prevents Node from erroring on an undefined reference
if (typeof(window) === 'undefined' || typeof(document) === 'undefined') { if (typeof(window) === 'undefined' || typeof(document) === 'undefined') {
// NB. We don't log here as apps try to create a call object as a test for // NB. We don't log here as apps try to create a call object as a test for

View File

@@ -19,6 +19,7 @@ import { logger } from '../logger';
import { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call'; import { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call';
import { EventType } from '../@types/event'; import { EventType } from '../@types/event';
import { MatrixClient } from '../client'; import { MatrixClient } from '../client';
import { MCallAnswer, MCallHangupReject } from "./callEventTypes";
// Don't ring unless we'd be ringing for at least 3 seconds: the user needs some // Don't ring unless we'd be ringing for at least 3 seconds: the user needs some
// time to press the 'accept' button // time to press the 'accept' button
@@ -252,9 +253,9 @@ export class CallEventHandler {
} else { } else {
if (call.state !== CallState.Ended) { if (call.state !== CallState.Ended) {
if (type === EventType.CallHangup) { if (type === EventType.CallHangup) {
call.onHangupReceived(content); call.onHangupReceived(content as MCallHangupReject);
} else { } else {
call.onRejectReceived(content); call.onRejectReceived(content as MCallHangupReject);
} }
this.calls.delete(content.call_id); this.calls.delete(content.call_id);
} }
@@ -274,7 +275,7 @@ export class CallEventHandler {
case EventType.CallAnswer: case EventType.CallAnswer:
if (weSentTheEvent) { if (weSentTheEvent) {
if (call.state === CallState.Ringing) { if (call.state === CallState.Ringing) {
call.onAnsweredElsewhere(content); call.onAnsweredElsewhere(content as MCallAnswer);
} }
} else { } else {
call.onAnswerReceived(event); call.onAnswerReceived(event);