UNPKG

twilio-video

Version:

Twilio Video JavaScript Library

298 lines (258 loc) 8.23 kB
'use strict'; const DefaultRecordingSignaling = require('./recording'); const StateMachine = require('../statemachine'); const DefaultTimeout = require('../util/timeout'); const { buildLogLevels } = require('../util'); const { DEFAULT_LOG_LEVEL } = require('../util/constants'); const Log = require('../util/log'); const { MediaConnectionError, MediaDTLSTransportFailedError, SignalingConnectionDisconnectedError } = require('../util/twilio-video-errors'); let nInstances = 0; /* RoomSignaling States ----------------------- +-----------+ +--------------+ | | | | | connected |---->| disconnected | | | | | +-----------+ +--------------+ | ^ ^ | | | | | +--------------+ | +---| | | | reconnecting | +----->| | +--------------+ */ const states = { connected: [ 'reconnecting', 'disconnected' ], reconnecting: [ 'connected', 'disconnected' ], disconnected: [] }; /** * A {@link Room} implementation * @extends StateMachine * @property {RTCPeerConnectionState} connectionState * @property {?Participant.SID} dominantSpeakerSid * @property {ParticipantSignaling} localParticipant * @property {RTCIceConnectionState} iceConnectionState * @property {string} name * @property {Map<string, RemoteParticipantSignaling>} participants * @property {RecordingSignaling} recording * @property {Room.SID} sid * @property {string} state - "connected", "reconnecting", or "disconnected" * @property {string} signalingConnectionState - "connected", * "reconnecting", or "disconnected" * @emits RoomSignaling#connectionStateChanged * @emits RoomSignaling#dominantSpeakerChanged * @emits RoomSignaling#iceConnectionStateChanged * @emits RoomSignaling#signalingConnectionStateChanged */ class RoomSignaling extends StateMachine { /** * Construct a {@link RoomSignaling}. * @param {ParticipantSignaling} localParticipant * @param {Room.SID} sid * @param {string} name * @param {object} options */ constructor(localParticipant, sid, name, options) { options = Object.assign({ logLevel: DEFAULT_LOG_LEVEL, RecordingSignaling: DefaultRecordingSignaling, Timeout: DefaultTimeout }, options); const logLevels = buildLogLevels(options.logLevel); super('connected', states); const RecordingSignaling = options.RecordingSignaling; const sessionTimeout = new options.Timeout(() => { this._disconnect(this._reconnectingError); }, options.sessionTimeout, false); Object.defineProperties(this, { _instanceId: { value: nInstances++ }, _log: { value: options.log ? options.log.createLog('default', this) : new Log('default', this, logLevels, options.loggerName) }, _mediaConnectionIsReconnecting: { writable: true, value: false }, _options: { value: options }, _reconnectingError: { value: null, writable: true }, _sessionTimeout: { value: sessionTimeout }, dominantSpeakerSid: { enumerable: true, value: null, writable: true }, localParticipant: { enumerable: true, value: localParticipant }, name: { enumerable: true, value: name }, participants: { enumerable: true, value: new Map() }, recording: { enumerable: true, value: new RecordingSignaling() }, sid: { enumerable: true, value: sid } }); this.on('connectionStateChanged', () => { if (this.connectionState === 'failed' && !['disconnected', 'failed'].includes(this.iceConnectionState)) { this._disconnect(new MediaDTLSTransportFailedError()); } }); this.on('iceConnectionStateChanged', () => maybeUpdateState(this)); this.on('signalingConnectionStateChanged', () => maybeUpdateState(this)); // NOTE(mmalavalli): In case "iceConnectionState" is already failed, update // the RoomSignaling state. setTimeout() ensures that the state is updated // after RoomV2's constructor is fully executed, thereby making "signalingConnectionState" // available here. setTimeout(() => maybeUpdateState(this)); } /** * Disconnect, possibly with an Error. * @private * @param {Error} [error] * @returns {boolean} */ _disconnect(error) { if (this.state !== 'disconnected') { this.preempt('disconnected', null, [error]); return true; } return false; } toString() { return `[RoomSignaling #${this._instanceId}: ${this.localParticipant ? this.localParticipant.sid : 'null'}]`; } /** * Connect {@link RemoteParticipantSignaling} to the {@link RoomSignaling}. * @param {RemoteParticipantSignaling} participant * @returns {boolean} */ connectParticipant(participant) { const self = this; if (participant.state === 'disconnected') { return false; } if (this.participants.has(participant.sid)) { return false; } this.participants.set(participant.sid, participant); participant.on('stateChanged', function stateChanged(state) { if (state === 'disconnected') { participant.removeListener('stateChanged', stateChanged); self.participants.delete(participant.sid); self.emit('participantDisconnected', participant); } }); this.emit('participantConnected', participant); return true; } /** * Disconnect. * @returns {boolean} */ disconnect() { return this._disconnect(); } /** * Set (or unset) the Dominant Speaker. * @param {?Participant.SID} dominantSpeakerSid * @returns {void} */ setDominantSpeaker(dominantSpeakerSid) { this.dominantSpeakerSid = dominantSpeakerSid; this.emit('dominantSpeakerChanged'); } } /** * @event RoomSignaling#event:connectionStateChanged */ /** * @event RoomSignaling#event:dominantSpeakerChanged */ /** * {@link RemoteParticipantSignaling} connected to the {@link RoomSignaling}. * @event RoomSignaling#event:participantConnected * @param {RemoteParticipantSignaling} participantSignaling */ /** * {@link RemoteParticipantSignaling} disconnected from the {@link RoomSignaling}. * @event RoomSignaling#event:participantDisconnected * @param {RemoteParticipantSignaling} participantSignaling */ /** * @event RoomSignaling#event:iceConnectionStateChanged */ /** * @event RoomSignaling#event:signalingConnectionStateChanged */ /** * Maybe update the {@link RoomSignaling} state. * @param {RoomSignaling} roomSignaling */ function maybeUpdateState(roomSignaling) { if (roomSignaling.state === 'disconnected' || roomSignaling.signalingConnectionState === 'disconnected') { roomSignaling._sessionTimeout.clear(); return; } let newState; if (roomSignaling.signalingConnectionState === 'reconnecting') { newState = roomSignaling.signalingConnectionState; } else if (roomSignaling.iceConnectionState === 'failed') { roomSignaling._mediaConnectionIsReconnecting = true; newState = 'reconnecting'; } else if (roomSignaling.iceConnectionState === 'new' || roomSignaling.iceConnectionState === 'checking') { newState = roomSignaling._mediaConnectionIsReconnecting ? 'reconnecting' : 'connected'; } else { roomSignaling._mediaConnectionIsReconnecting = false; roomSignaling._reconnectingError = null; roomSignaling._sessionTimeout.clear(); newState = 'connected'; } if (newState === roomSignaling.state) { return; } if (newState === 'reconnecting') { roomSignaling._reconnectingError = roomSignaling.signalingConnectionState === 'reconnecting' ? new SignalingConnectionDisconnectedError() : new MediaConnectionError(); roomSignaling._sessionTimeout.start(); roomSignaling.preempt(newState, null, [roomSignaling._reconnectingError]); } else { roomSignaling.preempt(newState); } } module.exports = RoomSignaling;