twilio-video
Version:
Twilio Video JavaScript Library
589 lines (587 loc) • 22.2 kB
JavaScript
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var StateMachine = require('../../statemachine');
var TwilioConnection = require('../../twilioconnection');
var DefaultBackoff = require('../../util/backoff');
var reconnectBackoffConfig = require('../../util/constants').reconnectBackoffConfig;
var Timeout = require('../../util/timeout');
var _a = require('../../util/constants'), SDK_NAME = _a.SDK_NAME, SDK_VERSION = _a.SDK_VERSION, SDP_FORMAT = _a.SDP_FORMAT;
var _b = require('../../util'), createBandwidthProfilePayload = _b.createBandwidthProfilePayload, createMediaSignalingPayload = _b.createMediaSignalingPayload, createMediaWarningsPayload = _b.createMediaWarningsPayload, createSubscribePayload = _b.createSubscribePayload, getUserAgent = _b.getUserAgent, isNonArrayObject = _b.isNonArrayObject;
var _c = require('../../util/twilio-video-errors'), createTwilioError = _c.createTwilioError, RoomCompletedError = _c.RoomCompletedError, SignalingConnectionError = _c.SignalingConnectionError, SignalingServerBusyError = _c.SignalingServerBusyError;
var ICE_VERSION = 1;
var RSP_VERSION = 2;
/*
TwilioConnectionTransport States
----------------
+-----------+
| |
| syncing |---------+
| | |
+-----------+ |
^ | |
| | |
| v v
+------------+ +-----------+ +--------------+
| | | | | |
| connecting |--->| connected |--->| disconnected |
| | | | | |
+------------+ +-----------+ +--------------+
| ^
| |
| |
+------------------------------+
*/
var states = {
connecting: [
'connected',
'disconnected'
],
connected: [
'disconnected',
'syncing'
],
syncing: [
'connected',
'disconnected'
],
disconnected: []
};
/**
* A {@link TwilioConnectionTransport} supports sending and receiving Room Signaling Protocol
* (RSP) messages. It also supports RSP requests, such as Sync and Disconnect.
* @extends StateMachine
* @emits TwilioConnectionTransport#connected
* @emits TwilioConnectionTransport#message
*/
var TwilioConnectionTransport = /** @class */ (function (_super) {
__extends(TwilioConnectionTransport, _super);
/**
* Construct a {@link TwilioConnectionTransport}.
* @param {?string} name
* @param {string} accessToken
* @param {ParticipantSignaling} localParticipant
* @param {PeerConnectionManager} peerConnectionManager
* @param {string} wsServer
* @param {object} [options]
*/
function TwilioConnectionTransport(name, accessToken, localParticipant, peerConnectionManager, wsServer, options) {
var _this = this;
options = Object.assign({
Backoff: DefaultBackoff,
TwilioConnection: TwilioConnection,
iceServers: null,
trackPriority: true,
trackSwitchOff: true,
renderHints: true,
userAgent: getUserAgent()
}, options);
_this = _super.call(this, 'connecting', states) || this;
Object.defineProperties(_this, {
_accessToken: {
value: accessToken
},
_adaptiveSimulcast: {
value: options.adaptiveSimulcast
},
_automaticSubscription: {
value: options.automaticSubscription
},
_bandwidthProfile: {
value: options.bandwidthProfile
},
_dominantSpeaker: {
value: options.dominantSpeaker
},
_receiveTranscriptions: {
value: options.receiveTranscriptions
},
_eventObserver: {
value: options.eventObserver,
writable: false
},
_renderHints: {
value: options.renderHints
},
_iceServersStatus: {
value: Array.isArray(options.iceServers)
? 'overrode'
: 'acquire'
},
_localParticipant: {
value: localParticipant
},
_name: {
value: name,
},
_networkQuality: {
value: isNonArrayObject(options.networkQuality) || options.networkQuality
},
_notifyWarnings: {
value: options.notifyWarnings
},
_options: {
value: options
},
_peerConnectionManager: {
value: peerConnectionManager
},
_sessionTimer: {
value: null,
writable: true
},
_sessionTimeoutMS: {
value: 0,
writable: true
},
_reconnectBackoff: {
value: new options.Backoff(reconnectBackoffConfig)
},
_session: {
value: null,
writable: true
},
_trackPriority: {
value: options.trackPriority
},
_trackSwitchOff: {
value: options.trackSwitchOff
},
_twilioConnection: {
value: null,
writable: true
},
_updatesReceived: {
value: []
},
_updatesToSend: {
value: []
},
_userAgent: {
value: options.userAgent
},
_wsServer: {
value: wsServer
}
});
setupTransport(_this);
return _this;
}
/**
* Create a Connect, Sync or Disconnect RSP message.
* @private
* @returns {?object}
*/
TwilioConnectionTransport.prototype._createConnectOrSyncOrDisconnectMessage = function () {
if (this.state === 'connected') {
return null;
}
if (this.state === 'disconnected') {
return {
session: this._session,
type: 'disconnect',
version: RSP_VERSION
};
}
var type = {
connecting: 'connect',
syncing: 'sync'
}[this.state];
var message = {
name: this._name,
participant: this._localParticipant.getState(),
peer_connections: this._peerConnectionManager.getStates(),
type: type,
version: RSP_VERSION
};
if (message.type === 'connect') {
message.ice_servers = this._iceServersStatus;
message.publisher = {
name: SDK_NAME,
sdk_version: SDK_VERSION,
user_agent: this._userAgent
};
if (this._bandwidthProfile) {
message.bandwidth_profile = createBandwidthProfilePayload(this._bandwidthProfile);
}
if (this._notifyWarnings) {
message.participant.media_warnings = createMediaWarningsPayload(this._notifyWarnings);
}
message.media_signaling = createMediaSignalingPayload(this._dominantSpeaker, this._networkQuality, this._trackPriority, this._trackSwitchOff, this._adaptiveSimulcast, this._renderHints, this._receiveTranscriptions);
message.subscribe = createSubscribePayload(this._automaticSubscription);
message.format = SDP_FORMAT;
message.token = this._accessToken;
}
else if (message.type === 'sync') {
message.session = this._session;
message.token = this._accessToken;
}
else if (message.type === 'update') {
message.session = this._session;
}
return message;
};
/**
* Create an "ice" message.
* @private
*/
TwilioConnectionTransport.prototype._createIceMessage = function () {
return {
edge: 'roaming',
token: this._accessToken,
type: 'ice',
version: ICE_VERSION
};
};
/**
* Send a Connect, Sync or Disconnect RSP message.
* @private
*/
TwilioConnectionTransport.prototype._sendConnectOrSyncOrDisconnectMessage = function () {
var message = this._createConnectOrSyncOrDisconnectMessage();
if (message) {
this._twilioConnection.sendMessage(message);
}
};
/**
* Disconnect the {@link TwilioConnectionTransport}. Returns true if calling the method resulted
* in disconnection.
* @param {TwilioError} [error]
* @returns {boolean}
*/
TwilioConnectionTransport.prototype.disconnect = function (error) {
if (this.state !== 'disconnected') {
this.preempt('disconnected', null, [error]);
this._sendConnectOrSyncOrDisconnectMessage();
this._twilioConnection.close();
return true;
}
return false;
};
/**
* Publish an RSP Update. Returns true if calling the method resulted in
* publishing (or eventually publishing) the update.
* @param {object} update
* @returns {boolean}
*/
TwilioConnectionTransport.prototype.publish = function (update) {
switch (this.state) {
case 'connected':
this._twilioConnection.sendMessage(Object.assign({
session: this._session,
type: 'update',
version: RSP_VERSION
}, update));
return true;
case 'connecting':
case 'syncing':
this._updatesToSend.push(update);
return true;
case 'disconnected':
default:
return false;
}
};
/**
* Publish (or queue) an event to the Insights gateway.
* @param {string} group - Event group name
* @param {string} name - Event name
* @param {string} level - Event level
* @param {object} payload - Event payload
* @returns {void}
*/
TwilioConnectionTransport.prototype.publishEvent = function (group, name, level, payload) {
this._eventObserver.emit('event', { group: group, name: name, level: level, payload: payload });
};
/**
* Sync the {@link TwilioConnectionTransport}. Returns true if calling the method resulted in
* syncing.
* @returns {boolean}
*/
TwilioConnectionTransport.prototype.sync = function () {
if (this.state === 'connected') {
this.preempt('syncing');
this._sendConnectOrSyncOrDisconnectMessage();
return true;
}
return false;
};
/**
* @private
* @returns {void}
*/
TwilioConnectionTransport.prototype._setSession = function (session, sessionTimeout) {
this._session = session;
this._sessionTimeoutMS = sessionTimeout * 1000;
};
/**
* Determines if we should attempt reconnect.
* returns a Promise to wait on before attempting to
* reconnect. returns null if its not okay to reconnect.
* @private
* @returns {Promise<void>}
*/
TwilioConnectionTransport.prototype._getReconnectTimer = function () {
var _this = this;
if (this._sessionTimeoutMS === 0) {
// this means either we have never connected.
// or we timed out while trying to reconnect
// In either case we do not want to reconnect.
return null;
}
// start session timer
if (!this._sessionTimer) {
this._sessionTimer = new Timeout(function () {
// ensure that _clearReconnectTimer wasn't
// called while we were waiting.
if (_this._sessionTimer) {
// do not allow any more reconnect attempts.
_this._sessionTimeoutMS = 0;
}
}, this._sessionTimeoutMS);
}
// return promise that waits with exponential backoff.
return new Promise(function (resolve) {
_this._reconnectBackoff.backoff(resolve);
});
};
/**
* clears the session reconnect timer.
*
* @private
* @returns {void}
*/
TwilioConnectionTransport.prototype._clearReconnectTimer = function () {
this._reconnectBackoff.reset();
if (this._sessionTimer) {
this._sessionTimer.clear();
this._sessionTimer = null;
}
};
return TwilioConnectionTransport;
}(StateMachine));
/**
* @event TwilioConnectionTransport#connected
* @param {object} initialState
*/
/**
* @event TwilioConnectionTransport#message
* @param {object} peerConnections
*/
function reducePeerConnections(peerConnections) {
return Array.from(peerConnections.reduce(function (peerConnectionsById, update) {
var reduced = peerConnectionsById.get(update.id) || update;
// First, reduce the top-level `description` property.
if (!reduced.description && update.description) {
reduced.description = update.description;
}
else if (reduced.description && update.description) {
if (update.description.revision > reduced.description.revision) {
reduced.description = update.description;
}
}
// Then, reduce the top-level `ice` property.
if (!reduced.ice && update.ice) {
reduced.ice = update.ice;
}
else if (reduced.ice && update.ice) {
if (update.ice.revision > reduced.ice.revision) {
reduced.ice = update.ice;
}
}
// Finally, update the map.
peerConnectionsById.set(reduced.id, reduced);
return peerConnectionsById;
}, new Map()).values());
}
function reduceUpdates(updates) {
return updates.reduce(function (reduced, update) {
// First, reduce the top-level `participant` property.
if (!reduced.participant && update.participant) {
reduced.participant = update.participant;
}
else if (reduced.participant && update.participant) {
if (update.participant.revision > reduced.participant.revision) {
reduced.participant = update.participant;
}
}
// Then, reduce the top-level `peer_connections` property.
/* eslint camelcase:0 */
if (!reduced.peer_connections && update.peer_connections) {
reduced.peer_connections = reducePeerConnections(update.peer_connections);
}
else if (reduced.peer_connections && update.peer_connections) {
reduced.peer_connections = reducePeerConnections(reduced.peer_connections.concat(update.peer_connections));
}
return reduced;
}, {});
}
function setupTransport(transport) {
function createOrResetTwilioConnection() {
if (transport.state === 'disconnected') {
return;
}
if (transport._twilioConnection) {
transport._twilioConnection.removeListener('message', handleMessage);
}
var _iceServersStatus = transport._iceServersStatus, _options = transport._options, _wsServer = transport._wsServer, state = transport.state;
var TwilioConnection = _options.TwilioConnection;
var twilioConnection = new TwilioConnection(_wsServer, Object.assign({
helloBody: state === 'connecting' && _iceServersStatus === 'acquire'
? transport._createIceMessage()
: transport._createConnectOrSyncOrDisconnectMessage()
}, _options));
twilioConnection.once('close', function (reason) {
if (reason === TwilioConnection.CloseReason.LOCAL) {
disconnect();
}
else {
disconnect(new Error(reason));
}
});
twilioConnection.on('message', handleMessage);
transport._twilioConnection = twilioConnection;
}
function disconnect(error) {
if (transport.state === 'disconnected') {
return;
}
if (!error) {
transport.disconnect();
return;
}
var reconnectTimer = transport._getReconnectTimer();
if (!reconnectTimer) {
var twilioError = error.message === TwilioConnection.CloseReason.BUSY
? new SignalingServerBusyError()
: new SignalingConnectionError();
transport.disconnect(twilioError);
return;
}
if (transport.state === 'connected') {
transport.preempt('syncing');
}
reconnectTimer.then(createOrResetTwilioConnection);
}
function handleMessage(message) {
if (transport.state === 'disconnected') {
return;
}
if (message.type === 'error') {
transport.disconnect(createTwilioError(message.code, message.message));
return;
}
switch (transport.state) {
case 'connected':
switch (message.type) {
case 'connected':
case 'synced':
case 'update':
case 'warning':
transport.emit('message', message);
return;
case 'disconnected':
transport.disconnect(message.status === 'completed'
? new RoomCompletedError()
: null);
return;
default:
// Do nothing.
return;
}
case 'connecting':
switch (message.type) {
case 'iced':
transport._options.onIced(message.ice_servers).then(function () {
transport._sendConnectOrSyncOrDisconnectMessage();
});
return;
case 'connected':
transport._setSession(message.session, message.options.session_timeout);
transport.emit('connected', message);
transport.preempt('connected');
return;
case 'synced':
case 'update':
transport._updatesReceived.push(message);
return;
case 'disconnected':
transport.disconnect(message.status === 'completed'
? new RoomCompletedError()
: null);
return;
default:
// Do nothing.
return;
}
case 'syncing':
switch (message.type) {
case 'connected':
case 'update':
transport._updatesReceived.push(message);
return;
case 'synced':
transport._clearReconnectTimer();
transport.emit('message', message);
transport.preempt('connected');
return;
case 'disconnected':
transport.disconnect(message.status === 'completed'
? new RoomCompletedError()
: null);
return;
default:
// Do nothing.
return;
}
default:
// Impossible
return;
}
}
transport.on('stateChanged', function stateChanged(state) {
switch (state) {
case 'connected': {
var updates = transport._updatesToSend.splice(0);
if (updates.length) {
transport.publish(reduceUpdates(updates));
}
transport._updatesReceived.splice(0).forEach(function (update) { return transport.emit('message', update); });
return;
}
case 'disconnected':
transport._twilioConnection.removeListener('message', handleMessage);
transport.removeListener('stateChanged', stateChanged);
return;
case 'syncing':
// Do nothing.
return;
default:
// Impossible
return;
}
});
var _options = transport._options, _iceServersStatus = transport._iceServersStatus;
var iceServers = _options.iceServers, onIced = _options.onIced;
if (_iceServersStatus === 'overrode') {
onIced(iceServers).then(createOrResetTwilioConnection);
}
else {
createOrResetTwilioConnection();
}
}
module.exports = TwilioConnectionTransport;
//# sourceMappingURL=twilioconnectiontransport.js.map