twilio-video
Advanced tools
Comparing version 2.0.0-beta2 to 2.0.0-beta3
@@ -0,1 +1,17 @@ | ||
2.0.0-beta3 (November 20, 2018) | ||
=============================== | ||
Bug Fixes | ||
--------- | ||
- Fixed a bug where unpublishing a LocalTrack from within one of its event | ||
listeners that have been added before publishing it to the Room throws a | ||
TypeError. (JSDK-2212) | ||
Note for Electron developers | ||
---------------------------- | ||
- twilio-video.js will no longer be usable on Electron 2.x or below. Please | ||
upgrade to Electron 3.x or higher. | ||
2.0.0-beta2 (October 1, 2018) | ||
@@ -2,0 +18,0 @@ ============================= |
@@ -373,5 +373,7 @@ 'use strict'; | ||
log.debug('Creating a new Room:', room); | ||
roomSignaling.on('stateChanged', function stateChanged() { | ||
log.info('Disconnected from Room:', room.toString()); | ||
roomSignaling.removeListener('stateChanged', stateChanged); | ||
roomSignaling.on('stateChanged', function stateChanged(state) { | ||
if (state === 'disconnected') { | ||
log.info('Disconnected from Room:', room.toString()); | ||
roomSignaling.removeListener('stateChanged', stateChanged); | ||
} | ||
}); | ||
@@ -378,0 +380,0 @@ |
@@ -195,4 +195,6 @@ 'use strict'; | ||
var trackSignaling = signaling.getPublication(localTrack._trackSender); | ||
trackSignaling.disable(); | ||
log.debug('Disabled the ' + util.trackClass(localTrack, true) + ':', localTrack.id); | ||
if (trackSignaling) { | ||
trackSignaling.disable(); | ||
log.debug('Disabled the ' + util.trackClass(localTrack, true) + ':', localTrack.id); | ||
} | ||
} | ||
@@ -202,4 +204,6 @@ | ||
var trackSignaling = signaling.getPublication(localTrack._trackSender); | ||
trackSignaling.enable(); | ||
log.debug('Enabled the ' + util.trackClass(localTrack, true) + ':', localTrack.id); | ||
if (trackSignaling) { | ||
trackSignaling.enable(); | ||
log.debug('Enabled the ' + util.trackClass(localTrack, true) + ':', localTrack.id); | ||
} | ||
} | ||
@@ -211,3 +215,5 @@ | ||
var trackSignaling = signaling.getPublication(localTrack._trackSender); | ||
trackSignaling.stop(); | ||
if (trackSignaling) { | ||
trackSignaling.stop(); | ||
} | ||
} | ||
@@ -214,0 +220,0 @@ |
@@ -62,2 +62,3 @@ 'use strict'; | ||
environment: options.environment, | ||
logLevel: options.logLevel, | ||
networkQuality: options.networkQuality, | ||
@@ -64,0 +65,0 @@ iceServerSourceStatus: iceServerSource.status, |
@@ -119,7 +119,8 @@ 'use strict'; | ||
var sdpFormat = getSdpFormat(options.sdpSemantics); | ||
var isUnifiedPlan = sdpFormat === 'unified'; | ||
var localMediaStream = isFirefox && RTCPeerConnection.prototype.addTransceiver ? null : new options.MediaStream(); | ||
var localMediaStream = isUnifiedPlan && RTCPeerConnection.prototype.addTransceiver ? null : new options.MediaStream(); | ||
if (options.dummyAudioMediaStreamTrack) { | ||
peerConnection.addTrack(options.dummyAudioMediaStreamTrack, localMediaStream || new MediaStream()); | ||
peerConnection.addTrack(options.dummyAudioMediaStreamTrack, localMediaStream || new options.MediaStream()); | ||
} | ||
@@ -153,2 +154,5 @@ | ||
}, | ||
_isUnifiedPlan: { | ||
value: isUnifiedPlan | ||
}, | ||
_lastIceConnectionState: { | ||
@@ -532,3 +536,3 @@ writable: true, | ||
// the same technique as Firefox. | ||
: isSafari || this._sdpFormat === 'unified' ? new OrderedTrackMatcher() : new IdentityTrackMatcher(); | ||
: isSafari || this._isUnifiedPlan ? new OrderedTrackMatcher() : new IdentityTrackMatcher(); | ||
} | ||
@@ -561,3 +565,3 @@ this._trackMatcher.update(sdp); | ||
// NOTE(mmalavalli): In Firefox, if the remote RTCPeerConnection sends | ||
// NOTE(mmalavalli): For "unified-plan" sdps, if the remote RTCPeerConnection sends | ||
// an offer with fewer audio m= lines than the number of audio RTCRTPSenders | ||
@@ -575,3 +579,3 @@ // in the local RTCPeerConnection, then the local RTCPeerConnection creates | ||
// value > 1. | ||
if (isFirefox) { | ||
if (this._isUnifiedPlan) { | ||
var senders = this._peerConnection.getSenders().filter(function (sender) { | ||
@@ -649,3 +653,2 @@ return sender.track; | ||
var revision = description.revision; | ||
var vp8SimulcastRequested = this._preferredVideoCodecs.some(function (codecSettings) { | ||
@@ -662,9 +665,6 @@ return codecSettings.codec.toLowerCase() === 'vp8' && codecSettings.simulcast; | ||
type: description.type, | ||
sdp: isChrome && vp8SimulcastRequested ? _this8._setSimulcast(description.sdp, _this8._trackIdsToAttributes) : description.sdp | ||
sdp: isChrome && vp8SimulcastRequested ? _this8._setSimulcast(description.sdp, _this8._sdpFormat, _this8._trackIdsToAttributes) : description.sdp | ||
}; | ||
} | ||
description = new _this8._RTCSessionDescription(description); | ||
if (description.type === 'answer') { | ||
_this8._lastStableDescriptionRevision = revision; | ||
} | ||
return _this8._peerConnection.setLocalDescription(description); | ||
@@ -683,2 +683,4 @@ }).catch(function (error) { | ||
_this8._descriptionRevision++; | ||
} else if (description.type === 'answer') { | ||
_this8._lastStableDescriptionRevision = _this8._descriptionRevision; | ||
} | ||
@@ -720,3 +722,3 @@ _this8._localUfrag = getUfrag(description); | ||
} | ||
if (_this9._sdpFormat === 'unified' && _this9._peerConnection.getTransceivers) { | ||
if (_this9._isUnifiedPlan && _this9._peerConnection.getTransceivers) { | ||
_this9._peerConnection.getTransceivers().forEach(function (transceiver) { | ||
@@ -789,5 +791,2 @@ if (shouldStopTransceiver(description, transceiver)) { | ||
return Promise.resolve().then(function () { | ||
if (description.type === 'answer') { | ||
_this10._lastStableDescriptionRevision = revision; | ||
} | ||
return _this10._setRemoteDescription(description); | ||
@@ -798,2 +797,3 @@ }).catch(function () { | ||
if (description.type === 'answer') { | ||
_this10._lastStableDescriptionRevision = revision; | ||
_this10._needsInitialAnswer = false; | ||
@@ -800,0 +800,0 @@ } |
@@ -120,6 +120,23 @@ 'use strict'; | ||
_createClass(PeerConnectionManager, [{ | ||
key: '_getConfiguration', | ||
key: '_closeAbsentPeerConnections', | ||
/** | ||
* Close the {@link PeerConnectionV2}s which are no longer relevant. | ||
* @param {Array<object>} peerConnectionStates | ||
* @returns {this} | ||
*/ | ||
value: function _closeAbsentPeerConnections(peerConnectionStates) { | ||
var peerConnectionIds = new Set(peerConnectionStates.map(function (peerConnectionState) { | ||
return peerConnectionState.id; | ||
})); | ||
this._peerConnections.forEach(function (peerConnection) { | ||
if (!peerConnectionIds.has(peerConnection.id)) { | ||
peerConnection._close(); | ||
} | ||
}); | ||
return this; | ||
} | ||
/** | ||
* Get the {@link PeerConnectionManager}'s configuration. | ||
@@ -129,2 +146,5 @@ * @private | ||
*/ | ||
}, { | ||
key: '_getConfiguration', | ||
value: function _getConfiguration() { | ||
@@ -312,2 +332,3 @@ return this._configurationDeferred.promise; | ||
* @param {Array<object>} peerConnectionStates | ||
* @param {boolean} [synced=false] | ||
* @returns {Promise<this>} | ||
@@ -321,2 +342,7 @@ */ | ||
var synced = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; | ||
if (synced) { | ||
this._closeAbsentPeerConnections(peerConnectionStates); | ||
} | ||
return this._getConfiguration().then(function (configuration) { | ||
@@ -323,0 +349,0 @@ return Promise.all(peerConnectionStates.map(function (peerConnectionState) { |
@@ -276,4 +276,2 @@ 'use strict'; | ||
var participantsToKeep = new Set(); | ||
if (roomState.subscribed && roomState.subscribed.revision > this._subscribedRevision) { | ||
@@ -283,8 +281,8 @@ this._subscribedRevision = roomState.subscribed.revision; | ||
if (trackState.id) { | ||
this._subscriptionFailures.delete(trackState.sid); | ||
this._subscribed.set(trackState.id, trackState.sid); | ||
} else if (trackState.error && !this._subscriptionFailures.has(trackState.sid)) { | ||
this._subscriptionFailures.set(trackState.sid, trackState.error); | ||
_this3._subscriptionFailures.delete(trackState.sid); | ||
_this3._subscribed.set(trackState.id, trackState.sid); | ||
} else if (trackState.error && !_this3._subscriptionFailures.has(trackState.sid)) { | ||
_this3._subscriptionFailures.set(trackState.sid, trackState.error); | ||
} | ||
}, this); | ||
}); | ||
@@ -304,12 +302,22 @@ var subscribedTrackIds = new Set(roomState.subscribed.tracks.filter(function (trackState) { | ||
var participantsToKeep = new Set(); | ||
// TODO(mroberts): Remove me once the Server is fixed. | ||
(roomState.participants || []).forEach(function (participantState) { | ||
if (participantState.sid === this.localParticipant.sid || this._disconnectedParticipantSids.has(participantState.sid)) { | ||
if (participantState.sid === _this3.localParticipant.sid || _this3._disconnectedParticipantSids.has(participantState.sid)) { | ||
return; | ||
} | ||
var participant = this._getOrCreateRemoteParticipant(participantState); | ||
var participant = _this3._getOrCreateRemoteParticipant(participantState); | ||
participant.update(participantState); | ||
participantsToKeep.add(participant); | ||
}, this); | ||
}); | ||
if (roomState.type === 'synced') { | ||
this.participants.forEach(function (participant) { | ||
if (!participantsToKeep.has(participant)) { | ||
participant.disconnect(); | ||
} | ||
}); | ||
} | ||
handleSubscriptions(this); | ||
@@ -320,3 +328,3 @@ | ||
if (roomState.peer_connections) { | ||
this._peerConnectionManager.update(roomState.peer_connections); | ||
this._peerConnectionManager.update(roomState.peer_connections, roomState.type === 'synced'); | ||
} | ||
@@ -332,5 +340,5 @@ | ||
if (track.sid) { | ||
this._published.set(track.id, track.sid); | ||
_this3._published.set(track.id, track.sid); | ||
} | ||
}, this); | ||
}); | ||
this.localParticipant.update(roomState.published); | ||
@@ -337,0 +345,0 @@ } |
@@ -18,14 +18,20 @@ 'use strict'; | ||
var StateMachine = require('../../statemachine'); | ||
var util = require('../../util'); | ||
var _require = require('../../util/twilio-video-errors'), | ||
RoomCompletedError = _require.RoomCompletedError; | ||
var _require = require('../../util'), | ||
createMediaSignalingPayload = _require.createMediaSignalingPayload, | ||
getSdpFormat = _require.getSdpFormat, | ||
getUserAgent = _require.getUserAgent, | ||
makeServerSIPURI = _require.makeServerSIPURI, | ||
withJitter = _require.withJitter; | ||
var _require2 = require('../../util/twilio-video-errors'), | ||
SignalingConnectionDisconnectedError = _require2.SignalingConnectionDisconnectedError, | ||
SignalingConnectionError = _require2.SignalingConnectionError, | ||
SignalingConnectionTimeoutError = _require2.SignalingConnectionTimeoutError, | ||
SignalingIncomingMessageInvalidError = _require2.SignalingIncomingMessageInvalidError, | ||
createTwilioError = _require2.createTwilioError; | ||
RoomCompletedError = _require2.RoomCompletedError; | ||
var _require3 = require('../../util/twilio-video-errors'), | ||
SignalingConnectionDisconnectedError = _require3.SignalingConnectionDisconnectedError, | ||
SignalingConnectionError = _require3.SignalingConnectionError, | ||
SignalingConnectionTimeoutError = _require3.SignalingConnectionTimeoutError, | ||
SignalingIncomingMessageInvalidError = _require3.SignalingIncomingMessageInvalidError, | ||
createTwilioError = _require3.createTwilioError; | ||
var SDK_NAME = packageInfo.name + '.js'; | ||
@@ -95,3 +101,3 @@ var SDK_VERSION = packageInfo.version; | ||
SIPJSMediaHandler: DefaultSIPJSMediaHandler, | ||
userAgent: util.getUserAgent() | ||
userAgent: getUserAgent() | ||
}, options); | ||
@@ -234,3 +240,3 @@ | ||
function createSession(transport, name, accessToken, localParticipant, peerConnectionManager, ua, SIPJSMediaHandler, iceServerSourceStatus, dominantSpeaker, networkQuality) { | ||
var target = 'sip:' + util.makeServerSIPURI(); | ||
var target = 'sip:' + makeServerSIPURI(); | ||
return ua.invite(target, { | ||
@@ -266,6 +272,6 @@ extraHeaders: [constants.headers.X_TWILIO_ACCESSTOKEN + ': ' + accessToken, 'Session-Expires: 120'], | ||
}; | ||
message.media_signaling = util.createMediaSignalingPayload(dominantSpeaker, networkQuality); | ||
message.media_signaling = createMediaSignalingPayload(dominantSpeaker, networkQuality); | ||
} | ||
var sdpFormat = util.getSdpFormat(transport._sdpSemantics); | ||
var sdpFormat = getSdpFormat(transport._sdpSemantics); | ||
if (type === 'connect' && sdpFormat) { | ||
@@ -285,14 +291,2 @@ message.format = sdpFormat; | ||
/** | ||
* Add random jitter to a given value in the range [-jitter, jitter]. | ||
* @private | ||
* @param {number} value | ||
* @param {number} jitter | ||
* @returns {number} value + random(-jitter, +jitter) | ||
*/ | ||
function withJitter(value, jitter) { | ||
var rand = Math.random(); | ||
return value - jitter + Math.floor(2 * jitter * rand + 0.5); | ||
} | ||
function publishWithRetries(transport, session, payload, attempts) { | ||
@@ -299,0 +293,0 @@ attempts = attempts || 0; |
@@ -16,9 +16,17 @@ 'use strict'; | ||
var TwilioConnection = require('../../twilioconnection'); | ||
var util = require('../../util'); | ||
var _require = require('../../util/twilio-video-errors'), | ||
createTwilioError = _require.createTwilioError, | ||
RoomCompletedError = _require.RoomCompletedError, | ||
SignalingConnectionError = _require.SignalingConnectionError; | ||
var _require = require('../../util'), | ||
createMediaSignalingPayload = _require.createMediaSignalingPayload, | ||
getSdpFormat = _require.getSdpFormat, | ||
getUserAgent = _require.getUserAgent, | ||
withJitter = _require.withJitter; | ||
var _require2 = require('../../util/twilio-video-errors'), | ||
createTwilioError = _require2.createTwilioError, | ||
RoomCompletedError = _require2.RoomCompletedError, | ||
SignalingConnectionError = _require2.SignalingConnectionError; | ||
var MAX_RECONNECT_ATTEMPTS = 5; | ||
var RECONNECT_BACKOFF_JITTER = 10; | ||
var RECONNECT_BACKOFF_MS = 50; | ||
var SDK_NAME = packageInfo.name + '.js'; | ||
@@ -86,3 +94,6 @@ var SDK_VERSION = packageInfo.version; | ||
TwilioConnection: TwilioConnection, | ||
userAgent: util.getUserAgent() | ||
maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS, | ||
reconnectBackOffJitter: RECONNECT_BACKOFF_JITTER, | ||
reconnectBackOffMs: RECONNECT_BACKOFF_MS, | ||
userAgent: getUserAgent() | ||
}, options); | ||
@@ -120,5 +131,18 @@ | ||
}, | ||
_options: { | ||
value: options | ||
}, | ||
_peerConnectionManager: { | ||
value: peerConnectionManager | ||
}, | ||
_reconnectAttemptsLeft: { | ||
value: options.maxReconnectAttempts, | ||
writable: true | ||
}, | ||
_reconnectBackOffJitter: { | ||
value: options.reconnectBackOffJitter | ||
}, | ||
_reconnectBackOffMs: { | ||
value: options.reconnectBackOffMs | ||
}, | ||
_sdpSemantics: { | ||
@@ -132,3 +156,4 @@ value: options.sdpSemantics | ||
_twilioConnection: { | ||
value: new options.TwilioConnection(wsServer, options) | ||
value: null, | ||
writable: true | ||
}, | ||
@@ -143,2 +168,5 @@ _updatesReceived: { | ||
value: options.userAgent | ||
}, | ||
_wsServer: { | ||
value: wsServer | ||
} | ||
@@ -194,5 +222,5 @@ }); | ||
message.media_signaling = util.createMediaSignalingPayload(this._dominantSpeaker, this._networkQuality); | ||
message.media_signaling = createMediaSignalingPayload(this._dominantSpeaker, this._networkQuality); | ||
var sdpFormat = util.getSdpFormat(this._sdpSemantics); | ||
var sdpFormat = getSdpFormat(this._sdpSemantics); | ||
if (sdpFormat) { | ||
@@ -359,3 +387,18 @@ message.format = sdpFormat; | ||
function createOrResetTwilioConnection() { | ||
if (transport._twilioConnection) { | ||
transport._twilioConnection.removeListener('message', handleMessage); | ||
} | ||
var _options = transport._options, | ||
_wsServer = transport._wsServer; | ||
var TwilioConnection = transport._options.TwilioConnection; | ||
transport._twilioConnection = new TwilioConnection(_wsServer, _options); | ||
return transport._twilioConnection; | ||
} | ||
function disconnect(error) { | ||
if (transport.state === 'disconnected') { | ||
return; | ||
} | ||
if (!error) { | ||
@@ -365,5 +408,37 @@ transport.disconnect(); | ||
} | ||
transport.disconnect(new SignalingConnectionError()); | ||
if (transport._reconnectAttemptsLeft <= 0) { | ||
transport.disconnect(new SignalingConnectionError()); | ||
return; | ||
} | ||
reconnect(); | ||
} | ||
function reconnect() { | ||
if (transport.state === 'connected') { | ||
transport.preempt('syncing'); | ||
} | ||
transport._reconnectAttemptsLeft--; | ||
var maxReconnectAttempts = transport._options.maxReconnectAttempts; | ||
var reconnectAttempts = maxReconnectAttempts - transport._reconnectAttemptsLeft; | ||
var backOffMs = (1 << reconnectAttempts) * transport._reconnectBackOffMs; | ||
setTimeout(startConnect, withJitter(backOffMs, transport._reconnectBackOffJitter)); | ||
} | ||
function resetReconnectAttemptsLeft() { | ||
var maxReconnectAttempts = transport._options.maxReconnectAttempts; | ||
transport._reconnectAttemptsLeft = maxReconnectAttempts; | ||
} | ||
function startConnect() { | ||
if (transport.state === 'disconnected') { | ||
return; | ||
} | ||
var twilioConnection = createOrResetTwilioConnection(); | ||
twilioConnection.once('close', disconnect); | ||
twilioConnection.on('message', handleMessage); | ||
twilioConnection.once('open', connect); | ||
} | ||
function handleMessage(message) { | ||
@@ -411,2 +486,5 @@ switch (transport.state) { | ||
switch (message.type) { | ||
case 'error': | ||
transport.disconnect(createTwilioError(message.code, message.message)); | ||
return; | ||
case 'connected': | ||
@@ -417,2 +495,3 @@ case 'update': | ||
case 'synced': | ||
resetReconnectAttemptsLeft(); | ||
transport.emit('message', message); | ||
@@ -434,7 +513,2 @@ transport.preempt('connected'); | ||
var twilioConnection = transport._twilioConnection; | ||
twilioConnection.once('close', disconnect); | ||
twilioConnection.on('message', handleMessage); | ||
twilioConnection.once('open', connect); | ||
transport.on('stateChanged', function stateChanged(state) { | ||
@@ -454,3 +528,3 @@ switch (state) { | ||
case 'disconnected': | ||
twilioConnection.removeListener('message', handleMessage); | ||
transport._twilioConnection.removeListener('message', handleMessage); | ||
transport.removeListener('stateChanged', stateChanged); | ||
@@ -466,4 +540,6 @@ return; | ||
}); | ||
startConnect(); | ||
} | ||
module.exports = TwilioConnectionTransport; |
@@ -14,6 +14,10 @@ 'use strict'; | ||
var _require = require('./util'), | ||
buildLogLevels = _require.buildLogLevels, | ||
makeUUID = _require.makeUUID; | ||
var Log = require('./util/log'); | ||
var Timeout = require('./util/timeout'); | ||
var nInstances = 0; | ||
/* | ||
@@ -39,3 +43,3 @@ TwilioConnection states | ||
var DEFAULT_MAX_CONSECUTIVE_MISSED_HEARTBEATS = 5; | ||
var DEFAULT_MAX_CONSECUTIVE_MISSED_HEARTBEATS = 3; | ||
var DEFAULT_MAX_REQUESTED_HEARTBEAT_TIMEOUT = 5000; | ||
@@ -74,2 +78,13 @@ var DEFAULT_WELCOME_TIMEOUT = 5000; | ||
options = Object.assign({ | ||
maxConsecutiveMissedHeartbeats: DEFAULT_MAX_CONSECUTIVE_MISSED_HEARTBEATS, | ||
requestedHeartbeatTimeout: DEFAULT_MAX_REQUESTED_HEARTBEAT_TIMEOUT, | ||
welcomeTimeout: DEFAULT_WELCOME_TIMEOUT, | ||
Log: Log, | ||
WebSocket: WebSocket | ||
}, options); | ||
var logLevels = buildLogLevels(options.logLevel); | ||
var log = new options.Log('default', _this, logLevels); | ||
Object.defineProperties(_this, { | ||
@@ -84,2 +99,8 @@ _consecutiveHeartbeatsMissed: { | ||
}, | ||
_instanceId: { | ||
value: ++nInstances | ||
}, | ||
_log: { | ||
value: log | ||
}, | ||
_messageQueue: { | ||
@@ -89,8 +110,3 @@ value: [] | ||
_options: { | ||
value: Object.assign({ | ||
maxConsecutiveMissedHeartbeats: DEFAULT_MAX_CONSECUTIVE_MISSED_HEARTBEATS, | ||
requestedHeartbeatTimeout: DEFAULT_MAX_REQUESTED_HEARTBEAT_TIMEOUT, | ||
welcomeTimeout: DEFAULT_WELCOME_TIMEOUT, | ||
WebSocket: WebSocket | ||
}, options) | ||
value: options | ||
}, | ||
@@ -126,13 +142,58 @@ _sendHeartbeatTimeout: { | ||
/** | ||
* The number of consecutive "hearbeat" messages missed. | ||
* @property {number} | ||
*/ | ||
_createClass(TwilioConnection, [{ | ||
key: 'toString', | ||
value: function toString() { | ||
return '[TwilioConnection #' + this._instanceId + ': ' + this._ws.url + ']'; | ||
} | ||
/** | ||
* The number of consecutive "hearbeat" messages missed. | ||
* @property {number} | ||
*/ | ||
_createClass(TwilioConnection, [{ | ||
key: '_connect', | ||
}, { | ||
key: '_close', | ||
/** | ||
* Close the {@link TwilioConnection}. | ||
* @param {{code: number, reason: string}} event | ||
* @private | ||
*/ | ||
value: function _close(_ref) { | ||
var code = _ref.code, | ||
reason = _ref.reason; | ||
if (this.state === 'closed') { | ||
return; | ||
} | ||
if (this._welcomeTimeout) { | ||
this._welcomeTimeout.clear(); | ||
} | ||
if (this._heartbeatTimeout) { | ||
this._heartbeatTimeout.clear(); | ||
} | ||
if (this._sendHeartbeatTimeout) { | ||
this._sendHeartbeatTimeout.clear(); | ||
} | ||
this._messageQueue.splice(0); | ||
var log = this._log; | ||
if (code === WS_CLOSE_NORMAL) { | ||
log.debug('Closed'); | ||
} else { | ||
log.warn('Closed: ' + code + ' - ' + reason); | ||
} | ||
this.transition('closed', null, code !== WS_CLOSE_NORMAL ? new Error('WebSocket Error ' + code + ': ' + reason) : null); | ||
var readyState = this._ws.readyState; | ||
var WebSocket = this._options.WebSocket; | ||
if (readyState !== WebSocket.CLOSING && readyState !== WebSocket.CLOSED) { | ||
this._ws.close(code, reason); | ||
} | ||
} | ||
/** | ||
* Connect to the TCMP server. | ||
@@ -142,2 +203,5 @@ * @param {string} serverUrl | ||
*/ | ||
}, { | ||
key: '_connect', | ||
value: function _connect(serverUrl) { | ||
@@ -147,19 +211,12 @@ var _this2 = this; | ||
this._ws = new this._options.WebSocket(serverUrl); | ||
var log = this._log; | ||
var ws = this._ws; | ||
log.debug('Created a new WebSocket:', ws); | ||
ws.addEventListener('close', function (event) { | ||
if (_this2._welcomeTimeout) { | ||
_this2._welcomeTimeout.clear(); | ||
} | ||
if (_this2._heartbeatTimeout) { | ||
_this2._heartbeatTimeout.clear(); | ||
} | ||
if (_this2._sendHeartbeatTimeout) { | ||
_this2._sendHeartbeatTimeout.clear(); | ||
} | ||
_this2._messageQueue.splice(0); | ||
_this2.transition('closed', null, event.code !== WS_CLOSE_NORMAL ? new Error('WebSocket Error ' + event.code + ': ' + event.reason) : null); | ||
return _this2._close(event); | ||
}); | ||
ws.addEventListener('message', function (message) { | ||
log.debug('Incoming: ' + message.data); | ||
try { | ||
@@ -188,2 +245,3 @@ message = JSON.parse(message.data); | ||
default: | ||
_this2._log.debug('Unknown message type: ' + message.type); | ||
_this2.emit('error', new Error('Unknown message type: ' + message.type)); | ||
@@ -195,2 +253,3 @@ break; | ||
ws.addEventListener('open', function () { | ||
log.debug('WebSocket opened:', ws); | ||
_this2._sendHello(); | ||
@@ -213,9 +272,12 @@ var welcomeTimeout = _this2._options.welcomeTimeout; | ||
key: '_handleBad', | ||
value: function _handleBad(_ref) { | ||
var reason = _ref.reason; | ||
value: function _handleBad(_ref2) { | ||
var reason = _ref2.reason; | ||
var log = this._log; | ||
if (this.state === 'connecting') { | ||
this._ws.close(WS_CLOSE_HELLO_FAILED, reason); | ||
log.warn('Closing: ' + WS_CLOSE_HELLO_FAILED + ' - ' + reason); | ||
this._close({ code: WS_CLOSE_HELLO_FAILED, reason: reason }); | ||
return; | ||
} | ||
log.debug('Error: ' + reason); | ||
this.emit('error', new Error(reason)); | ||
@@ -251,5 +313,7 @@ } | ||
this._consecutiveHeartbeatsMissed++; | ||
var log = this._log; | ||
var maxConsecutiveMissedHeartbeats = this._options.maxConsecutiveMissedHeartbeats; | ||
log.debug('Consecutive heartbeats missed: ' + this._consecutiveHeartbeatsMissed); | ||
if (this._consecutiveHeartbeatsMissed < maxConsecutiveMissedHeartbeats) { | ||
@@ -259,3 +323,6 @@ this._heartbeatTimeout.reset(); | ||
} | ||
this._ws.close(WS_CLOSE_HEARTBEATS_MISSED, 'Missed ' + maxConsecutiveMissedHeartbeats + ' "heartbeat" messages'); | ||
var reason = 'Missed ' + maxConsecutiveMissedHeartbeats + ' "heartbeat" messages'; | ||
log.warn('Closing: ' + WS_CLOSE_HEARTBEATS_MISSED + ' - ' + reason); | ||
this._close({ code: WS_CLOSE_HEARTBEATS_MISSED, reason: reason }); | ||
} | ||
@@ -271,4 +338,4 @@ | ||
key: '_handleMessage', | ||
value: function _handleMessage(_ref2) { | ||
var body = _ref2.body; | ||
value: function _handleMessage(_ref3) { | ||
var body = _ref3.body; | ||
@@ -289,6 +356,6 @@ if (this.state !== 'open') { | ||
key: '_handleWelcome', | ||
value: function _handleWelcome(_ref3) { | ||
value: function _handleWelcome(_ref4) { | ||
var _this3 = this; | ||
var negotiatedTimeout = _ref3.negotiatedTimeout; | ||
var negotiatedTimeout = _ref4.negotiatedTimeout; | ||
@@ -323,3 +390,5 @@ if (this.state !== 'connecting') { | ||
} | ||
this._ws.close(WS_CLOSE_WELCOME_TIMEOUT, '"welcome" message timeout expired'); | ||
var reason = '"welcome" message timeout expired'; | ||
this._log.warn('Closing: ' + WS_CLOSE_WELCOME_TIMEOUT + ' - ' + reason); | ||
this._close({ code: WS_CLOSE_WELCOME_TIMEOUT, reason: reason }); | ||
} | ||
@@ -336,3 +405,10 @@ | ||
value: function _send(message) { | ||
this._ws.send(JSON.stringify(message)); | ||
var readyState = this._ws.readyState; | ||
var WebSocket = this._options.WebSocket; | ||
if (readyState === WebSocket.OPEN) { | ||
var data = JSON.stringify(message); | ||
this._log.debug('Outgoing: ' + data); | ||
this._ws.send(data); | ||
} | ||
} | ||
@@ -407,3 +483,3 @@ | ||
this._sendOrEnqueue({ type: 'bye' }); | ||
this._ws.close(); | ||
this._ws.close(WS_CLOSE_NORMAL); | ||
} | ||
@@ -458,2 +534,3 @@ | ||
* @typedef {object} TwilioConnectionOptions | ||
* @property {LogLevel} [logLevel=warn] - Log level of the {@link TwilioConnection} | ||
* @property {number} [maxConsecutiveMissedHeartbeats=5] - Max. number of consecutive "heartbeat" messages that can be missed | ||
@@ -460,0 +537,0 @@ * @property {number} [requestedHeartbeatTimeout=5000] - "heartbeat" timeout (ms) requested by the {@link TwilioConnection} |
@@ -642,2 +642,14 @@ /* globals mozRTCPeerConnection */ | ||
/** | ||
* Add random jitter to a given value in the range [-jitter, jitter]. | ||
* @private | ||
* @param {number} value | ||
* @param {number} jitter | ||
* @returns {number} value + random(-jitter, +jitter) | ||
*/ | ||
function withJitter(value, jitter) { | ||
var rand = Math.random(); | ||
return value - jitter + Math.floor(2 * jitter * rand + 0.5); | ||
} | ||
exports.constants = constants; | ||
@@ -671,2 +683,3 @@ exports.createMediaSignalingPayload = createMediaSignalingPayload; | ||
exports.getSdpFormat = getSdpFormat; | ||
exports.valueToJSON = valueToJSON; | ||
exports.valueToJSON = valueToJSON; | ||
exports.withJitter = withJitter; |
@@ -233,6 +233,7 @@ 'use strict'; | ||
* @param {string} sdp | ||
* @param {'planb' | 'unified'} sdpFormat | ||
* @param {Map<Track.ID, TrackAttributes>} trackIdsToAttributes | ||
* @returns {string} Updated SDP string | ||
*/ | ||
function setSimulcast(sdp, trackIdsToAttributes) { | ||
function setSimulcast(sdp, sdpFormat, trackIdsToAttributes) { | ||
var mediaSections = getMediaSections(sdp); | ||
@@ -252,3 +253,3 @@ var session = sdp.split('\r\nm=')[0]; | ||
}); | ||
return hasVP8PayloadType ? setSimulcastInMediaSection(section, trackIdsToAttributes) : section; | ||
return hasVP8PayloadType ? setSimulcastInMediaSection(section, sdpFormat, trackIdsToAttributes) : section; | ||
})).concat('').join('\r\n'); | ||
@@ -255,0 +256,0 @@ } |
'use strict'; | ||
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
@@ -32,3 +34,3 @@ | ||
* @param {Track.ID} trackId - The MediaStreamTrack ID | ||
* @param {string} streamId - The MediaStream ID | ||
* @param {MediaStreamID} streamId - The MediaStream ID | ||
* @param {string} cName - The MediaStream cname | ||
@@ -199,10 +201,50 @@ */ | ||
/** | ||
* Create SSRC attribute tuples. | ||
* @param {string} section | ||
* @param {'planb' | 'unified'} sdpFormat | ||
* @returns {Array<[SSRC, MediaStreamID, Track.ID]>} | ||
*/ | ||
function createSSRCAttributeTuples(section, sdpFormat) { | ||
return { | ||
planb: createPlanBSSRCAttributeTuples, | ||
unified: createUnifiedPlanSSRCAttributeTuples | ||
}[sdpFormat](section); | ||
} | ||
/** | ||
* Create "plan-b" SSRC attribute tuples. | ||
* @param {string} section | ||
* @returns {Array<[SSRC, MediaStreamID, Track.ID]>} | ||
*/ | ||
function createPlanBSSRCAttributeTuples(section) { | ||
return getMatches(section, '^a=ssrc:([0-9]+) msid:([^\\s]+) ([^\\s]+)$'); | ||
} | ||
/** | ||
* Create "unified-plan" SSRC attribute tuples. | ||
* @param {string} section | ||
* @returns {Array<[SSRC, MediaStreamID, Track.ID]>} | ||
*/ | ||
function createUnifiedPlanSSRCAttributeTuples(section) { | ||
var _flatMap = flatMap(getMatches(section, '^a=msid:(.+) (.+)$')), | ||
_flatMap2 = _slicedToArray(_flatMap, 2), | ||
streamId = _flatMap2[0], | ||
trackId = _flatMap2[1]; | ||
var ssrcs = flatMap(getMatches(section, '^a=ssrc:(.+) cname:.+$')); | ||
return ssrcs.map(function (ssrc) { | ||
return [ssrc, streamId, trackId]; | ||
}); | ||
} | ||
/** | ||
* Create a Map of MediaStreamTrack IDs and their {@link TrackAttributes}. | ||
* @param {string} section - SDP media section | ||
* @param {'planb' | 'unified'} sdpFormat | ||
* @returns {Map<Track.ID, TrackAttributes>} | ||
*/ | ||
function createTrackIdsToAttributes(section) { | ||
function createTrackIdsToAttributes(section, sdpFormat) { | ||
var simSSRCs = getSimulcastSSRCs(section); | ||
var ssrcAttrTuples = getMatches(section, '^a=ssrc:([0-9]+) msid:([^\\s]+) ([^\\s]+)$'); | ||
var rtxPairs = getSSRCRtxPairs(section); | ||
var ssrcAttrTuples = createSSRCAttributeTuples(section, sdpFormat); | ||
@@ -225,2 +267,3 @@ return ssrcAttrTuples.reduce(function (trackIdsToSSRCs, tuple) { | ||
* @param {string} section - SDP media section | ||
* @param {'planb' | 'unified'} sdpFormat | ||
* @param {Map<Track.ID, TrackAttributes>} trackIdsToAttributes - Existing | ||
@@ -230,4 +273,4 @@ * map which will be updated for new MediaStreamTrack IDs | ||
*/ | ||
function setSimulcastInMediaSection(section, trackIdsToAttributes) { | ||
var newTrackIdsToAttributes = createTrackIdsToAttributes(section); | ||
function setSimulcastInMediaSection(section, sdpFormat, trackIdsToAttributes) { | ||
var newTrackIdsToAttributes = createTrackIdsToAttributes(section, sdpFormat); | ||
var newTrackIds = Array.from(newTrackIdsToAttributes.keys()); | ||
@@ -263,7 +306,18 @@ var trackIds = Array.from(trackIdsToAttributes.keys()); | ||
// "relevantSdpLines" are removed. | ||
var sectionLines = new Set(section.split('\r\n').concat(relevantSdpLines)); | ||
return flatMap(sectionLines).join('\r\n'); | ||
var sectionLines = flatMap(new Set(section.split('\r\n').concat(relevantSdpLines))); | ||
var xGoogleFlagConference = 'a=x-google-flag:conference'; | ||
if (!section.match(xGoogleFlagConference)) { | ||
sectionLines.push(xGoogleFlagConference); | ||
} | ||
return sectionLines.join('\r\n'); | ||
} | ||
/** | ||
* String representing a MediaStream ID. | ||
* @typedef {string} MediaStreamID | ||
*/ | ||
/** | ||
* String representing the SSRC of a MediaStreamTrack. | ||
@@ -270,0 +324,0 @@ * @typedef {string} SSRC |
@@ -391,5 +391,7 @@ 'use strict'; | ||
log.debug('Creating a new Room:', room); | ||
roomSignaling.on('stateChanged', function stateChanged() { | ||
log.info('Disconnected from Room:', room.toString()); | ||
roomSignaling.removeListener('stateChanged', stateChanged); | ||
roomSignaling.on('stateChanged', function stateChanged(state) { | ||
if (state === 'disconnected') { | ||
log.info('Disconnected from Room:', room.toString()); | ||
roomSignaling.removeListener('stateChanged', stateChanged); | ||
} | ||
}); | ||
@@ -396,0 +398,0 @@ |
@@ -162,4 +162,6 @@ 'use strict'; | ||
const trackSignaling = signaling.getPublication(localTrack._trackSender); | ||
trackSignaling.disable(); | ||
log.debug(`Disabled the ${util.trackClass(localTrack, true)}:`, localTrack.id); | ||
if (trackSignaling) { | ||
trackSignaling.disable(); | ||
log.debug(`Disabled the ${util.trackClass(localTrack, true)}:`, localTrack.id); | ||
} | ||
} | ||
@@ -169,4 +171,6 @@ | ||
const trackSignaling = signaling.getPublication(localTrack._trackSender); | ||
trackSignaling.enable(); | ||
log.debug(`Enabled the ${util.trackClass(localTrack, true)}:`, localTrack.id); | ||
if (trackSignaling) { | ||
trackSignaling.enable(); | ||
log.debug(`Enabled the ${util.trackClass(localTrack, true)}:`, localTrack.id); | ||
} | ||
} | ||
@@ -178,3 +182,5 @@ | ||
const trackSignaling = signaling.getPublication(localTrack._trackSender); | ||
trackSignaling.stop(); | ||
if (trackSignaling) { | ||
trackSignaling.stop(); | ||
} | ||
} | ||
@@ -181,0 +187,0 @@ |
@@ -62,2 +62,3 @@ 'use strict'; | ||
environment: options.environment, | ||
logLevel: options.logLevel, | ||
networkQuality: options.networkQuality, | ||
@@ -64,0 +65,0 @@ iceServerSourceStatus: iceServerSource.status, |
@@ -103,4 +103,5 @@ 'use strict'; | ||
const sdpFormat = getSdpFormat(options.sdpSemantics); | ||
const isUnifiedPlan = sdpFormat === 'unified'; | ||
const localMediaStream = isFirefox && RTCPeerConnection.prototype.addTransceiver | ||
const localMediaStream = isUnifiedPlan && RTCPeerConnection.prototype.addTransceiver | ||
? null | ||
@@ -110,3 +111,3 @@ : new options.MediaStream(); | ||
if (options.dummyAudioMediaStreamTrack) { | ||
peerConnection.addTrack(options.dummyAudioMediaStreamTrack, localMediaStream || new MediaStream()); | ||
peerConnection.addTrack(options.dummyAudioMediaStreamTrack, localMediaStream || new options.MediaStream()); | ||
} | ||
@@ -140,2 +141,5 @@ | ||
}, | ||
_isUnifiedPlan: { | ||
value: isUnifiedPlan | ||
}, | ||
_lastIceConnectionState: { | ||
@@ -472,3 +476,3 @@ writable: true, | ||
// the same technique as Firefox. | ||
: isSafari || this._sdpFormat === 'unified' | ||
: isSafari || this._isUnifiedPlan | ||
? new OrderedTrackMatcher() | ||
@@ -500,3 +504,3 @@ : new IdentityTrackMatcher(); | ||
// NOTE(mmalavalli): In Firefox, if the remote RTCPeerConnection sends | ||
// NOTE(mmalavalli): For "unified-plan" sdps, if the remote RTCPeerConnection sends | ||
// an offer with fewer audio m= lines than the number of audio RTCRTPSenders | ||
@@ -514,3 +518,3 @@ // in the local RTCPeerConnection, then the local RTCPeerConnection creates | ||
// value > 1. | ||
if (isFirefox) { | ||
if (this._isUnifiedPlan) { | ||
const senders = this._peerConnection.getSenders().filter(sender => sender.track); | ||
@@ -577,3 +581,2 @@ shouldReoffer = ['audio', 'video'].reduce((shouldOffer, kind) => { | ||
_setLocalDescription(description) { | ||
const revision = description.revision; | ||
const vp8SimulcastRequested = this._preferredVideoCodecs.some(codecSettings => codecSettings.codec.toLowerCase() === 'vp8' && codecSettings.simulcast); | ||
@@ -589,3 +592,3 @@ | ||
sdp: isChrome && vp8SimulcastRequested | ||
? this._setSimulcast(description.sdp, this._trackIdsToAttributes) | ||
? this._setSimulcast(description.sdp, this._sdpFormat, this._trackIdsToAttributes) | ||
: description.sdp | ||
@@ -595,5 +598,2 @@ }; | ||
description = new this._RTCSessionDescription(description); | ||
if (description.type === 'answer') { | ||
this._lastStableDescriptionRevision = revision; | ||
} | ||
return this._peerConnection.setLocalDescription(description); | ||
@@ -612,2 +612,4 @@ }).catch(error => { | ||
this._descriptionRevision++; | ||
} else if (description.type === 'answer') { | ||
this._lastStableDescriptionRevision = this._descriptionRevision; | ||
} | ||
@@ -651,3 +653,3 @@ this._localUfrag = getUfrag(description); | ||
} | ||
if (this._sdpFormat === 'unified' && this._peerConnection.getTransceivers) { | ||
if (this._isUnifiedPlan && this._peerConnection.getTransceivers) { | ||
this._peerConnection.getTransceivers().forEach(transceiver => { | ||
@@ -716,11 +718,7 @@ if (shouldStopTransceiver(description, transceiver)) { | ||
const revision = description.revision; | ||
return Promise.resolve().then(() => { | ||
if (description.type === 'answer') { | ||
this._lastStableDescriptionRevision = revision; | ||
} | ||
return this._setRemoteDescription(description); | ||
}).catch(() => { | ||
return Promise.resolve().then(() => this._setRemoteDescription(description)).catch(() => { | ||
throw new MediaClientRemoteDescFailedError(); | ||
}).then(() => { | ||
if (description.type === 'answer') { | ||
this._lastStableDescriptionRevision = revision; | ||
this._needsInitialAnswer = false; | ||
@@ -727,0 +725,0 @@ } |
@@ -112,2 +112,17 @@ 'use strict'; | ||
/** | ||
* Close the {@link PeerConnectionV2}s which are no longer relevant. | ||
* @param {Array<object>} peerConnectionStates | ||
* @returns {this} | ||
*/ | ||
_closeAbsentPeerConnections(peerConnectionStates) { | ||
const peerConnectionIds = new Set(peerConnectionStates.map(peerConnectionState => peerConnectionState.id)); | ||
this._peerConnections.forEach(peerConnection => { | ||
if (!peerConnectionIds.has(peerConnection.id)) { | ||
peerConnection._close(); | ||
} | ||
}); | ||
return this; | ||
} | ||
/** | ||
* Get the {@link PeerConnectionManager}'s configuration. | ||
@@ -269,5 +284,9 @@ * @private | ||
* @param {Array<object>} peerConnectionStates | ||
* @param {boolean} [synced=false] | ||
* @returns {Promise<this>} | ||
*/ | ||
update(peerConnectionStates) { | ||
update(peerConnectionStates, synced = false) { | ||
if (synced) { | ||
this._closeAbsentPeerConnections(peerConnectionStates); | ||
} | ||
return this._getConfiguration().then(configuration => { | ||
@@ -274,0 +293,0 @@ return Promise.all(peerConnectionStates.map(peerConnectionState => { |
@@ -233,7 +233,5 @@ 'use strict'; | ||
_update(roomState) { | ||
const participantsToKeep = new Set(); | ||
if (roomState.subscribed && roomState.subscribed.revision > this._subscribedRevision) { | ||
this._subscribedRevision = roomState.subscribed.revision; | ||
roomState.subscribed.tracks.forEach(function(trackState) { | ||
roomState.subscribed.tracks.forEach(trackState => { | ||
if (trackState.id) { | ||
@@ -245,3 +243,3 @@ this._subscriptionFailures.delete(trackState.sid); | ||
} | ||
}, this); | ||
}); | ||
@@ -259,4 +257,6 @@ const subscribedTrackIds = new Set(roomState.subscribed.tracks | ||
const participantsToKeep = new Set(); | ||
// TODO(mroberts): Remove me once the Server is fixed. | ||
(roomState.participants || []).forEach(function(participantState) { | ||
(roomState.participants || []).forEach(participantState => { | ||
if (participantState.sid === this.localParticipant.sid || | ||
@@ -269,4 +269,12 @@ this._disconnectedParticipantSids.has(participantState.sid)) { | ||
participantsToKeep.add(participant); | ||
}, this); | ||
}); | ||
if (roomState.type === 'synced') { | ||
this.participants.forEach(participant => { | ||
if (!participantsToKeep.has(participant)) { | ||
participant.disconnect(); | ||
} | ||
}); | ||
} | ||
handleSubscriptions(this); | ||
@@ -277,3 +285,3 @@ | ||
if (roomState.peer_connections) { | ||
this._peerConnectionManager.update(roomState.peer_connections); | ||
this._peerConnectionManager.update(roomState.peer_connections, roomState.type === 'synced'); | ||
} | ||
@@ -287,7 +295,7 @@ | ||
this._publishedRevision = roomState.published.revision; | ||
roomState.published.tracks.forEach(function(track) { | ||
roomState.published.tracks.forEach(track => { | ||
if (track.sid) { | ||
this._published.set(track.id, track.sid); | ||
} | ||
}, this); | ||
}); | ||
this.localParticipant.update(roomState.published); | ||
@@ -294,0 +302,0 @@ } |
@@ -10,3 +10,10 @@ 'use strict'; | ||
const StateMachine = require('../../statemachine'); | ||
const util = require('../../util'); | ||
const { | ||
createMediaSignalingPayload, | ||
getSdpFormat, | ||
getUserAgent, | ||
makeServerSIPURI, | ||
withJitter | ||
} = require('../../util'); | ||
const { RoomCompletedError } = require('../../util/twilio-video-errors'); | ||
@@ -90,3 +97,3 @@ | ||
SIPJSMediaHandler: DefaultSIPJSMediaHandler, | ||
userAgent: util.getUserAgent() | ||
userAgent: getUserAgent() | ||
}, options); | ||
@@ -228,3 +235,3 @@ super('connecting', states); | ||
function createSession(transport, name, accessToken, localParticipant, peerConnectionManager, ua, SIPJSMediaHandler, iceServerSourceStatus, dominantSpeaker, networkQuality) { | ||
const target = `sip:${util.makeServerSIPURI()}`; | ||
const target = `sip:${makeServerSIPURI()}`; | ||
return ua.invite(target, { | ||
@@ -263,3 +270,3 @@ extraHeaders: [ | ||
}; | ||
message.media_signaling = util.createMediaSignalingPayload( | ||
message.media_signaling = createMediaSignalingPayload( | ||
dominantSpeaker, | ||
@@ -269,3 +276,3 @@ networkQuality); | ||
const sdpFormat = util.getSdpFormat(transport._sdpSemantics); | ||
const sdpFormat = getSdpFormat(transport._sdpSemantics); | ||
if (type === 'connect' && sdpFormat) { | ||
@@ -285,14 +292,2 @@ message.format = sdpFormat; | ||
/** | ||
* Add random jitter to a given value in the range [-jitter, jitter]. | ||
* @private | ||
* @param {number} value | ||
* @param {number} jitter | ||
* @returns {number} value + random(-jitter, +jitter) | ||
*/ | ||
function withJitter(value, jitter) { | ||
const rand = Math.random(); | ||
return value - jitter + Math.floor(2 * jitter * rand + 0.5); | ||
} | ||
function publishWithRetries(transport, session, payload, attempts) { | ||
@@ -299,0 +294,0 @@ attempts = attempts || 0; |
@@ -8,5 +8,11 @@ 'use strict'; | ||
const TwilioConnection = require('../../twilioconnection'); | ||
const util = require('../../util'); | ||
const { | ||
createMediaSignalingPayload, | ||
getSdpFormat, | ||
getUserAgent, | ||
withJitter | ||
} = require('../../util'); | ||
const { | ||
createTwilioError, | ||
@@ -17,2 +23,5 @@ RoomCompletedError, | ||
const MAX_RECONNECT_ATTEMPTS = 5; | ||
const RECONNECT_BACKOFF_JITTER = 10; | ||
const RECONNECT_BACKOFF_MS = 50; | ||
const SDK_NAME = `${packageInfo.name}.js`; | ||
@@ -84,3 +93,6 @@ const SDK_VERSION = packageInfo.version; | ||
TwilioConnection, | ||
userAgent: util.getUserAgent() | ||
maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS, | ||
reconnectBackOffJitter: RECONNECT_BACKOFF_JITTER, | ||
reconnectBackOffMs: RECONNECT_BACKOFF_MS, | ||
userAgent: getUserAgent() | ||
}, options); | ||
@@ -123,5 +135,18 @@ super('connecting', states); | ||
}, | ||
_options: { | ||
value: options | ||
}, | ||
_peerConnectionManager: { | ||
value: peerConnectionManager | ||
}, | ||
_reconnectAttemptsLeft: { | ||
value: options.maxReconnectAttempts, | ||
writable: true | ||
}, | ||
_reconnectBackOffJitter: { | ||
value: options.reconnectBackOffJitter | ||
}, | ||
_reconnectBackOffMs: { | ||
value: options.reconnectBackOffMs | ||
}, | ||
_sdpSemantics: { | ||
@@ -135,3 +160,4 @@ value: options.sdpSemantics | ||
_twilioConnection: { | ||
value: new options.TwilioConnection(wsServer, options) | ||
value: null, | ||
writable: true | ||
}, | ||
@@ -146,2 +172,5 @@ _updatesReceived: { | ||
value: options.userAgent | ||
}, | ||
_wsServer: { | ||
value: wsServer | ||
} | ||
@@ -192,7 +221,7 @@ }); | ||
message.media_signaling = util.createMediaSignalingPayload( | ||
message.media_signaling = createMediaSignalingPayload( | ||
this._dominantSpeaker, | ||
this._networkQuality); | ||
const sdpFormat = util.getSdpFormat(this._sdpSemantics); | ||
const sdpFormat = getSdpFormat(this._sdpSemantics); | ||
if (sdpFormat) { | ||
@@ -345,3 +374,16 @@ message.format = sdpFormat; | ||
function createOrResetTwilioConnection() { | ||
if (transport._twilioConnection) { | ||
transport._twilioConnection.removeListener('message', handleMessage); | ||
} | ||
const { _options, _wsServer } = transport; | ||
const { TwilioConnection } = transport._options; | ||
transport._twilioConnection = new TwilioConnection(_wsServer, _options); | ||
return transport._twilioConnection; | ||
} | ||
function disconnect(error) { | ||
if (transport.state === 'disconnected') { | ||
return; | ||
} | ||
if (!error) { | ||
@@ -351,5 +393,35 @@ transport.disconnect(); | ||
} | ||
transport.disconnect(new SignalingConnectionError()); | ||
if (transport._reconnectAttemptsLeft <= 0) { | ||
transport.disconnect(new SignalingConnectionError()); | ||
return; | ||
} | ||
reconnect(); | ||
} | ||
function reconnect() { | ||
if (transport.state === 'connected') { | ||
transport.preempt('syncing'); | ||
} | ||
transport._reconnectAttemptsLeft--; | ||
const { maxReconnectAttempts } = transport._options; | ||
const reconnectAttempts = maxReconnectAttempts - transport._reconnectAttemptsLeft; | ||
const backOffMs = (1 << reconnectAttempts) * transport._reconnectBackOffMs; | ||
setTimeout(startConnect, withJitter(backOffMs, transport._reconnectBackOffJitter)); | ||
} | ||
function resetReconnectAttemptsLeft() { | ||
const { maxReconnectAttempts } = transport._options; | ||
transport._reconnectAttemptsLeft = maxReconnectAttempts; | ||
} | ||
function startConnect() { | ||
if (transport.state === 'disconnected') { | ||
return; | ||
} | ||
const twilioConnection = createOrResetTwilioConnection(); | ||
twilioConnection.once('close', disconnect); | ||
twilioConnection.on('message', handleMessage); | ||
twilioConnection.once('open', connect); | ||
} | ||
function handleMessage(message) { | ||
@@ -401,2 +473,5 @@ switch (transport.state) { | ||
switch (message.type) { | ||
case 'error': | ||
transport.disconnect(createTwilioError(message.code, message.message)); | ||
return; | ||
case 'connected': | ||
@@ -407,2 +482,3 @@ case 'update': | ||
case 'synced': | ||
resetReconnectAttemptsLeft(); | ||
transport.emit('message', message); | ||
@@ -426,7 +502,2 @@ transport.preempt('connected'); | ||
const twilioConnection = transport._twilioConnection; | ||
twilioConnection.once('close', disconnect); | ||
twilioConnection.on('message', handleMessage); | ||
twilioConnection.once('open', connect); | ||
transport.on('stateChanged', function stateChanged(state) { | ||
@@ -443,3 +514,3 @@ switch (state) { | ||
case 'disconnected': | ||
twilioConnection.removeListener('message', handleMessage); | ||
transport._twilioConnection.removeListener('message', handleMessage); | ||
transport.removeListener('stateChanged', stateChanged); | ||
@@ -455,4 +526,6 @@ return; | ||
}); | ||
startConnect(); | ||
} | ||
module.exports = TwilioConnectionTransport; |
'use strict'; | ||
const StateMachine = require('./statemachine'); | ||
const { makeUUID } = require('./util'); | ||
const { buildLogLevels, makeUUID } = require('./util'); | ||
const Log = require('./util/log'); | ||
const Timeout = require('./util/timeout'); | ||
let nInstances = 0; | ||
/* | ||
@@ -27,3 +30,3 @@ TwilioConnection states | ||
const DEFAULT_MAX_CONSECUTIVE_MISSED_HEARTBEATS = 5; | ||
const DEFAULT_MAX_CONSECUTIVE_MISSED_HEARTBEATS = 3; | ||
const DEFAULT_MAX_REQUESTED_HEARTBEAT_TIMEOUT = 5000; | ||
@@ -57,2 +60,13 @@ const DEFAULT_WELCOME_TIMEOUT = 5000; | ||
options = Object.assign({ | ||
maxConsecutiveMissedHeartbeats: DEFAULT_MAX_CONSECUTIVE_MISSED_HEARTBEATS, | ||
requestedHeartbeatTimeout: DEFAULT_MAX_REQUESTED_HEARTBEAT_TIMEOUT, | ||
welcomeTimeout: DEFAULT_WELCOME_TIMEOUT, | ||
Log, | ||
WebSocket | ||
}, options); | ||
const logLevels = buildLogLevels(options.logLevel); | ||
const log = new options.Log('default', this, logLevels); | ||
Object.defineProperties(this, { | ||
@@ -67,2 +81,8 @@ _consecutiveHeartbeatsMissed: { | ||
}, | ||
_instanceId: { | ||
value: ++nInstances | ||
}, | ||
_log: { | ||
value: log | ||
}, | ||
_messageQueue: { | ||
@@ -72,8 +92,3 @@ value: [] | ||
_options: { | ||
value: Object.assign({ | ||
maxConsecutiveMissedHeartbeats: DEFAULT_MAX_CONSECUTIVE_MISSED_HEARTBEATS, | ||
requestedHeartbeatTimeout: DEFAULT_MAX_REQUESTED_HEARTBEAT_TIMEOUT, | ||
welcomeTimeout: DEFAULT_WELCOME_TIMEOUT, | ||
WebSocket | ||
}, options) | ||
value: options | ||
}, | ||
@@ -102,2 +117,6 @@ _sendHeartbeatTimeout: { | ||
toString() { | ||
return `[TwilioConnection #${this._instanceId}: ${this._ws.url}]`; | ||
} | ||
/** | ||
@@ -112,2 +131,40 @@ * The number of consecutive "hearbeat" messages missed. | ||
/** | ||
* Close the {@link TwilioConnection}. | ||
* @param {{code: number, reason: string}} event | ||
* @private | ||
*/ | ||
_close({ code, reason }) { | ||
if (this.state === 'closed') { | ||
return; | ||
} | ||
if (this._welcomeTimeout) { | ||
this._welcomeTimeout.clear(); | ||
} | ||
if (this._heartbeatTimeout) { | ||
this._heartbeatTimeout.clear(); | ||
} | ||
if (this._sendHeartbeatTimeout) { | ||
this._sendHeartbeatTimeout.clear(); | ||
} | ||
this._messageQueue.splice(0); | ||
const log = this._log; | ||
if (code === WS_CLOSE_NORMAL) { | ||
log.debug('Closed'); | ||
} else { | ||
log.warn(`Closed: ${code} - ${reason}`); | ||
} | ||
this.transition('closed', null, code !== WS_CLOSE_NORMAL | ||
? new Error(`WebSocket Error ${code}: ${reason}`) | ||
: null); | ||
const { readyState } = this._ws; | ||
const { WebSocket } = this._options; | ||
if (readyState !== WebSocket.CLOSING && readyState !== WebSocket.CLOSED) { | ||
this._ws.close(code, reason); | ||
} | ||
} | ||
/** | ||
* Connect to the TCMP server. | ||
@@ -119,21 +176,10 @@ * @param {string} serverUrl | ||
this._ws = new this._options.WebSocket(serverUrl); | ||
const log = this._log; | ||
const ws = this._ws; | ||
ws.addEventListener('close', event => { | ||
if (this._welcomeTimeout) { | ||
this._welcomeTimeout.clear(); | ||
} | ||
if (this._heartbeatTimeout) { | ||
this._heartbeatTimeout.clear(); | ||
} | ||
if (this._sendHeartbeatTimeout) { | ||
this._sendHeartbeatTimeout.clear(); | ||
} | ||
this._messageQueue.splice(0); | ||
this.transition('closed', null, event.code !== WS_CLOSE_NORMAL | ||
? new Error(`WebSocket Error ${event.code}: ${event.reason}`) | ||
: null); | ||
}); | ||
log.debug('Created a new WebSocket:', ws); | ||
ws.addEventListener('close', event => this._close(event)); | ||
ws.addEventListener('message', message => { | ||
log.debug(`Incoming: ${message.data}`); | ||
try { | ||
@@ -162,2 +208,3 @@ message = JSON.parse(message.data); | ||
default: | ||
this._log.debug(`Unknown message type: ${message.type}`); | ||
this.emit('error', new Error(`Unknown message type: ${message.type}`)); | ||
@@ -169,2 +216,3 @@ break; | ||
ws.addEventListener('open', () => { | ||
log.debug('WebSocket opened:', ws); | ||
this._sendHello(); | ||
@@ -182,6 +230,9 @@ const { welcomeTimeout } = this._options; | ||
_handleBad({ reason }) { | ||
const log = this._log; | ||
if (this.state === 'connecting') { | ||
this._ws.close(WS_CLOSE_HELLO_FAILED, reason); | ||
log.warn(`Closing: ${WS_CLOSE_HELLO_FAILED} - ${reason}`); | ||
this._close({ code: WS_CLOSE_HELLO_FAILED, reason }); | ||
return; | ||
} | ||
log.debug(`Error: ${reason}`); | ||
this.emit('error', new Error(reason)); | ||
@@ -211,4 +262,6 @@ } | ||
this._consecutiveHeartbeatsMissed++; | ||
const log = this._log; | ||
const { maxConsecutiveMissedHeartbeats } = this._options; | ||
log.debug(`Consecutive heartbeats missed: ${this._consecutiveHeartbeatsMissed}`); | ||
if (this._consecutiveHeartbeatsMissed < maxConsecutiveMissedHeartbeats) { | ||
@@ -218,4 +271,6 @@ this._heartbeatTimeout.reset(); | ||
} | ||
this._ws.close(WS_CLOSE_HEARTBEATS_MISSED, | ||
`Missed ${maxConsecutiveMissedHeartbeats} "heartbeat" messages`); | ||
const reason = `Missed ${maxConsecutiveMissedHeartbeats} "heartbeat" messages`; | ||
log.warn(`Closing: ${WS_CLOSE_HEARTBEATS_MISSED} - ${reason}`); | ||
this._close({ code: WS_CLOSE_HEARTBEATS_MISSED, reason }); | ||
} | ||
@@ -260,3 +315,5 @@ | ||
} | ||
this._ws.close(WS_CLOSE_WELCOME_TIMEOUT, '"welcome" message timeout expired'); | ||
const reason = '"welcome" message timeout expired'; | ||
this._log.warn(`Closing: ${WS_CLOSE_WELCOME_TIMEOUT} - ${reason}`); | ||
this._close({ code: WS_CLOSE_WELCOME_TIMEOUT, reason }); | ||
} | ||
@@ -270,3 +327,9 @@ | ||
_send(message) { | ||
this._ws.send(JSON.stringify(message)); | ||
const { readyState } = this._ws; | ||
const { WebSocket } = this._options; | ||
if (readyState === WebSocket.OPEN) { | ||
const data = JSON.stringify(message); | ||
this._log.debug(`Outgoing: ${data}`); | ||
this._ws.send(data); | ||
} | ||
} | ||
@@ -324,3 +387,3 @@ | ||
this._sendOrEnqueue({ type: 'bye' }); | ||
this._ws.close(); | ||
this._ws.close(WS_CLOSE_NORMAL); | ||
} | ||
@@ -364,2 +427,3 @@ | ||
* @typedef {object} TwilioConnectionOptions | ||
* @property {LogLevel} [logLevel=warn] - Log level of the {@link TwilioConnection} | ||
* @property {number} [maxConsecutiveMissedHeartbeats=5] - Max. number of consecutive "heartbeat" messages that can be missed | ||
@@ -366,0 +430,0 @@ * @property {number} [requestedHeartbeatTimeout=5000] - "heartbeat" timeout (ms) requested by the {@link TwilioConnection} |
@@ -618,2 +618,14 @@ /* globals mozRTCPeerConnection */ | ||
/** | ||
* Add random jitter to a given value in the range [-jitter, jitter]. | ||
* @private | ||
* @param {number} value | ||
* @param {number} jitter | ||
* @returns {number} value + random(-jitter, +jitter) | ||
*/ | ||
function withJitter(value, jitter) { | ||
const rand = Math.random(); | ||
return value - jitter + Math.floor(2 * jitter * rand + 0.5); | ||
} | ||
exports.constants = constants; | ||
@@ -648,1 +660,2 @@ exports.createMediaSignalingPayload = createMediaSignalingPayload; | ||
exports.valueToJSON = valueToJSON; | ||
exports.withJitter = withJitter; |
@@ -231,6 +231,7 @@ 'use strict'; | ||
* @param {string} sdp | ||
* @param {'planb' | 'unified'} sdpFormat | ||
* @param {Map<Track.ID, TrackAttributes>} trackIdsToAttributes | ||
* @returns {string} Updated SDP string | ||
*/ | ||
function setSimulcast(sdp, trackIdsToAttributes) { | ||
function setSimulcast(sdp, sdpFormat, trackIdsToAttributes) { | ||
const mediaSections = getMediaSections(sdp); | ||
@@ -249,3 +250,3 @@ const session = sdp.split('\r\nm=')[0]; | ||
return hasVP8PayloadType | ||
? setSimulcastInMediaSection(section, trackIdsToAttributes) | ||
? setSimulcastInMediaSection(section, sdpFormat, trackIdsToAttributes) | ||
: section; | ||
@@ -252,0 +253,0 @@ })).concat('').join('\r\n'); |
@@ -27,3 +27,3 @@ 'use strict'; | ||
* @param {Track.ID} trackId - The MediaStreamTrack ID | ||
* @param {string} streamId - The MediaStream ID | ||
* @param {MediaStreamID} streamId - The MediaStream ID | ||
* @param {string} cName - The MediaStream cname | ||
@@ -174,10 +174,44 @@ */ | ||
/** | ||
* Create SSRC attribute tuples. | ||
* @param {string} section | ||
* @param {'planb' | 'unified'} sdpFormat | ||
* @returns {Array<[SSRC, MediaStreamID, Track.ID]>} | ||
*/ | ||
function createSSRCAttributeTuples(section, sdpFormat) { | ||
return { | ||
planb: createPlanBSSRCAttributeTuples, | ||
unified: createUnifiedPlanSSRCAttributeTuples | ||
}[sdpFormat](section); | ||
} | ||
/** | ||
* Create "plan-b" SSRC attribute tuples. | ||
* @param {string} section | ||
* @returns {Array<[SSRC, MediaStreamID, Track.ID]>} | ||
*/ | ||
function createPlanBSSRCAttributeTuples(section) { | ||
return getMatches(section, '^a=ssrc:([0-9]+) msid:([^\\s]+) ([^\\s]+)$'); | ||
} | ||
/** | ||
* Create "unified-plan" SSRC attribute tuples. | ||
* @param {string} section | ||
* @returns {Array<[SSRC, MediaStreamID, Track.ID]>} | ||
*/ | ||
function createUnifiedPlanSSRCAttributeTuples(section) { | ||
const [streamId, trackId] = flatMap(getMatches(section, '^a=msid:(.+) (.+)$')); | ||
const ssrcs = flatMap(getMatches(section, '^a=ssrc:(.+) cname:.+$')); | ||
return ssrcs.map(ssrc => [ssrc, streamId, trackId]); | ||
} | ||
/** | ||
* Create a Map of MediaStreamTrack IDs and their {@link TrackAttributes}. | ||
* @param {string} section - SDP media section | ||
* @param {'planb' | 'unified'} sdpFormat | ||
* @returns {Map<Track.ID, TrackAttributes>} | ||
*/ | ||
function createTrackIdsToAttributes(section) { | ||
function createTrackIdsToAttributes(section, sdpFormat) { | ||
const simSSRCs = getSimulcastSSRCs(section); | ||
const ssrcAttrTuples = getMatches(section, '^a=ssrc:([0-9]+) msid:([^\\s]+) ([^\\s]+)$'); | ||
const rtxPairs = getSSRCRtxPairs(section); | ||
const ssrcAttrTuples = createSSRCAttributeTuples(section, sdpFormat); | ||
@@ -203,2 +237,3 @@ return ssrcAttrTuples.reduce((trackIdsToSSRCs, tuple) => { | ||
* @param {string} section - SDP media section | ||
* @param {'planb' | 'unified'} sdpFormat | ||
* @param {Map<Track.ID, TrackAttributes>} trackIdsToAttributes - Existing | ||
@@ -208,4 +243,4 @@ * map which will be updated for new MediaStreamTrack IDs | ||
*/ | ||
function setSimulcastInMediaSection(section, trackIdsToAttributes) { | ||
const newTrackIdsToAttributes = createTrackIdsToAttributes(section); | ||
function setSimulcastInMediaSection(section, sdpFormat, trackIdsToAttributes) { | ||
const newTrackIdsToAttributes = createTrackIdsToAttributes(section, sdpFormat); | ||
const newTrackIds = Array.from(newTrackIdsToAttributes.keys()); | ||
@@ -235,7 +270,18 @@ let trackIds = Array.from(trackIdsToAttributes.keys()); | ||
// "relevantSdpLines" are removed. | ||
const sectionLines = new Set(section.split('\r\n').concat(relevantSdpLines)); | ||
return flatMap(sectionLines).join('\r\n'); | ||
const sectionLines = flatMap(new Set(section.split('\r\n').concat(relevantSdpLines))); | ||
const xGoogleFlagConference = 'a=x-google-flag:conference'; | ||
if (!section.match(xGoogleFlagConference)) { | ||
sectionLines.push(xGoogleFlagConference); | ||
} | ||
return sectionLines.join('\r\n'); | ||
} | ||
/** | ||
* String representing a MediaStream ID. | ||
* @typedef {string} MediaStreamID | ||
*/ | ||
/** | ||
* String representing the SSRC of a MediaStreamTrack. | ||
@@ -242,0 +288,0 @@ * @typedef {string} SSRC |
@@ -5,3 +5,3 @@ { | ||
"description": "Twilio Video JavaScript library", | ||
"version": "2.0.0-beta2", | ||
"version": "2.0.0-beta3", | ||
"homepage": "https://twilio.com", | ||
@@ -120,3 +120,3 @@ "author": "Mark Andrus Roberts <mroberts@twilio.com>", | ||
"@twilio/sip.js": "^0.7.7", | ||
"@twilio/webrtc": "^3.0.0", | ||
"@twilio/webrtc": "^3.1.0", | ||
"ws": "^3.3.1", | ||
@@ -123,0 +123,0 @@ "xmlhttprequest": "^1.8.0" |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3347582
67362
Updated@twilio/webrtc@^3.1.0