constructor()

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;
  }