UNPKG

@larva.io/webcomponents

Version:

Fentrica SmartUnits WebComponents package

1,077 lines (1,068 loc) 832 kB
/*! * (C) Fentrica http://fentrica.com - Seee LICENSE.md */ import { r as registerInstance, c as createEvent, h, g as getElement } from './index-C4h1muVj.js'; import { C as Camera, D as DEFAULT_UPLINK } from './camera-CpTI1_g1.js'; import './global-C56buD75.js'; import './_commonjsHelpers-BhmlBuJX.js'; /** * Function which returns a MediaStreamFactory. * @public */ function defaultMediaStreamFactory() { return (constraints) => { // if no audio or video, return a media stream without tracks if (!constraints.audio && !constraints.video) { return Promise.resolve(new MediaStream()); } // getUserMedia() is a powerful feature which can only be used in secure contexts; in insecure contexts, // navigator.mediaDevices is undefined, preventing access to getUserMedia(). A secure context is, in short, // a page loaded using HTTPS or the file:/// URL scheme, or a page loaded from localhost. // https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Privacy_and_security if (navigator.mediaDevices === undefined) { return Promise.reject(new Error("Media devices not available in insecure contexts.")); } return navigator.mediaDevices.getUserMedia.call(navigator.mediaDevices, constraints); }; } /** * Function which returns an RTCConfiguration. * @public */ function defaultPeerConnectionConfiguration() { const configuration = { bundlePolicy: "balanced", certificates: undefined, iceCandidatePoolSize: 0, iceServers: [{ urls: "stun:stun.l.google.com:19302" }], iceTransportPolicy: "all", rtcpMuxPolicy: "require" }; return configuration; } /** * A base class implementing a WebRTC session description handler for sip.js. * @remarks * It is expected/intended to be extended by specific WebRTC based applications. * @privateRemarks * So do not put application specific implementation in here. * @public */ class SessionDescriptionHandler { /** * Constructor * @param logger - A logger * @param mediaStreamFactory - A factory to provide a MediaStream * @param options - Options passed from the SessionDescriptionHandleFactory */ constructor(logger, mediaStreamFactory, sessionDescriptionHandlerConfiguration) { logger.debug("SessionDescriptionHandler.constructor"); this.logger = logger; this.mediaStreamFactory = mediaStreamFactory; this.sessionDescriptionHandlerConfiguration = sessionDescriptionHandlerConfiguration; this._localMediaStream = new MediaStream(); this._remoteMediaStream = new MediaStream(); this._peerConnection = new RTCPeerConnection(sessionDescriptionHandlerConfiguration === null || sessionDescriptionHandlerConfiguration === void 0 ? void 0 : sessionDescriptionHandlerConfiguration.peerConnectionConfiguration); this.initPeerConnectionEventHandlers(); } /** * The local media stream currently being sent. * * @remarks * The local media stream initially has no tracks, so the presence of tracks * should not be assumed. Furthermore, tracks may be added or removed if the * local media changes - for example, on upgrade from audio only to a video session. * At any given time there will be at most one audio track and one video track * (it's possible that this restriction may not apply to sub-classes). * Use `MediaStream.onaddtrack` or add a listener for the `addtrack` event * to detect when a new track becomes available: * https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/onaddtrack */ get localMediaStream() { return this._localMediaStream; } /** * The remote media stream currently being received. * * @remarks * The remote media stream initially has no tracks, so the presence of tracks * should not be assumed. Furthermore, tracks may be added or removed if the * remote media changes - for example, on upgrade from audio only to a video session. * At any given time there will be at most one audio track and one video track * (it's possible that this restriction may not apply to sub-classes). * Use `MediaStream.onaddtrack` or add a listener for the `addtrack` event * to detect when a new track becomes available: * https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/onaddtrack */ get remoteMediaStream() { return this._remoteMediaStream; } /** * The data channel. Undefined before it is created. */ get dataChannel() { return this._dataChannel; } /** * The peer connection. Undefined if peer connection has closed. * * @remarks * Use the peerConnectionDelegate to get access to the events associated * with the RTCPeerConnection. For example... * * Do NOT do this... * ```ts * peerConnection.onicecandidate = (event) => { * // do something * }; * ``` * Instead, do this... * ```ts * peerConnection.peerConnectionDelegate = { * onicecandidate: (event) => { * // do something * } * }; * ``` * While access to the underlying `RTCPeerConnection` is provided, note that * using methods which modify it may break the operation of this class. * In particular, this class depends on exclusive access to the * event handler properties. If you need access to the peer connection * events, either register for events using `addEventListener()` on * the `RTCPeerConnection` or set the `peerConnectionDelegate` on * this `SessionDescriptionHandler`. */ get peerConnection() { return this._peerConnection; } /** * A delegate which provides access to the peer connection event handlers. * * @remarks * Use the peerConnectionDelegate to get access to the events associated * with the RTCPeerConnection. For example... * * Do NOT do this... * ```ts * peerConnection.onicecandidate = (event) => { * // do something * }; * ``` * Instead, do this... * ``` * peerConnection.peerConnectionDelegate = { * onicecandidate: (event) => { * // do something * } * }; * ``` * Setting the peer connection event handlers directly is not supported * and may break this class. As this class depends on exclusive access * to them. This delegate is intended to provide access to the * RTCPeerConnection events in a fashion which is supported. */ get peerConnectionDelegate() { return this._peerConnectionDelegate; } set peerConnectionDelegate(delegate) { this._peerConnectionDelegate = delegate; } // The addtrack event does not get fired when JavaScript code explicitly adds tracks to the stream (by calling addTrack()). // https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/onaddtrack static dispatchAddTrackEvent(stream, track) { stream.dispatchEvent(new MediaStreamTrackEvent("addtrack", { track })); } // The removetrack event does not get fired when JavaScript code explicitly removes tracks from the stream (by calling removeTrack()). // https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/onremovetrack static dispatchRemoveTrackEvent(stream, track) { stream.dispatchEvent(new MediaStreamTrackEvent("removetrack", { track })); } /** * Stop tracks and close peer connection. */ close() { this.logger.debug("SessionDescriptionHandler.close"); if (this._peerConnection === undefined) { return; } this._peerConnection.getReceivers().forEach((receiver) => { receiver.track && receiver.track.stop(); }); this._peerConnection.getSenders().forEach((sender) => { sender.track && sender.track.stop(); }); if (this._dataChannel) { this._dataChannel.close(); } this._peerConnection.close(); this._peerConnection = undefined; } /** * Helper function to enable/disable media tracks. * @param enable - If true enable tracks, otherwise disable tracks. */ enableReceiverTracks(enable) { const peerConnection = this.peerConnection; if (!peerConnection) { throw new Error("Peer connection closed."); } peerConnection.getReceivers().forEach((receiver) => { if (receiver.track) { receiver.track.enabled = enable; } }); } /** * Helper function to enable/disable media tracks. * @param enable - If true enable tracks, otherwise disable tracks. */ enableSenderTracks(enable) { const peerConnection = this.peerConnection; if (!peerConnection) { throw new Error("Peer connection closed."); } peerConnection.getSenders().forEach((sender) => { if (sender.track) { sender.track.enabled = enable; } }); } /** * Creates an offer or answer. * @param options - Options bucket. * @param modifiers - Modifiers. */ getDescription(options, modifiers) { var _a, _b; this.logger.debug("SessionDescriptionHandler.getDescription"); if (this._peerConnection === undefined) { return Promise.reject(new Error("Peer connection closed.")); } // Callback on data channel creation this.onDataChannel = options === null || options === void 0 ? void 0 : options.onDataChannel; // ICE will restart upon applying an offer created with the iceRestart option const iceRestart = (_a = options === null || options === void 0 ? void 0 : options.offerOptions) === null || _a === void 0 ? void 0 : _a.iceRestart; // ICE gathering timeout may be set on a per call basis, otherwise the configured default is used const iceTimeout = (options === null || options === void 0 ? void 0 : options.iceGatheringTimeout) === undefined ? (_b = this.sessionDescriptionHandlerConfiguration) === null || _b === void 0 ? void 0 : _b.iceGatheringTimeout : options === null || options === void 0 ? void 0 : options.iceGatheringTimeout; return this.getLocalMediaStream(options) .then(() => this.updateDirection(options)) .then(() => this.createDataChannel(options)) .then(() => this.createLocalOfferOrAnswer(options)) .then((sessionDescription) => this.applyModifiers(sessionDescription, modifiers)) .then((sessionDescription) => this.setLocalSessionDescription(sessionDescription)) .then(() => this.waitForIceGatheringComplete(iceRestart, iceTimeout)) .then(() => this.getLocalSessionDescription()) .then((sessionDescription) => { return { body: sessionDescription.sdp, contentType: "application/sdp" }; }) .catch((error) => { this.logger.error("SessionDescriptionHandler.getDescription failed - " + error); throw error; }); } /** * Returns true if the SessionDescriptionHandler can handle the Content-Type described by a SIP message. * @param contentType - The content type that is in the SIP Message. */ hasDescription(contentType) { this.logger.debug("SessionDescriptionHandler.hasDescription"); return contentType === "application/sdp"; } /** * Called when ICE gathering completes and resolves any waiting promise. * @remarks * May be called prior to ICE gathering actually completing to allow the * session descirption handler proceed with whatever candidates have been * gathered up to this point in time. Use this to stop waiting on ICE to * complete if you are implementing your own ICE gathering completion strategy. */ iceGatheringComplete() { this.logger.debug("SessionDescriptionHandler.iceGatheringComplete"); // clear timer if need be if (this.iceGatheringCompleteTimeoutId !== undefined) { this.logger.debug("SessionDescriptionHandler.iceGatheringComplete - clearing timeout"); clearTimeout(this.iceGatheringCompleteTimeoutId); this.iceGatheringCompleteTimeoutId = undefined; } // resolve and cleanup promise if need be if (this.iceGatheringCompletePromise !== undefined) { this.logger.debug("SessionDescriptionHandler.iceGatheringComplete - resolving promise"); this.iceGatheringCompleteResolve && this.iceGatheringCompleteResolve(); this.iceGatheringCompletePromise = undefined; this.iceGatheringCompleteResolve = undefined; this.iceGatheringCompleteReject = undefined; } } /** * Send DTMF via RTP (RFC 4733). * Returns true if DTMF send is successful, false otherwise. * @param tones - A string containing DTMF digits. * @param options - Options object to be used by sendDtmf. */ sendDtmf(tones, options) { this.logger.debug("SessionDescriptionHandler.sendDtmf"); if (this._peerConnection === undefined) { this.logger.error("SessionDescriptionHandler.sendDtmf failed - peer connection closed"); return false; } const senders = this._peerConnection.getSenders(); if (senders.length === 0) { this.logger.error("SessionDescriptionHandler.sendDtmf failed - no senders"); return false; } const dtmfSender = senders[0].dtmf; if (!dtmfSender) { this.logger.error("SessionDescriptionHandler.sendDtmf failed - no DTMF sender"); return false; } const duration = options === null || options === void 0 ? void 0 : options.duration; const interToneGap = options === null || options === void 0 ? void 0 : options.interToneGap; try { dtmfSender.insertDTMF(tones, duration, interToneGap); } catch (e) { this.logger.error(e.toString()); return false; } this.logger.log("SessionDescriptionHandler.sendDtmf sent via RTP: " + tones.toString()); return true; } /** * Sets an offer or answer. * @param sdp - The session description. * @param options - Options bucket. * @param modifiers - Modifiers. */ setDescription(sdp, options, modifiers) { this.logger.debug("SessionDescriptionHandler.setDescription"); if (this._peerConnection === undefined) { return Promise.reject(new Error("Peer connection closed.")); } // Callback on data channel creation this.onDataChannel = options === null || options === void 0 ? void 0 : options.onDataChannel; // SDP type const type = this._peerConnection.signalingState === "have-local-offer" ? "answer" : "offer"; return this.getLocalMediaStream(options) .then(() => this.applyModifiers({ sdp, type }, modifiers)) .then((sessionDescription) => this.setRemoteSessionDescription(sessionDescription)) .catch((error) => { this.logger.error("SessionDescriptionHandler.setDescription failed - " + error); throw error; }); } /** * Applies modifiers to SDP prior to setting the local or remote description. * @param sdp - SDP to modify. * @param modifiers - Modifiers to apply. */ applyModifiers(sdp, modifiers) { this.logger.debug("SessionDescriptionHandler.applyModifiers"); if (!modifiers || modifiers.length === 0) { return Promise.resolve(sdp); } return modifiers .reduce((cur, next) => cur.then(next), Promise.resolve(sdp)) .then((modified) => { this.logger.debug("SessionDescriptionHandler.applyModifiers - modified sdp"); if (!modified.sdp || !modified.type) { throw new Error("Invalid SDP."); } return { sdp: modified.sdp, type: modified.type }; }); } /** * Create a data channel. * @remarks * Only creates a data channel if SessionDescriptionHandlerOptions.dataChannel is true. * Only creates a data channel if creating a local offer. * Only if one does not already exist. * @param options - Session description handler options. */ createDataChannel(options) { if (this._peerConnection === undefined) { return Promise.reject(new Error("Peer connection closed.")); } // only create a data channel if requested if ((options === null || options === void 0 ? void 0 : options.dataChannel) !== true) { return Promise.resolve(); } // do not create a data channel if we already have one if (this._dataChannel) { return Promise.resolve(); } switch (this._peerConnection.signalingState) { case "stable": // if we are stable, assume we are creating a local offer so create a data channel this.logger.debug("SessionDescriptionHandler.createDataChannel - creating data channel"); try { this._dataChannel = this._peerConnection.createDataChannel((options === null || options === void 0 ? void 0 : options.dataChannelLabel) || "", options === null || options === void 0 ? void 0 : options.dataChannelOptions); if (this.onDataChannel) { this.onDataChannel(this._dataChannel); } return Promise.resolve(); } catch (error) { return Promise.reject(error); } case "have-remote-offer": return Promise.resolve(); case "have-local-offer": case "have-local-pranswer": case "have-remote-pranswer": case "closed": default: return Promise.reject(new Error("Invalid signaling state " + this._peerConnection.signalingState)); } } /** * Depending on current signaling state, create a local offer or answer. * @param options - Session description handler options. */ createLocalOfferOrAnswer(options) { if (this._peerConnection === undefined) { return Promise.reject(new Error("Peer connection closed.")); } switch (this._peerConnection.signalingState) { case "stable": // if we are stable, assume we are creating a local offer this.logger.debug("SessionDescriptionHandler.createLocalOfferOrAnswer - creating SDP offer"); return this._peerConnection.createOffer(options === null || options === void 0 ? void 0 : options.offerOptions); case "have-remote-offer": // if we have a remote offer, assume we are creating a local answer this.logger.debug("SessionDescriptionHandler.createLocalOfferOrAnswer - creating SDP answer"); return this._peerConnection.createAnswer(options === null || options === void 0 ? void 0 : options.answerOptions); case "have-local-offer": case "have-local-pranswer": case "have-remote-pranswer": case "closed": default: return Promise.reject(new Error("Invalid signaling state " + this._peerConnection.signalingState)); } } /** * Get a media stream from the media stream factory and set the local media stream. * @param options - Session description handler options. */ getLocalMediaStream(options) { this.logger.debug("SessionDescriptionHandler.getLocalMediaStream"); if (this._peerConnection === undefined) { return Promise.reject(new Error("Peer connection closed.")); } let constraints = Object.assign({}, options === null || options === void 0 ? void 0 : options.constraints); // if we already have a local media stream... if (this.localMediaStreamConstraints) { // ignore constraint "downgrades" constraints.audio = constraints.audio || this.localMediaStreamConstraints.audio; constraints.video = constraints.video || this.localMediaStreamConstraints.video; // if constraints have not changed, do not get a new media stream if (JSON.stringify(this.localMediaStreamConstraints.audio) === JSON.stringify(constraints.audio) && JSON.stringify(this.localMediaStreamConstraints.video) === JSON.stringify(constraints.video)) { return Promise.resolve(); } } else { // if no constraints have been specified, default to audio for initial media stream if (constraints.audio === undefined && constraints.video === undefined) { constraints = { audio: true }; } } this.localMediaStreamConstraints = constraints; return this.mediaStreamFactory(constraints, this, options).then((mediaStream) => this.setLocalMediaStream(mediaStream)); } /** * Sets the peer connection's sender tracks and local media stream tracks. * * @remarks * Only the first audio and video tracks of the provided MediaStream are utilized. * Adds tracks if audio and/or video tracks are not already present, otherwise replaces tracks. * * @param stream - Media stream containing tracks to be utilized. */ setLocalMediaStream(stream) { this.logger.debug("SessionDescriptionHandler.setLocalMediaStream"); if (!this._peerConnection) { throw new Error("Peer connection undefined."); } const pc = this._peerConnection; const localStream = this._localMediaStream; const trackUpdates = []; const updateTrack = (newTrack) => { const kind = newTrack.kind; if (kind !== "audio" && kind !== "video") { throw new Error(`Unknown new track kind ${kind}.`); } const sender = pc.getSenders().find((sender) => sender.track && sender.track.kind === kind); if (sender) { trackUpdates.push(new Promise((resolve) => { this.logger.debug(`SessionDescriptionHandler.setLocalMediaStream - replacing sender ${kind} track`); resolve(); }).then(() => sender .replaceTrack(newTrack) .then(() => { const oldTrack = localStream.getTracks().find((localTrack) => localTrack.kind === kind); if (oldTrack) { oldTrack.stop(); localStream.removeTrack(oldTrack); SessionDescriptionHandler.dispatchRemoveTrackEvent(localStream, oldTrack); } localStream.addTrack(newTrack); SessionDescriptionHandler.dispatchAddTrackEvent(localStream, newTrack); }) .catch((error) => { this.logger.error(`SessionDescriptionHandler.setLocalMediaStream - failed to replace sender ${kind} track`); throw error; }))); } else { trackUpdates.push(new Promise((resolve) => { this.logger.debug(`SessionDescriptionHandler.setLocalMediaStream - adding sender ${kind} track`); resolve(); }).then(() => { // Review: could make streamless tracks a configurable option? // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addTrack#Usage_notes try { pc.addTrack(newTrack, localStream); } catch (error) { this.logger.error(`SessionDescriptionHandler.setLocalMediaStream - failed to add sender ${kind} track`); throw error; } localStream.addTrack(newTrack); SessionDescriptionHandler.dispatchAddTrackEvent(localStream, newTrack); })); } }; // update peer connection audio tracks const audioTracks = stream.getAudioTracks(); if (audioTracks.length) { updateTrack(audioTracks[0]); } // update peer connection video tracks const videoTracks = stream.getVideoTracks(); if (videoTracks.length) { updateTrack(videoTracks[0]); } return trackUpdates.reduce((p, x) => p.then(() => x), Promise.resolve()); } /** * Gets the peer connection's local session description. */ getLocalSessionDescription() { this.logger.debug("SessionDescriptionHandler.getLocalSessionDescription"); if (this._peerConnection === undefined) { return Promise.reject(new Error("Peer connection closed.")); } const sdp = this._peerConnection.localDescription; if (!sdp) { return Promise.reject(new Error("Failed to get local session description")); } return Promise.resolve(sdp); } /** * Sets the peer connection's local session description. * @param sessionDescription - sessionDescription The session description. */ setLocalSessionDescription(sessionDescription) { this.logger.debug("SessionDescriptionHandler.setLocalSessionDescription"); if (this._peerConnection === undefined) { return Promise.reject(new Error("Peer connection closed.")); } return this._peerConnection.setLocalDescription(sessionDescription); } /** * Sets the peer connection's remote session description. * @param sessionDescription - The session description. */ setRemoteSessionDescription(sessionDescription) { this.logger.debug("SessionDescriptionHandler.setRemoteSessionDescription"); if (this._peerConnection === undefined) { return Promise.reject(new Error("Peer connection closed.")); } const sdp = sessionDescription.sdp; let type; switch (this._peerConnection.signalingState) { case "stable": // if we are stable assume this is a remote offer type = "offer"; break; case "have-local-offer": // if we made an offer, assume this is a remote answer type = "answer"; break; case "have-local-pranswer": case "have-remote-offer": case "have-remote-pranswer": case "closed": default: return Promise.reject(new Error("Invalid signaling state " + this._peerConnection.signalingState)); } if (!sdp) { this.logger.error("SessionDescriptionHandler.setRemoteSessionDescription failed - cannot set null sdp"); return Promise.reject(new Error("SDP is undefined")); } return this._peerConnection.setRemoteDescription({ sdp, type }); } /** * Sets a remote media stream track. * * @remarks * Adds tracks if audio and/or video tracks are not already present, otherwise replaces tracks. * * @param track - Media stream track to be utilized. */ setRemoteTrack(track) { this.logger.debug("SessionDescriptionHandler.setRemoteTrack"); const remoteStream = this._remoteMediaStream; if (remoteStream.getTrackById(track.id)) { this.logger.debug(`SessionDescriptionHandler.setRemoteTrack - have remote ${track.kind} track`); } else if (track.kind === "audio") { this.logger.debug(`SessionDescriptionHandler.setRemoteTrack - adding remote ${track.kind} track`); remoteStream.getAudioTracks().forEach((track) => { track.stop(); remoteStream.removeTrack(track); SessionDescriptionHandler.dispatchRemoveTrackEvent(remoteStream, track); }); remoteStream.addTrack(track); SessionDescriptionHandler.dispatchAddTrackEvent(remoteStream, track); } else if (track.kind === "video") { this.logger.debug(`SessionDescriptionHandler.setRemoteTrack - adding remote ${track.kind} track`); remoteStream.getVideoTracks().forEach((track) => { track.stop(); remoteStream.removeTrack(track); SessionDescriptionHandler.dispatchRemoveTrackEvent(remoteStream, track); }); remoteStream.addTrack(track); SessionDescriptionHandler.dispatchAddTrackEvent(remoteStream, track); } } /** * Depending on the current signaling state and the session hold state, update transceiver direction. * @param options - Session description handler options. */ updateDirection(options) { if (this._peerConnection === undefined) { return Promise.reject(new Error("Peer connection closed.")); } // 4.2.3. setDirection // // The setDirection method sets the direction of a transceiver, which // affects the direction property of the associated "m=" section on // future calls to createOffer and createAnswer. The permitted values // for direction are "recvonly", "sendrecv", "sendonly", and "inactive", // mirroring the identically named direction attributes defined in // [RFC4566], Section 6. // // When creating offers, the transceiver direction is directly reflected // in the output, even for re-offers. When creating answers, the // transceiver direction is intersected with the offered direction, as // explained in Section 5.3 below. // // Note that while setDirection sets the direction property of the // transceiver immediately (Section 4.2.4), this property does not // immediately affect whether the transceiver's RtpSender will send or // its RtpReceiver will receive. The direction in effect is represented // by the currentDirection property, which is only updated when an // answer is applied. // // 4.2.4. direction // // The direction property indicates the last value passed into // setDirection. If setDirection has never been called, it is set to // the direction the transceiver was initialized with. // // 4.2.5. currentDirection // // The currentDirection property indicates the last negotiated direction // for the transceiver's associated "m=" section. More specifically, it // indicates the direction attribute [RFC3264] of the associated "m=" // section in the last applied answer (including provisional answers), // with "send" and "recv" directions reversed if it was a remote answer. // For example, if the direction attribute for the associated "m=" // section in a remote answer is "recvonly", currentDirection is set to // "sendonly". // // If an answer that references this transceiver has not yet been // applied or if the transceiver is stopped, currentDirection is set to // "null". // https://tools.ietf.org/html/rfc8829#section-4.2.3 // // * A direction attribute, determined by applying the rules regarding // the offered direction specified in [RFC3264], Section 6.1, and // then intersecting with the direction of the associated // RtpTransceiver. For example, in the case where an "m=" section is // offered as "sendonly" and the local transceiver is set to // "sendrecv", the result in the answer is a "recvonly" direction. // https://tools.ietf.org/html/rfc8829#section-5.3.1 // // If a stream is offered as sendonly, the corresponding stream MUST be // marked as recvonly or inactive in the answer. If a media stream is // listed as recvonly in the offer, the answer MUST be marked as // sendonly or inactive in the answer. If an offered media stream is // listed as sendrecv (or if there is no direction attribute at the // media or session level, in which case the stream is sendrecv by // default), the corresponding stream in the answer MAY be marked as // sendonly, recvonly, sendrecv, or inactive. If an offered media // stream is listed as inactive, it MUST be marked as inactive in the // answer. // https://tools.ietf.org/html/rfc3264#section-6.1 switch (this._peerConnection.signalingState) { case "stable": // if we are stable, assume we are creating a local offer this.logger.debug("SessionDescriptionHandler.updateDirection - setting offer direction"); { // determine the direction to offer given the current direction and hold state const directionToOffer = (currentDirection) => { switch (currentDirection) { case "inactive": return (options === null || options === void 0 ? void 0 : options.hold) ? "inactive" : "recvonly"; case "recvonly": return (options === null || options === void 0 ? void 0 : options.hold) ? "inactive" : "recvonly"; case "sendonly": return (options === null || options === void 0 ? void 0 : options.hold) ? "sendonly" : "sendrecv"; case "sendrecv": return (options === null || options === void 0 ? void 0 : options.hold) ? "sendonly" : "sendrecv"; case "stopped": return "stopped"; default: throw new Error("Should never happen"); } }; // set the transceiver direction to the offer direction this._peerConnection.getTransceivers().forEach((transceiver) => { if (transceiver.direction /* guarding, but should always be true */) { const offerDirection = directionToOffer(transceiver.direction); if (transceiver.direction !== offerDirection) { transceiver.direction = offerDirection; } } }); } break; case "have-remote-offer": // if we have a remote offer, assume we are creating a local answer this.logger.debug("SessionDescriptionHandler.updateDirection - setting answer direction"); // FIXME: This is not the correct way to determine the answer direction as it is only // considering first match in the offered SDP and using that to determine the answer direction. // While that may be fine for our current use cases, it is not a generally correct approach. { // determine the offered direction const offeredDirection = (() => { const description = this._peerConnection.remoteDescription; if (!description) { throw new Error("Failed to read remote offer"); } const searchResult = /a=sendrecv\r\n|a=sendonly\r\n|a=recvonly\r\n|a=inactive\r\n/.exec(description.sdp); if (searchResult) { switch (searchResult[0]) { case "a=inactive\r\n": return "inactive"; case "a=recvonly\r\n": return "recvonly"; case "a=sendonly\r\n": return "sendonly"; case "a=sendrecv\r\n": return "sendrecv"; default: throw new Error("Should never happen"); } } return "sendrecv"; })(); // determine the answer direction based on the offered direction and our hold state const answerDirection = (() => { switch (offeredDirection) { case "inactive": return "inactive"; case "recvonly": return "sendonly"; case "sendonly": return (options === null || options === void 0 ? void 0 : options.hold) ? "inactive" : "recvonly"; case "sendrecv": return (options === null || options === void 0 ? void 0 : options.hold) ? "sendonly" : "sendrecv"; default: throw new Error("Should never happen"); } })(); // set the transceiver direction to the answer direction this._peerConnection.getTransceivers().forEach((transceiver) => { if (transceiver.direction /* guarding, but should always be true */) { if (transceiver.direction !== "stopped" && transceiver.direction !== answerDirection) { transceiver.direction = answerDirection; } } }); } break; case "have-local-offer": case "have-local-pranswer": case "have-remote-pranswer": case "closed": default: return Promise.reject(new Error("Invalid signaling state " + this._peerConnection.signalingState)); } return Promise.resolve(); } /** * Wait for ICE gathering to complete. * @param restart - If true, waits if current state is "complete" (waits for transition to "complete"). * @param timeout - Milliseconds after which waiting times out. No timeout if 0. */ waitForIceGatheringComplete(restart = false, timeout = 0) { this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete"); if (this._peerConnection === undefined) { return Promise.reject("Peer connection closed."); } // guard already complete if (!restart && this._peerConnection.iceGatheringState === "complete") { this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete - already complete"); return Promise.resolve(); } // only one may be waiting, reject any prior if (this.iceGatheringCompletePromise !== undefined) { this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete - rejecting prior waiting promise"); this.iceGatheringCompleteReject && this.iceGatheringCompleteReject(new Error("Promise superseded.")); this.iceGatheringCompletePromise = undefined; this.iceGatheringCompleteResolve = undefined; this.iceGatheringCompleteReject = undefined; } this.iceGatheringCompletePromise = new Promise((resolve, reject) => { this.iceGatheringCompleteResolve = resolve; this.iceGatheringCompleteReject = reject; if (timeout > 0) { this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete - timeout in " + timeout); this.iceGatheringCompleteTimeoutId = setTimeout(() => { this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete - timeout"); this.iceGatheringComplete(); }, timeout); } }); return this.iceGatheringCompletePromise; } /** * Initializes the peer connection event handlers */ initPeerConnectionEventHandlers() { this.logger.debug("SessionDescriptionHandler.initPeerConnectionEventHandlers"); if (!this._peerConnection) throw new Error("Peer connection undefined."); const peerConnection = this._peerConnection; peerConnection.onconnectionstatechange = (event) => { var _a; const newState = peerConnection.connectionState; this.logger.debug(`SessionDescriptionHandler.onconnectionstatechange ${newState}`); if ((_a = this._peerConnectionDelegate) === null || _a === void 0 ? void 0 : _a.onconnectionstatechange) { this._peerConnectionDelegate.onconnectionstatechange(event); } }; peerConnection.ondatachannel = (event) => { var _a; this.logger.debug(`SessionDescriptionHandler.ondatachannel`); this._dataChannel = event.channel; if (this.onDataChannel) { this.onDataChannel(this._dataChannel); } if ((_a = this._peerConnectionDelegate) === null || _a === void 0 ? void 0 : _a.ondatachannel) { this._peerConnectionDelegate.ondatachannel(event); } }; peerConnection.onicecandidate = (event) => { var _a; this.logger.debug(`SessionDescriptionHandler.onicecandidate`); if ((_a = this._peerConnectionDelegate) === null || _a === void 0 ? void 0 : _a.onicecandidate) { this._peerConnectionDelegate.onicecandidate(event); } }; peerConnection.onicecandidateerror = (event) => { var _a; this.logger.debug(`SessionDescriptionHandler.onicecandidateerror`); if ((_a = this._peerConnectionDelegate) === null || _a === void 0 ? void 0 : _a.onicecandidateerror) { this._peerConnectionDelegate.onicecandidateerror(event); } }; peerConnection.oniceconnectionstatechange = (event) => { var _a; const newState = peerConnection.iceConnectionState; this.logger.debug(`SessionDescriptionHandler.oniceconnectionstatechange ${newState}`); if ((_a = this._peerConnectionDelegate) === null || _a === void 0 ? void 0 : _a.oniceconnectionstatechange) { this._peerConnectionDelegate.oniceconnectionstatechange(event); } }; peerConnection.onicegatheringstatechange = (event) => { var _a; const newState = peerConnection.iceGatheringState; this.logger.debug(`SessionDescriptionHandler.onicegatheringstatechange ${newState}`); if (newState === "complete") { this.iceGatheringComplete(); // complete waiting for ICE gathering to complete } if ((_a = this._peerConnectionDelegate) === null || _a === void 0 ? void 0 : _a.onicegatheringstatechange) { this._peerConnectionDelegate.onicegatheringstatechange(event); } }; peerConnection.onnegotiationneeded = (event) => { var _a; this.logger.debug(`SessionDescriptionHandler.onnegotiationneeded`); if ((_a = this._peerConnectionDelegate) === null || _a === void 0 ? void 0 : _a.onnegotiationneeded) { this._peerConnectionDelegate.onnegotiationneeded(event); } }; peerConnection.onsignalingstatechange = (event) => { var _a; const newState = peerConnection.signalingState; this.logger.debug(`SessionDescriptionHandler.onsignalingstatechange ${newState}`); if ((_a = this._peerConnectionDelegate) === null || _a === void 0 ? void 0 : _a.onsignalingstatechange) { this._peerConnectionDelegate.onsignalingstatechange(event); } }; peerConnection.ontrack = (event) => { var _a; const kind = event.track.kind; const enabled = event.track.enabled ? "enabled" : "disabled"; this.logger.debug(`SessionDescriptionHandler.ontrack ${kind} ${enabled}`); this.setRemoteTrack(event.track); if ((_a = this._peerConnectionDelegate) === null || _a === void 0 ? void 0 : _a.ontrack) { this._peerConnectionDelegate.ontrack(event); } }; } } /** * Function which returns a SessionDescriptionHandlerFactory. * @remarks * See {@link defaultPeerConnectionConfiguration} for the default peer connection configuration. * The ICE gathering timeout defaults to 5000ms. * @param mediaStreamFactory - MediaStream factory. * @public */ function defaultSessionDescriptionHandlerFactory(mediaStreamFactory) { return (session, options) => { // provide a default media stream factory if need be if (mediaStreamFactory === undefined) { mediaStreamFactory = defaultMediaStreamFactory(); } // make sure we allow `0` to be passed in so timeout can be disabled const iceGatheringTimeout = (options === null || options === void 0 ? void 0 : options.iceGatheringTimeout) !== undefined ? options === null || options === void 0 ? void 0 : options.iceGatheringTimeout : 5000; // merge passed factory options into default session description configuration const sessionDescriptionHandlerConfiguration = { iceGatheringTimeout, peerConnectionConfiguration: Object.assign(Object.assign({}, defaultPeerConnectionConfiguration()), options === null || options === void 0 ? void 0 : options.peerConnectionConfiguration) }; const logger = session.userAgent.getLogger("sip.SessionDescriptionHandler"); return new SessionDescriptionHandler(logger, mediaStreamFactory, sessionDescriptionHandlerConfiguration); }; } /** * Function which returns a ManagedSessionFactory. * @public */ function defaultManagedSessionFactory() { return (sessionManager, session) => { return { session, held: false, muted: false }; }; } /** * @internal */ class Parameters { constructor(parameters) { this.parameters = {}; // for in is required here as the Grammar parser is adding to the prototype chain for (const param in parameters) { // eslint-disable-next-line no-prototype-builtins if (parameters.hasOwnProperty(param)) { this.setParam(param, parameters[param]); } } } setParam(key, value) { if (key) { this.parameters[key.toLowerCase()] = (typeof value === "undefined" || value === null) ? null : value.toString(); } } getParam(key) { if (key) { return this.parameters[key.toLowerCase()]; } } hasParam(key) { return !!(key && this.parameters[key.toLowerCase()] !== undefined); } deleteParam(key) { key = key.toLowerCase(); if (this.hasParam(key)) { const value = this.parameters[key]; delete this.parameters[key]; return value; } } clearParams() { this.parameters = {}; } } /** * Name Address SIP header. * @public */ class NameAddrHeader extends Parameters { /** * Constructor * @param uri - * @param displayName - * @param parameters - */ constructor(uri, displayName, parameters) { super(parameters); this.uri = uri; this._displayName = displayName; } get friendlyName() { return this.displayName || this.uri.aor; } get displayName() { return this._displayName; } set displayName(value) { this._displayName = value; } clone() { return new NameAddrHeader(this.uri.clone(), this._displayName, JSON.parse(JSON.stringify(this.parameters))); } toString() { let body = (this.displayName || this.displayName === "0") ? '"' + this.displayName + '" ' : ""; body += "<" + this.uri.toString() + ">"; for (const parameter in this.parameters) { // eslint-disable-next-line no-prototype-builtins if (this.parameters.hasOwnProperty(parameter)) { body += ";" + parameter; if (this.parameters[parameter] !== null) { body += "=" + this.parameters[parameter]; } } } return body; } } /* eslint-disable @typescript-eslint/no-explicit-any */ /** * URI. * @public */ class URI extends Parameters { /** * Constructor * @param scheme - * @param user - * @param host - * @param port - * @param parameters - * @param headers - */ constructor(scheme = "sip", user, host, port, parameters, headers) { super(parameters || {}); this.headers = {}; // Checks if (!host) {