in chat/protocols/matrix/lib/matrix-sdk/webrtc/call.js [187:460]
constructor(opts) {
super();
_defineProperty(this, "roomId", void 0);
_defineProperty(this, "callId", void 0);
_defineProperty(this, "invitee", void 0);
_defineProperty(this, "hangupParty", void 0);
_defineProperty(this, "hangupReason", void 0);
_defineProperty(this, "direction", void 0);
_defineProperty(this, "ourPartyId", void 0);
_defineProperty(this, "peerConn", void 0);
_defineProperty(this, "toDeviceSeq", 0);
// whether this call should have push-to-talk semantics
// This should be set by the consumer on incoming & outgoing calls.
_defineProperty(this, "isPtt", false);
_defineProperty(this, "_state", CallState.Fledgling);
_defineProperty(this, "client", void 0);
_defineProperty(this, "forceTURN", void 0);
_defineProperty(this, "turnServers", void 0);
// A queue for candidates waiting to go out.
// We try to amalgamate candidates into a single candidate message where
// possible
_defineProperty(this, "candidateSendQueue", []);
_defineProperty(this, "candidateSendTries", 0);
_defineProperty(this, "candidatesEnded", false);
_defineProperty(this, "feeds", []);
// our transceivers for each purpose and type of media
_defineProperty(this, "transceivers", new Map());
_defineProperty(this, "inviteOrAnswerSent", false);
_defineProperty(this, "waitForLocalAVStream", false);
_defineProperty(this, "successor", void 0);
_defineProperty(this, "opponentMember", void 0);
_defineProperty(this, "opponentVersion", void 0);
// 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.
_defineProperty(this, "opponentPartyId", void 0);
_defineProperty(this, "opponentCaps", void 0);
_defineProperty(this, "iceDisconnectedTimeout", void 0);
_defineProperty(this, "iceReconnectionTimeOut", void 0);
_defineProperty(this, "inviteTimeout", void 0);
_defineProperty(this, "removeTrackListeners", new Map());
// 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
_defineProperty(this, "remoteOnHold", false);
// 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.
// The typescript definitions have this type as 'any' :(
_defineProperty(this, "callStatsAtEnd", void 0);
// Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example
_defineProperty(this, "makingOffer", false);
_defineProperty(this, "ignoreOffer", false);
_defineProperty(this, "isSettingRemoteAnswerPending", false);
_defineProperty(this, "responsePromiseChain", void 0);
// If candidates arrive before we've picked an opponent (which, in particular,
// will happen if the opponent sends candidates eagerly before the user answers
// the call) we buffer them up here so we can then add the ones from the party we pick
_defineProperty(this, "remoteCandidateBuffer", new Map());
_defineProperty(this, "remoteAssertedIdentity", void 0);
_defineProperty(this, "remoteSDPStreamMetadata", void 0);
_defineProperty(this, "callLengthInterval", void 0);
_defineProperty(this, "callStartTime", void 0);
_defineProperty(this, "opponentDeviceId", void 0);
_defineProperty(this, "opponentDeviceInfo", void 0);
_defineProperty(this, "opponentSessionId", void 0);
_defineProperty(this, "groupCallId", void 0);
// Used to keep the timer for the delay before actually stopping our
// video track after muting (see setLocalVideoMuted)
_defineProperty(this, "stopVideoTrackTimer", void 0);
// Used to allow connection without Video and Audio. To establish a webrtc connection without media a Data channel is
// needed At the moment this property is true if we allow MatrixClient with isVoipWithNoMediaAllowed = true
_defineProperty(this, "isOnlyDataChannelAllowed", void 0);
_defineProperty(this, "stats", void 0);
/**
* Internal
*/
_defineProperty(this, "gotLocalIceCandidate", event => {
if (event.candidate) {
if (this.candidatesEnded) {
_logger.logger.warn(`Call ${this.callId} gotLocalIceCandidate() got candidate after candidates have ended!`);
}
_logger.logger.debug(`Call ${this.callId} got local ICE ${event.candidate.sdpMid} ${event.candidate.candidate}`);
if (this.callHasEnded()) return;
// As with the offer, note we need to make a copy of this object, not
// pass the original: that broke in Chrome ~m43.
if (event.candidate.candidate === "") {
this.queueCandidate(null);
} else {
this.queueCandidate(event.candidate);
}
}
});
_defineProperty(this, "onIceGatheringStateChange", event => {
_logger.logger.debug(`Call ${this.callId} onIceGatheringStateChange() ice gathering state changed to ${this.peerConn.iceGatheringState}`);
if (this.peerConn?.iceGatheringState === "complete") {
this.queueCandidate(null); // We should leave it to WebRTC to announce the end
_logger.logger.debug(`Call ${this.callId} onIceGatheringStateChange() ice gathering state complete, set candidates have ended`);
}
});
_defineProperty(this, "getLocalOfferFailed", err => {
_logger.logger.error(`Call ${this.callId} getLocalOfferFailed() running`, err);
this.emit(CallEvent.Error, new CallError(CallErrorCode.LocalOfferFailed, "Failed to get local offer!", err), this);
this.terminate(CallParty.Local, CallErrorCode.LocalOfferFailed, false);
});
_defineProperty(this, "getUserMediaFailed", err => {
if (this.successor) {
this.successor.getUserMediaFailed(err);
return;
}
_logger.logger.warn(`Call ${this.callId} getUserMediaFailed() failed to get user media - ending call`, err);
this.emit(CallEvent.Error, new CallError(CallErrorCode.NoUserMedia, "Couldn't start capturing media! Is your microphone set up and does this app have permission?", err), this);
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
});
_defineProperty(this, "placeCallFailed", err => {
if (this.successor) {
this.successor.placeCallFailed(err);
return;
}
_logger.logger.warn(`Call ${this.callId} placeCallWithCallFeeds() failed - ending call`, err);
this.emit(CallEvent.Error, new CallError(CallErrorCode.IceFailed, "Couldn't start call! Invalid ICE server configuration.", err), this);
this.terminate(CallParty.Local, CallErrorCode.IceFailed, false);
});
_defineProperty(this, "onIceConnectionStateChanged", () => {
if (this.callHasEnded()) {
return; // because ICE can still complete as we're ending the call
}
_logger.logger.debug(`Call ${this.callId} onIceConnectionStateChanged() running (state=${this.peerConn?.iceConnectionState}, conn=${this.peerConn?.connectionState})`);
// ideally we'd consider the call to be connected when we get media but
// chrome doesn't implement any of the 'onstarted' events yet
if (["connected", "completed"].includes(this.peerConn?.iceConnectionState ?? "")) {
clearTimeout(this.iceDisconnectedTimeout);
this.iceDisconnectedTimeout = undefined;
if (this.iceReconnectionTimeOut) {
clearTimeout(this.iceReconnectionTimeOut);
}
this.state = CallState.Connected;
if (!this.callLengthInterval && !this.callStartTime) {
this.callStartTime = Date.now();
this.callLengthInterval = setInterval(() => {
this.emit(CallEvent.LengthChanged, Math.round((Date.now() - this.callStartTime) / 1000), this);
}, CALL_LENGTH_INTERVAL);
}
} else if (this.peerConn?.iceConnectionState == "failed") {
this.candidatesEnded = false;
// Firefox for Android does not yet have support for restartIce()
// (the types say it's always defined though, so we have to cast
// to prevent typescript from warning).
if (this.peerConn?.restartIce) {
this.candidatesEnded = false;
_logger.logger.debug(`Call ${this.callId} onIceConnectionStateChanged() ice restart (state=${this.peerConn?.iceConnectionState})`);
this.peerConn.restartIce();
} else {
_logger.logger.info(`Call ${this.callId} onIceConnectionStateChanged() hanging up call (ICE failed and no ICE restart method)`);
this.hangup(CallErrorCode.IceFailed, false);
}
} else if (this.peerConn?.iceConnectionState == "disconnected") {
this.candidatesEnded = false;
this.iceReconnectionTimeOut = setTimeout(() => {
_logger.logger.info(`Call ${this.callId} onIceConnectionStateChanged() ICE restarting because of ICE disconnected, (state=${this.peerConn?.iceConnectionState}, conn=${this.peerConn?.connectionState})`);
if (this.peerConn?.restartIce) {
this.candidatesEnded = false;
this.peerConn.restartIce();
}
this.iceReconnectionTimeOut = undefined;
}, ICE_RECONNECTING_TIMEOUT);
this.iceDisconnectedTimeout = setTimeout(() => {
_logger.logger.info(`Call ${this.callId} onIceConnectionStateChanged() hanging up call (ICE disconnected for too long)`);
this.hangup(CallErrorCode.IceFailed, false);
}, ICE_DISCONNECTED_TIMEOUT);
this.state = CallState.Connecting;
}
// In PTT mode, override feed status to muted when we lose connection to
// the peer, since we don't want to block the line if they're not saying anything.
// Experimenting in Chrome, this happens after 5 or 6 seconds, which is probably
// fast enough.
if (this.isPtt && ["failed", "disconnected"].includes(this.peerConn.iceConnectionState)) {
for (const feed of this.getRemoteFeeds()) {
feed.setAudioVideoMuted(true, true);
}
}
});
_defineProperty(this, "onSignallingStateChanged", () => {
_logger.logger.debug(`Call ${this.callId} onSignallingStateChanged() running (state=${this.peerConn?.signalingState})`);
});
_defineProperty(this, "onTrack", ev => {
if (ev.streams.length === 0) {
_logger.logger.warn(`Call ${this.callId} onTrack() called with streamless track streamless (kind=${ev.track.kind})`);
return;
}
const stream = ev.streams[0];
this.pushRemoteFeed(stream);
if (!this.removeTrackListeners.has(stream)) {
const onRemoveTrack = () => {
if (stream.getTracks().length === 0) {
_logger.logger.info(`Call ${this.callId} onTrack() removing track (streamId=${stream.id})`);
this.deleteFeedByStream(stream);
stream.removeEventListener("removetrack", onRemoveTrack);
this.removeTrackListeners.delete(stream);
}
};
stream.addEventListener("removetrack", onRemoveTrack);
this.removeTrackListeners.set(stream, onRemoveTrack);
}
});
_defineProperty(this, "onDataChannel", ev => {
this.emit(CallEvent.DataChannel, ev.channel, this);
});
_defineProperty(this, "onNegotiationNeeded", async () => {
_logger.logger.info(`Call ${this.callId} onNegotiationNeeded() negotiation is needed!`);
if (this.state !== CallState.CreateOffer && this.opponentVersion === 0) {
_logger.logger.info(`Call ${this.callId} onNegotiationNeeded() opponent does not support renegotiation: ignoring negotiationneeded event`);
return;
}
this.queueGotLocalOffer();
});
_defineProperty(this, "onHangupReceived", msg => {
_logger.logger.debug(`Call ${this.callId} onHangupReceived() running`);
// party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen
// a partner yet but we're treating the hangup as a reject as per VoIP v0)
if (this.partyIdMatches(msg) || this.state === CallState.Ringing) {
// default reason is user_hangup
this.terminate(CallParty.Remote, msg.reason || CallErrorCode.UserHangup, true);
} else {
_logger.logger.info(`Call ${this.callId} onHangupReceived() ignoring message from party ID ${msg.party_id}: our partner is ${this.opponentPartyId}`);
}
});
_defineProperty(this, "onRejectReceived", msg => {
_logger.logger.debug(`Call ${this.callId} onRejectReceived() running`);
// No need to check party_id for reject because if we'd received either
// an answer or reject, we wouldn't be in state InviteSent
const shouldTerminate =
// reject events also end the call if it's ringing: it's another of
// our devices rejecting the call.
[CallState.InviteSent, CallState.Ringing].includes(this.state) ||
// also if we're in the init state and it's an inbound call, since
// this means we just haven't entered the ringing state yet
this.state === CallState.Fledgling && this.direction === CallDirection.Inbound;
if (shouldTerminate) {
this.terminate(CallParty.Remote, msg.reason || CallErrorCode.UserHangup, true);
} else {
_logger.logger.debug(`Call ${this.callId} onRejectReceived() called in wrong state (state=${this.state})`);
}
});
_defineProperty(this, "onAnsweredElsewhere", msg => {
_logger.logger.debug(`Call ${this.callId} onAnsweredElsewhere() running`);
this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true);
});
this.roomId = opts.roomId;
this.invitee = opts.invitee;
this.client = opts.client;
if (!this.client.deviceId) throw new Error("Client must have a device ID to start calls");
this.forceTURN = opts.forceTURN ?? false;
this.ourPartyId = this.client.deviceId;
this.opponentDeviceId = opts.opponentDeviceId;
this.opponentSessionId = opts.opponentSessionId;
this.groupCallId = opts.groupCallId;
// Array of Objects with urls, username, credential keys
this.turnServers = opts.turnServers || [];
if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
this.turnServers.push({
urls: [FALLBACK_ICE_SERVER]
});
}
for (const server of this.turnServers) {
(0, _utils.checkObjectHasKeys)(server, ["urls"]);
}
this.callId = genCallID();
// If the Client provides calls without audio and video we need a datachannel for a webrtc connection
this.isOnlyDataChannelAllowed = this.client.isVoipWithNoMediaAllowed;
}