UNPKG

@twilio/voice-sdk

Version:
1,037 lines (1,033 loc) 87 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var index = require('../errors/index.js'); var log = require('../log.js'); var util = require('../util.js'); var rtcpc = require('./rtcpc.js'); var sdp = require('./sdp.js'); var generated = require('../errors/generated.js'); // @ts-nocheck var ICE_GATHERING_TIMEOUT = 15000; var ICE_GATHERING_FAIL_NONE = 'none'; var ICE_GATHERING_FAIL_TIMEOUT = 'timeout'; var INITIAL_ICE_CONNECTION_STATE = 'new'; var VOLUME_INTERVAL_MS = 50; /** * @typedef {Object} PeerConnection * @param audioHelper * @param pstream * @param options * @return {PeerConnection} * @constructor */ function PeerConnection(audioHelper, pstream, options) { var _this = this; if (!audioHelper || !pstream) { throw new index.InvalidArgumentError('Audiohelper, and pstream are required arguments'); } if (!(this instanceof PeerConnection)) { return new PeerConnection(audioHelper, pstream, options); } this._log = new log.default('PeerConnection'); function noop() { this._log.warn('Unexpected noop call in peerconnection'); } this.onaudio = noop; this.onopen = noop; this.onerror = noop; this.onclose = noop; this.ondisconnected = noop; this.onfailed = noop; this.onconnected = noop; this.onreconnected = noop; this.onsignalingstatechange = noop; this.ondtlstransportstatechange = noop; this.onicegatheringfailure = noop; this.onicegatheringstatechange = noop; this.oniceconnectionstatechange = noop; this.onpcconnectionstatechange = noop; this.onicecandidate = noop; this.onselectedcandidatepairchange = noop; this.onvolume = noop; this.version = null; this.pstream = pstream; this.stream = null; this.sinkIds = new Set(['default']); this.outputs = new Map(); this.status = 'connecting'; this.callSid = null; this.isMuted = false; var AudioContext = typeof window !== 'undefined' && (window.AudioContext || window.webkitAudioContext); this._isSinkSupported = !!AudioContext && typeof HTMLAudioElement !== 'undefined' && HTMLAudioElement.prototype.setSinkId; // NOTE(mmalavalli): Since each Connection creates its own AudioContext, // after 6 instances an exception is thrown. Refer https://www.w3.org/2011/audio/track/issues/3. // In order to get around it, we are re-using the Device's AudioContext. this._audioContext = AudioContext && audioHelper._audioContext; this._audioHelper = audioHelper; this._audioProcessorEventObserver = audioHelper._getAudioProcessorEventObserver(); this._hasIceCandidates = false; this._hasIceGatheringFailures = false; this._iceGatheringTimeoutId = null; this._masterAudio = null; this._masterAudioDeviceId = null; this._mediaStreamSource = null; this._dtmfSender = null; this._dtmfSenderUnsupported = false; this._callEvents = []; this._nextTimeToPublish = Date.now(); this._onAnswerOrRinging = noop; this._onHangup = noop; this._remoteStream = null; this._shouldManageStream = true; this._iceState = INITIAL_ICE_CONNECTION_STATE; this.options = options = options || {}; this.navigator = options.navigator || (typeof navigator !== 'undefined' ? navigator : null); this.util = options.util || util; this.codecPreferences = options.codecPreferences; this._onAudioProcessorAdded = function (isRemote) { _this._handleAudioProcessorEvent(isRemote, true); }; this._onAudioProcessorRemoved = function (isRemote) { _this._handleAudioProcessorEvent(isRemote, false); }; this._audioProcessorEventObserver.on('add', this._onAudioProcessorAdded); this._audioProcessorEventObserver.on('remove', this._onAudioProcessorRemoved); return this; } PeerConnection.prototype.uri = function () { return this._uri; }; /** * Open the underlying RTCPeerConnection with a MediaStream obtained by * passed constraints. The resulting MediaStream is created internally * and will therefore be managed and destroyed internally. * @param {MediaStreamConstraints} constraints */ PeerConnection.prototype.openDefaultDeviceWithConstraints = function (constraints) { return this._audioHelper._openDefaultDeviceWithConstraints(constraints) .then(this._setInputTracksFromStream.bind(this, false)); }; /** * Replace the existing input audio tracks with the audio tracks from the * passed input audio stream. We re-use the existing stream because * the AnalyzerNode is bound to the stream. * @param {MediaStream} stream */ PeerConnection.prototype.setInputTracksFromStream = function (stream) { var self = this; return this._setInputTracksFromStream(true, stream).then(function () { self._shouldManageStream = false; }); }; PeerConnection.prototype._createAnalyser = function (audioContext, options) { options = Object.assign({ fftSize: 32, smoothingTimeConstant: 0.3, }, options); var analyser = audioContext.createAnalyser(); // tslint:disable-next-line for (var field in options) { analyser[field] = options[field]; } return analyser; }; PeerConnection.prototype._setVolumeHandler = function (handler) { this.onvolume = handler; }; PeerConnection.prototype._startPollingVolume = function () { if (!this._audioContext || !this.stream || !this._remoteStream) { return; } var audioContext = this._audioContext; var inputAnalyser = this._inputAnalyser = this._createAnalyser(audioContext); var inputBufferLength = inputAnalyser.frequencyBinCount; var inputDataArray = new Uint8Array(inputBufferLength); this._inputAnalyser2 = this._createAnalyser(audioContext, { maxDecibels: 0, minDecibels: -127, smoothingTimeConstant: 0, }); var outputAnalyser = this._outputAnalyser = this._createAnalyser(audioContext); var outputBufferLength = outputAnalyser.frequencyBinCount; var outputDataArray = new Uint8Array(outputBufferLength); this._outputAnalyser2 = this._createAnalyser(audioContext, { maxDecibels: 0, minDecibels: -127, smoothingTimeConstant: 0, }); this._updateInputStreamSource(this.stream); this._updateOutputStreamSource(this._remoteStream); var self = this; setTimeout(function emitVolume() { if (!self._audioContext) { return; } else if (self.status === 'closed') { self._inputAnalyser.disconnect(); self._outputAnalyser.disconnect(); self._inputAnalyser2.disconnect(); self._outputAnalyser2.disconnect(); return; } self._inputAnalyser.getByteFrequencyData(inputDataArray); var inputVolume = self.util.average(inputDataArray); self._inputAnalyser2.getByteFrequencyData(inputDataArray); var inputVolume2 = self.util.average(inputDataArray); self._outputAnalyser.getByteFrequencyData(outputDataArray); var outputVolume = self.util.average(outputDataArray); self._outputAnalyser2.getByteFrequencyData(outputDataArray); var outputVolume2 = self.util.average(outputDataArray); self.onvolume(inputVolume / 255, outputVolume / 255, inputVolume2, outputVolume2); setTimeout(emitVolume, VOLUME_INTERVAL_MS); }, VOLUME_INTERVAL_MS); }; PeerConnection.prototype._stopStream = function _stopStream() { // We shouldn't stop the tracks if they were not created inside // this PeerConnection. if (!this._shouldManageStream) { return; } this._audioHelper._stopDefaultInputDeviceStream(); }; /** * Update the stream source with the new input audio stream. * @param {MediaStream} stream * @private */ PeerConnection.prototype._updateInputStreamSource = function (stream) { if (this._inputStreamSource) { this._inputStreamSource.disconnect(); } try { this._inputStreamSource = this._audioContext.createMediaStreamSource(stream); this._inputStreamSource.connect(this._inputAnalyser); this._inputStreamSource.connect(this._inputAnalyser2); } catch (ex) { this._log.warn('Unable to update input MediaStreamSource', ex); this._inputStreamSource = null; } }; /** * Update the stream source with the new ouput audio stream. * @param {MediaStream} stream * @private */ PeerConnection.prototype._updateOutputStreamSource = function (stream) { if (this._outputStreamSource) { this._outputStreamSource.disconnect(); } try { this._outputStreamSource = this._audioContext.createMediaStreamSource(stream); this._outputStreamSource.connect(this._outputAnalyser); this._outputStreamSource.connect(this._outputAnalyser2); } catch (ex) { this._log.warn('Unable to update output MediaStreamSource', ex); this._outputStreamSource = null; } }; /** * Replace the tracks of the current stream with new tracks. We do this rather than replacing the * whole stream because AnalyzerNodes are bound to a stream. * @param {Boolean} shouldClone - Whether the stream should be cloned if it is the first * stream, or set directly. As a rule of thumb, streams that are passed in externally may have * their lifecycle managed externally, and should be cloned so that we do not tear it or its tracks * down when the call ends. Streams that we create internally (inside PeerConnection) should be set * directly so that when the call ends it is disposed of. * @param {MediaStream} newStream - The new stream to copy the tracks over from. * @private */ PeerConnection.prototype._setInputTracksFromStream = function (shouldClone, newStream) { var _this = this; if (!newStream) { return Promise.reject(new index.InvalidArgumentError('Can not set input stream to null while in a call')); } if (!newStream.getAudioTracks().length) { return Promise.reject(new index.InvalidArgumentError('Supplied input stream has no audio tracks')); } var localStream = this.stream; var getStreamPromise = function () { // Apply mute settings to new input track _this.mute(_this.isMuted); return Promise.resolve(_this.stream); }; if (!localStream) { // We can't use MediaStream.clone() here because it stopped copying over tracks // as of Chrome 61. https://bugs.chromium.org/p/chromium/issues/detail?id=770908 this.stream = shouldClone ? cloneStream(newStream, this.options.MediaStream) : newStream; } else { // If the call was started with gUM, and we are now replacing that track with an // external stream's tracks, we should stop the old managed track. if (this._shouldManageStream) { this._stopStream(); } if (!this._sender) { this._sender = this.version.pc.getSenders()[0]; } return this._sender.replaceTrack(newStream.getAudioTracks()[0]).then(function () { _this._updateInputStreamSource(newStream); _this.stream = shouldClone ? cloneStream(newStream, _this.options.MediaStream) : newStream; return getStreamPromise(); }); } return getStreamPromise(); }; PeerConnection.prototype._onInputDevicesChanged = function () { if (!this.stream) { return; } // If all of our active tracks are ended, then our active input was lost var activeInputWasLost = this.stream.getAudioTracks().every(function (track) { return track.readyState === 'ended'; }); // We only want to act if we manage the stream in PeerConnection (It was created // here, rather than passed in.) if (activeInputWasLost && this._shouldManageStream) { this.openDefaultDeviceWithConstraints({ audio: true }); } }; PeerConnection.prototype._onIceGatheringFailure = function (type) { this._hasIceGatheringFailures = true; this.onicegatheringfailure(type); }; PeerConnection.prototype._onMediaConnectionStateChange = function (newState) { var previousState = this._iceState; if (previousState === newState || (newState !== 'connected' && newState !== 'disconnected' && newState !== 'failed')) { return; } this._iceState = newState; var message; switch (newState) { case 'connected': if (previousState === 'disconnected' || previousState === 'failed') { message = 'ICE liveliness check succeeded. Connection with Twilio restored'; this._log.info(message); this.onreconnected(message); } else { message = 'Media connection established.'; this._log.info(message); this.onconnected(message); } this._stopIceGatheringTimeout(); this._hasIceGatheringFailures = false; break; case 'disconnected': message = 'ICE liveliness check failed. May be having trouble connecting to Twilio'; this._log.warn(message); this.ondisconnected(message); break; case 'failed': message = 'Connection with Twilio was interrupted.'; this._log.warn(message); this.onfailed(message); break; } }; PeerConnection.prototype._setSinkIds = function (sinkIds) { if (!this._isSinkSupported) { return Promise.reject(new index.NotSupportedError('Audio output selection is not supported by this browser')); } this.sinkIds = new Set(sinkIds.forEach ? sinkIds : [sinkIds]); return this.version ? this._updateAudioOutputs() : Promise.resolve(); }; /** * Start timeout for ICE Gathering */ PeerConnection.prototype._startIceGatheringTimeout = function startIceGatheringTimeout() { var _this = this; this._stopIceGatheringTimeout(); this._iceGatheringTimeoutId = setTimeout(function () { _this._onIceGatheringFailure(ICE_GATHERING_FAIL_TIMEOUT); }, ICE_GATHERING_TIMEOUT); }; /** * Stop timeout for ICE Gathering */ PeerConnection.prototype._stopIceGatheringTimeout = function stopIceGatheringTimeout() { clearInterval(this._iceGatheringTimeoutId); }; PeerConnection.prototype._updateAudioOutputs = function updateAudioOutputs() { var addedOutputIds = Array.from(this.sinkIds).filter(function (id) { return !this.outputs.has(id); }, this); var removedOutputIds = Array.from(this.outputs.keys()).filter(function (id) { return !this.sinkIds.has(id); }, this); var self = this; var createOutputPromises = addedOutputIds.map(this._createAudioOutput, this); return Promise.all(createOutputPromises).then(function () { return Promise.all(removedOutputIds.map(self._removeAudioOutput, self)); }); }; PeerConnection.prototype._createAudio = function createAudio(arr) { var audio = new Audio(arr); this.onaudio(audio); return audio; }; PeerConnection.prototype._createAudioOutput = function createAudioOutput(id) { var _this = this; var dest = null; if (this._mediaStreamSource) { dest = this._audioContext.createMediaStreamDestination(); this._mediaStreamSource.connect(dest); } var audio = this._createAudio(); setAudioSource(audio, dest && dest.stream ? dest.stream : this.pcStream, this._audioHelper) .catch(function () { return _this._log.error('Error attaching stream to element (_createAudioOutput).'); }); var self = this; return audio.setSinkId(id).then(function () { return audio.play(); }).then(function () { self.outputs.set(id, { audio: audio, dest: dest, }); }); }; PeerConnection.prototype._removeAudioOutputs = function removeAudioOutputs() { if (this._masterAudio && typeof this._masterAudioDeviceId !== 'undefined') { this._disableOutput(this, this._masterAudioDeviceId); this.outputs.delete(this._masterAudioDeviceId); this._masterAudioDeviceId = null; // Release the audio resources before deleting the audio if (!this._masterAudio.paused) { this._masterAudio.pause(); } if (typeof this._masterAudio.srcObject !== 'undefined') { this._masterAudio.srcObject = null; } else { this._masterAudio.src = ''; } this._masterAudio = null; } return Array.from(this.outputs.keys()).map(this._removeAudioOutput, this); }; PeerConnection.prototype._disableOutput = function disableOutput(pc, id) { var output = pc.outputs.get(id); if (!output) { return; } if (output.audio) { output.audio.pause(); output.audio.src = ''; } if (output.dest) { output.dest.disconnect(); } }; /** * Disable a non-master output, and update the master output to assume its state. This * is called when the device ID assigned to the master output has been removed from * active devices. We can not simply remove the master audio output, so we must * instead reassign it. * @private * @param {PeerConnection} pc * @param {string} masterId - The current device ID assigned to the master audio element. */ PeerConnection.prototype._reassignMasterOutput = function reassignMasterOutput(pc, masterId) { var masterOutput = pc.outputs.get(masterId); pc.outputs.delete(masterId); var self = this; var activeDeviceId = Array.from(pc.outputs.keys())[0]; // The audio device key could also be '' on Chrome if no media device permissions are allowed var idToReplace = typeof activeDeviceId === 'string' ? activeDeviceId : 'default'; return masterOutput.audio.setSinkId(idToReplace).then(function () { self._disableOutput(pc, idToReplace); pc.outputs.set(idToReplace, masterOutput); pc._masterAudioDeviceId = idToReplace; }).catch(function rollback() { pc.outputs.set(masterId, masterOutput); self._log.info('Could not reassign master output. Attempted to roll back.'); }); }; PeerConnection.prototype._removeAudioOutput = function removeAudioOutput(id) { if (this._masterAudioDeviceId === id) { return this._reassignMasterOutput(this, id); } this._disableOutput(this, id); this.outputs.delete(id); return Promise.resolve(); }; /** * Use an AudioContext to potentially split our audio output stream to multiple * audio devices. This is only available to browsers with AudioContext and * HTMLAudioElement.setSinkId() available. We save the source stream in * _masterAudio, and use it for one of the active audio devices. We keep * track of its ID because we must replace it if we lose its initial device. */ PeerConnection.prototype._onAddTrack = function onAddTrack(pc, stream) { var audio = pc._masterAudio = this._createAudio(); setAudioSource(audio, stream, this._audioHelper) .then(function () { return audio.play(); }) .catch(function () { return pc._log.error('Error attaching stream to element (_onAddTrack).'); }); // Assign the initial master audio element to a random active output device var activeDeviceId = Array.from(pc.outputs.keys())[0]; // The audio device key could also be '' on Chrome if no media device permissions are allowed var deviceId = typeof activeDeviceId === 'string' ? activeDeviceId : 'default'; pc._masterAudioDeviceId = deviceId; pc.outputs.set(deviceId, { audio: audio }); try { pc._mediaStreamSource = pc._audioContext.createMediaStreamSource(stream); } catch (ex) { this._log.warn('Unable to create a MediaStreamSource from onAddTrack', ex); this._mediaStreamSource = null; } pc.pcStream = stream; pc._updateAudioOutputs(); }; /** * Use a single audio element to play the audio output stream. This does not * support multiple output devices, and is a fallback for when AudioContext * and/or HTMLAudioElement.setSinkId() is not available to the client. */ PeerConnection.prototype._fallbackOnAddTrack = function fallbackOnAddTrack(pc, stream) { var audio = document && document.createElement('audio'); setAudioSource(audio, stream, this._audioHelper) .then(function () { return audio.play(); }) .catch(function () { return pc._log.error('Error attaching stream to element (_fallbackOnAddTrack).'); }); pc.outputs.set('default', { audio: audio }); }; PeerConnection.prototype._setEncodingParameters = function (enableDscp) { if (!enableDscp || !this._sender || typeof this._sender.getParameters !== 'function' || typeof this._sender.setParameters !== 'function') { return; } var params = this._sender.getParameters(); if (!params.priority && !(params.encodings && params.encodings.length)) { return; } // This is how MDN's RTPSenderParameters defines priority params.priority = 'high'; // And this is how it's currently implemented in Chrome M72+ if (params.encodings && params.encodings.length) { params.encodings.forEach(function (encoding) { encoding.priority = 'high'; encoding.networkPriority = 'high'; }); } this._sender.setParameters(params); }; PeerConnection.prototype._setupPeerConnection = function (rtcConfiguration) { var _this = this; var self = this; var version = new (this.options.rtcpcFactory || rtcpc.default)({ RTCPeerConnection: this.options.RTCPeerConnection }); version.create(rtcConfiguration); addStream(version.pc, this.stream); var supportedCodecs = RTCRtpReceiver.getCapabilities('audio').codecs; this._log.debug('sorting codecs', supportedCodecs, this.codecPreferences); var sortedCodecs = util.sortByMimeTypes(supportedCodecs, this.codecPreferences); var transceiver = version.pc.getTransceivers()[0]; this._log.debug('setting sorted codecs', sortedCodecs); transceiver.setCodecPreferences(sortedCodecs); var eventName = 'ontrack' in version.pc ? 'ontrack' : 'onaddstream'; version.pc[eventName] = function (event) { var stream = self._remoteStream = event.stream || event.streams[0]; if (typeof version.pc.getSenders === 'function') { _this._sender = version.pc.getSenders()[0]; } if (self._isSinkSupported) { self._onAddTrack(self, stream); } else { self._fallbackOnAddTrack(self, stream); } self._startPollingVolume(); }; return version; }; PeerConnection.prototype._maybeSetIceAggressiveNomination = function (sdp$1) { return this.options.forceAggressiveIceNomination ? sdp.setIceAggressiveNomination(sdp$1) : sdp$1; }; PeerConnection.prototype._setupChannel = function () { var _this = this; var pc = this.version.pc; // Chrome 25 supports onopen this.version.pc.onopen = function () { _this.status = 'open'; _this.onopen(); }; // Chrome 26 doesn't support onopen so must detect state change this.version.pc.onstatechange = function () { if (_this.version.pc && _this.version.pc.readyState === 'stable') { _this.status = 'open'; _this.onopen(); } }; // Chrome 27 changed onstatechange to onsignalingstatechange this.version.pc.onsignalingstatechange = function () { var state = pc.signalingState; _this._log.info("signalingState is \"".concat(state, "\"")); if (_this.version.pc && _this.version.pc.signalingState === 'stable') { _this.status = 'open'; _this.onopen(); } _this.onsignalingstatechange(pc.signalingState); }; // Chrome 72+ pc.onconnectionstatechange = function (event) { var state = pc.connectionState; if (!state && event && event.target) { // VDI environment var targetPc = event.target; state = targetPc.connectionState || targetPc.connectionState_; _this._log.info("pc.connectionState not detected. Using target PC. State=".concat(state)); } if (!state) { _this._log.warn("onconnectionstatechange detected but state is \"".concat(state, "\"")); } else { _this._log.info("pc.connectionState is \"".concat(state, "\"")); } _this.onpcconnectionstatechange(state); _this._onMediaConnectionStateChange(state); }; pc.onicecandidate = function (event) { var candidate = event.candidate; if (candidate) { _this._hasIceCandidates = true; _this.onicecandidate(candidate); _this._setupRTCIceTransportListener(); } _this._log.info("ICE Candidate: ".concat(JSON.stringify(candidate))); }; pc.onicegatheringstatechange = function () { var state = pc.iceGatheringState; if (state === 'gathering') { _this._startIceGatheringTimeout(); } else if (state === 'complete') { _this._stopIceGatheringTimeout(); // Fail if no candidates found if (!_this._hasIceCandidates) { _this._onIceGatheringFailure(ICE_GATHERING_FAIL_NONE); } // There was a failure mid-gathering phase. We want to start our timer and issue // an ice restart if we don't get connected after our timeout if (_this._hasIceCandidates && _this._hasIceGatheringFailures) { _this._startIceGatheringTimeout(); } } _this._log.info("pc.iceGatheringState is \"".concat(pc.iceGatheringState, "\"")); _this.onicegatheringstatechange(state); }; pc.oniceconnectionstatechange = function () { _this._log.info("pc.iceConnectionState is \"".concat(pc.iceConnectionState, "\"")); _this.oniceconnectionstatechange(pc.iceConnectionState); _this._onMediaConnectionStateChange(pc.iceConnectionState); }; }; PeerConnection.prototype._initializeMediaStream = function (rtcConfiguration) { // if mediastream already open then do nothing if (this.status === 'open') { return false; } if (this.pstream.status === 'disconnected') { this.onerror({ info: { code: 31000, message: 'Cannot establish connection. Client is disconnected', twilioError: new generated.SignalingErrors.ConnectionDisconnected(), } }); this.close(); return false; } this.version = this._setupPeerConnection(rtcConfiguration); this._setupChannel(); return true; }; /** * Remove reconnection-related listeners * @private */ PeerConnection.prototype._removeReconnectionListeners = function () { if (this.pstream) { this.pstream.removeListener('answer', this._onAnswerOrRinging); this.pstream.removeListener('hangup', this._onHangup); } }; /** * Setup a listener for RTCDtlsTransport to capture state changes events * @private */ PeerConnection.prototype._setupRTCDtlsTransportListener = function () { var _this = this; var dtlsTransport = this.getRTCDtlsTransport(); if (!dtlsTransport || dtlsTransport.onstatechange) { return; } var handler = function () { _this._log.info("dtlsTransportState is \"".concat(dtlsTransport.state, "\"")); _this.ondtlstransportstatechange(dtlsTransport.state); }; // Publish initial state handler(); dtlsTransport.onstatechange = handler; }; /** * Setup a listener for RTCIceTransport to capture selected candidate pair changes * @private */ PeerConnection.prototype._setupRTCIceTransportListener = function () { var _this = this; var iceTransport = this._getRTCIceTransport(); if (!iceTransport || iceTransport.onselectedcandidatepairchange) { return; } iceTransport.onselectedcandidatepairchange = function () { return _this.onselectedcandidatepairchange(iceTransport.getSelectedCandidatePair()); }; }; /** * Restarts ICE for the current connection * ICE Restart failures are ignored. Retries are managed in Connection * @private */ PeerConnection.prototype.iceRestart = function () { var _this = this; this._log.info('Attempting to restart ICE...'); this._hasIceCandidates = false; this.version.createOffer(this.options.maxAverageBitrate, { iceRestart: true }).then(function () { _this._removeReconnectionListeners(); _this._onAnswerOrRinging = function (payload) { _this._removeReconnectionListeners(); if (!payload.sdp || _this.version.pc.signalingState !== 'have-local-offer') { var message = 'Invalid state or param during ICE Restart:' + "hasSdp:".concat(!!payload.sdp, ", signalingState:").concat(_this.version.pc.signalingState); _this._log.warn(message); return; } var sdp = _this._maybeSetIceAggressiveNomination(payload.sdp); _this._answerSdp = sdp; if (_this.status !== 'closed') { _this.version.processAnswer(_this.codecPreferences, sdp, null, function (err) { var message = err && err.message ? err.message : err; _this._log.error("Failed to process answer during ICE Restart. Error: ".concat(message)); }); } }; _this._onHangup = function () { _this._log.info('Received hangup during ICE Restart'); _this._removeReconnectionListeners(); }; _this.pstream.on('answer', _this._onAnswerOrRinging); _this.pstream.on('hangup', _this._onHangup); _this.pstream.reinvite(_this.version.getSDP(), _this.callSid); }).catch(function (err) { var message = err && err.message ? err.message : err; _this._log.error("Failed to createOffer during ICE Restart. Error: ".concat(message)); // CreateOffer failures doesn't transition ice state to failed // We need trigger it so it can be picked up by retries _this.onfailed(message); }); }; PeerConnection.prototype.makeOutgoingCall = function (params, signalingReconnectToken, callsid, rtcConfiguration, onMediaStarted) { var _this = this; if (!this._initializeMediaStream(rtcConfiguration)) { return; } var self = this; this.callSid = callsid; function onAnswerSuccess() { if (self.options) { self._setEncodingParameters(self.options.dscp); } onMediaStarted(self.version.pc); } function onAnswerError(err) { var errMsg = err.message || err; self.onerror({ info: { code: 31000, message: "Error processing answer: ".concat(errMsg), twilioError: new generated.MediaErrors.ClientRemoteDescFailed(), } }); } this._onAnswerOrRinging = function (payload) { if (!payload.sdp) { return; } var sdp = _this._maybeSetIceAggressiveNomination(payload.sdp); self._answerSdp = sdp; if (self.status !== 'closed') { self.version.processAnswer(_this.codecPreferences, sdp, onAnswerSuccess, onAnswerError); } self.pstream.removeListener('answer', self._onAnswerOrRinging); self.pstream.removeListener('ringing', self._onAnswerOrRinging); }; this.pstream.on('answer', this._onAnswerOrRinging); this.pstream.on('ringing', this._onAnswerOrRinging); function onOfferSuccess() { if (self.status !== 'closed') { if (signalingReconnectToken) { self.pstream.reconnect(self.version.getSDP(), self.callSid, signalingReconnectToken); } else { self.pstream.invite(self.version.getSDP(), self.callSid, params); } self._setupRTCDtlsTransportListener(); } } function onOfferError(err) { var errMsg = err.message || err; self.onerror({ info: { code: 31000, message: "Error creating the offer: ".concat(errMsg), twilioError: new generated.MediaErrors.ClientLocalDescFailed(), } }); } this.version.createOffer(this.options.maxAverageBitrate, { audio: true }, onOfferSuccess, onOfferError); }; PeerConnection.prototype.answerIncomingCall = function (callSid, sdp, rtcConfiguration, onMediaStarted) { if (!this._initializeMediaStream(rtcConfiguration)) { return; } sdp = this._maybeSetIceAggressiveNomination(sdp); this._answerSdp = sdp.replace(/^a=setup:actpass$/gm, 'a=setup:passive'); this.callSid = callSid; var self = this; function onAnswerSuccess() { if (self.status !== 'closed') { self.pstream.answer(self.version.getSDP(), callSid); if (self.options) { self._setEncodingParameters(self.options.dscp); } onMediaStarted(self.version.pc); self._setupRTCDtlsTransportListener(); } } function onAnswerError(err) { var errMsg = err.message || err; self.onerror({ info: { code: 31000, message: "Error creating the answer: ".concat(errMsg), twilioError: new generated.MediaErrors.ClientRemoteDescFailed(), } }); } this.version.processSDP(this.options.maxAverageBitrate, this.codecPreferences, sdp, { audio: true }, onAnswerSuccess, onAnswerError); }; PeerConnection.prototype.close = function () { if (this.version && this.version.pc) { if (this.version.pc.signalingState !== 'closed') { this.version.pc.close(); } this.version.pc = null; } if (this.stream) { this.mute(false); this._stopStream(); } this.stream = null; this._removeReconnectionListeners(); this._stopIceGatheringTimeout(); this._audioHelper._destroyRemoteProcessedStream(); this._audioProcessorEventObserver.removeListener('add', this._onAudioProcessorAdded); this._audioProcessorEventObserver.removeListener('remove', this._onAudioProcessorRemoved); Promise.all(this._removeAudioOutputs()).catch(function () { // We don't need to alert about failures here. }); if (this._mediaStreamSource) { this._mediaStreamSource.disconnect(); } if (this._inputAnalyser) { this._inputAnalyser.disconnect(); } if (this._outputAnalyser) { this._outputAnalyser.disconnect(); } if (this._inputAnalyser2) { this._inputAnalyser2.disconnect(); } if (this._outputAnalyser2) { this._outputAnalyser2.disconnect(); } this.status = 'closed'; this.onclose(); }; PeerConnection.prototype.reject = function (callSid) { this.callSid = callSid; }; PeerConnection.prototype.ignore = function (callSid) { this.callSid = callSid; }; /** * Mute or unmute input audio. If the stream is not yet present, the setting * is saved and applied to future streams/tracks. * @params {boolean} shouldMute - Whether the input audio should * be muted or unmuted. */ PeerConnection.prototype.mute = function (shouldMute) { this.isMuted = shouldMute; if (!this.stream) { return; } if (this._sender && this._sender.track) { this._sender.track.enabled = !shouldMute; } else { var audioTracks = typeof this.stream.getAudioTracks === 'function' ? this.stream.getAudioTracks() : this.stream.audioTracks; audioTracks.forEach(function (track) { track.enabled = !shouldMute; }); } }; /** * Get or create an RTCDTMFSender for the first local audio MediaStreamTrack * we can get from the RTCPeerConnection. Return null if unsupported. * @instance * @returns ?RTCDTMFSender */ PeerConnection.prototype.getOrCreateDTMFSender = function getOrCreateDTMFSender() { if (this._dtmfSender || this._dtmfSenderUnsupported) { return this._dtmfSender || null; } var self = this; var pc = this.version.pc; if (!pc) { this._log.warn('No RTCPeerConnection available to call createDTMFSender on'); return null; } if (typeof pc.getSenders === 'function' && (typeof RTCDTMFSender === 'function' || typeof RTCDtmfSender === 'function')) { var chosenSender = pc.getSenders().find(function (sender) { return sender.dtmf; }); if (chosenSender) { this._log.info('Using RTCRtpSender#dtmf'); this._dtmfSender = chosenSender.dtmf; return this._dtmfSender; } } if (typeof pc.createDTMFSender === 'function' && typeof pc.getLocalStreams === 'function') { var track = pc.getLocalStreams().map(function (stream) { var tracks = self._getAudioTracks(stream); return tracks && tracks[0]; })[0]; if (!track) { this._log.warn('No local audio MediaStreamTrack available on the RTCPeerConnection to pass to createDTMFSender'); return null; } this._log.info('Creating RTCDTMFSender'); this._dtmfSender = pc.createDTMFSender(track); return this._dtmfSender; } this._log.info('RTCPeerConnection does not support RTCDTMFSender'); this._dtmfSenderUnsupported = true; return null; }; /** * Get the RTCDtlTransport object from the PeerConnection * @returns RTCDtlTransport */ PeerConnection.prototype.getRTCDtlsTransport = function getRTCDtlsTransport() { var sender = this.version && this.version.pc && typeof this.version.pc.getSenders === 'function' && this.version.pc.getSenders()[0]; return sender && sender.transport || null; }; PeerConnection.prototype._canStopMediaStreamTrack = function () { return typeof MediaStreamTrack.prototype.stop === 'function'; }; PeerConnection.prototype._getAudioTracks = function (stream) { return typeof stream.getAudioTracks === 'function' ? stream.getAudioTracks() : stream.audioTracks; }; /** * Get the RTCIceTransport object from the PeerConnection * @returns RTCIceTransport */ PeerConnection.prototype._getRTCIceTransport = function _getRTCIceTransport() { var dtlsTransport = this.getRTCDtlsTransport(); return dtlsTransport && dtlsTransport.iceTransport || null; }; // Is PeerConnection.protocol used outside of our SDK? We should remove this if not. PeerConnection.protocol = ((function () { return rtcpc.default.test() ? new rtcpc.default() : null; }))(); PeerConnection.prototype._handleAudioProcessorEvent = function (isRemote, isAddProcessor) { var _this = this; if (!isRemote || !this._remoteStream) { return; } var audio = null; if (this._masterAudio) { this._log.info('Setting audio source for master audio.'); audio = this._masterAudio; } else { this._log.info('No master audio. Setting audio source for default audio output.'); audio = this.outputs.get('default').audio; } setAudioSource(audio, this._remoteStream, this._audioHelper) .then(function () { var successLog = isAddProcessor ? 'Successfully updated audio source with processed stream' : 'Successfully reverted audio source to original stream'; _this._log.info(successLog); // If the audio was paused, resume playback if (audio.paused) { _this._log.info('Resuming audio playback'); audio.play(); } }) .catch(function () { var errorLog = isAddProcessor ? 'Failed to update audio source' : 'Failed to revert audio source'; _this._log.error(errorLog); }); }; function addStream(pc, stream) { if (typeof pc.addTrack === 'function') { stream.getAudioTracks().forEach(function (track) { // The second parameters, stream, should not be necessary per the latest editor's // draft, but FF requires it. https://bugzilla.mozilla.org/show_bug.cgi?id=1231414 pc.addTrack(track, stream); }); } else { pc.addStream(stream); } } function cloneStream(oldStream, _MediaStream) { var newStream; if (_MediaStream) { newStream = new _MediaStream(); } else if (typeof MediaStream !== 'undefined') { newStream = new MediaStream(); } else { newStream = new webkitMediaStream(); } oldStream.getAudioTracks().forEach(newStream.addTrack, newStream); return newStream; } /** * Sets the source of an HTMLAudioElement to the specified MediaStream and * applies a remote audio processor if available * @param {HTMLAudioElement} audio * @param {MediaStream} stream * @returns {Promise} Fulfilled if the audio source was set successfully */ function setAudioSource(audio, stream, audioHelper) { return audioHelper._maybeCreateRemoteProcessedStream(stream).then(function (maybeProcessedStream) { if (typeof audio.srcObject !== 'undefined') { audio.srcObject = maybeProcessedStream; } else if (typeof audio.mozSrcObject !== 'undefined') { audio.mozSrcObject = maybeProcessedStream; } else if (typeof audio.src !== 'undefined') { var _window = audio.options.window || window; audio.src = (_window.URL || _window.webkitURL).createObjectURL(maybeProcessedStream); } else { return Promise.reject(); } return Promise.resolve(); }); } PeerConnection.enabled = rtcpc.default.test(); exports.default = PeerConnection; //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGVlcmNvbm5lY3Rpb24uanMiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL2xpYi90d2lsaW8vcnRjL3BlZXJjb25uZWN0aW9uLnRzIl0sInNvdXJjZXNDb250ZW50IjpbbnVsbF0sIm5hbWVzIjpbIkludmFsaWRBcmd1bWVudEVycm9yIiwiTG9nIiwiTm90U3VwcG9ydGVkRXJyb3IiLCJSVENQQyIsInV0aWwuc29ydEJ5TWltZVR5cGVzIiwic2RwIiwic2V0SWNlQWdncmVzc2l2ZU5vbWluYXRpb24iLCJTaWduYWxpbmdFcnJvcnMiLCJNZWRpYUVycm9ycyJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7QUFBQTtBQVlBLElBQU0scUJBQXFCLEdBQUcsS0FBSztBQUNuQyxJQUFNLHVCQUF1QixHQUFHLE1BQU07QUFDdEMsSUFBTSwwQkFBMEIsR0FBRyxTQUFTO0FBQzVDLElBQU0sNEJBQTRCLEdBQUcsS0FBSztBQUMxQyxJQUFNLGtCQUFrQixHQUFHLEVBQUU7QUFFN0I7Ozs7Ozs7QUFPRztBQUNILFNBQVMsY0FBYyxDQUFDLFdBQVcsRUFBRSxPQUFPLEVBQUUsT0FBTyxFQUFBO0lBQXJELElBQUEsS0FBQSxHQUFBLElBQUE7QUFDRSxJQUFBLElBQUksQ0FBQyxXQUFXLElBQUksQ0FBQyxPQUFPLEVBQUU7QUFDNUIsUUFBQSxNQUFNLElBQUlBLDBCQUFvQixDQUFDLGlEQUFpRCxDQUFDO0lBQ25GO0FBRUEsSUFBQSxJQUFJLEVBQUUsSUFBSSxZQUFZLGNBQWMsQ0FBQyxFQUFFO1FBQ3JDLE9BQU8sSUFBSSxjQUFjLENBQUMsV0FBVyxFQUFFLE9BQU8sRUFBRSxPQUFPLENBQUM7SUFDMUQ7SUFFQSxJQUFJLENBQUMsSUFBSSxHQUFHLElBQUlDLFdBQUcsQ0FBQyxnQkFBZ0IsQ0FBQztBQUVyQyxJQUFBLFNBQVMsSUFBSSxHQUFBO0FBQ1gsUUFBQSxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyx3Q0FBd0MsQ0FBQztJQUMxRDtBQUNBLElBQUEsSUFBSSxDQUFDLE9B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