/**
* The list of <a href="#method_joinRoom"><code>joinRoom()</code> method</a> socket connection failure states.
* @attribute SOCKET_ERROR
* @param {Number} CONNECTION_FAILED <small>Value <code>0</code></small>
* The value of the failure state when <code>joinRoom()</code> socket connection failed to establish with
* the Signaling server at the first attempt.
* @param {Number} RECONNECTION_FAILED <small>Value <code>-1</code></small>
* The value of the failure state when <code>joinRoom()</code> socket connection failed to establish
* the Signaling server after the first attempt.
* @param {Number} CONNECTION_ABORTED <small>Value <code>-2</code></small>
* The value of the failure state when <code>joinRoom()</code> socket connection will not attempt
* to reconnect after the failure of the first attempt in <code>CONNECTION_FAILED</code> as there
* are no more ports or transports to attempt for reconnection.
* @param {Number} RECONNECTION_ABORTED <small>Value <code>-3</code></small>
* The value of the failure state when <code>joinRoom()</code> socket connection will not attempt
* to reconnect after the failure of several attempts in <code>RECONNECTION_FAILED</code> as there
* are no more ports or transports to attempt for reconnection.
* @param {Number} RECONNECTION_ATTEMPT <small>Value <code>-4</code></small>
* The value of the failure state when <code>joinRoom()</code> socket connection is attempting
* to reconnect with a new port or transport after the failure of attempts in
* <code>CONNECTION_FAILED</code> or <code>RECONNECTED_FAILED</code>.
* @type JSON
* @readOnly
* @for Skylink
* @since 0.5.6
*/
Skylink.prototype.SOCKET_ERROR = {
CONNECTION_FAILED: 0,
RECONNECTION_FAILED: -1,
CONNECTION_ABORTED: -2,
RECONNECTION_ABORTED: -3,
RECONNECTION_ATTEMPT: -4
};
/**
* The list of <a href="#method_joinRoom"><code>joinRoom()</code> method</a> socket connection reconnection states.
* @attribute SOCKET_FALLBACK
* @param {String} NON_FALLBACK <small>Value <code>"nonfallback"</code></small>
* The value of the reconnection state when <code>joinRoom()</code> socket connection is at its initial state
* without transitioning to any new socket port or transports yet.
* @param {String} FALLBACK_PORT <small>Value <code>"fallbackPortNonSSL"</code></small>
* The value of the reconnection state when <code>joinRoom()</code> socket connection is reconnecting with
* another new HTTP port using WebSocket transports to attempt to establish connection with Signaling server.
* @param {String} FALLBACK_PORT_SSL <small>Value <code>"fallbackPortSSL"</code></small>
* The value of the reconnection state when <code>joinRoom()</code> socket connection is reconnecting with
* another new HTTPS port using WebSocket transports to attempt to establish connection with Signaling server.
* @param {String} LONG_POLLING <small>Value <code>"fallbackLongPollingNonSSL"</code></small>
* The value of the reconnection state when <code>joinRoom()</code> socket connection is reconnecting with
* another new HTTP port using Polling transports to attempt to establish connection with Signaling server.
* @param {String} LONG_POLLING <small>Value <code>"fallbackLongPollingSSL"</code></small>
* The value of the reconnection state when <code>joinRoom()</code> socket connection is reconnecting with
* another new HTTPS port using Polling transports to attempt to establish connection with Signaling server.
* @type JSON
* @readOnly
* @for Skylink
* @since 0.5.6
*/
Skylink.prototype.SOCKET_FALLBACK = {
NON_FALLBACK: 'nonfallback',
FALLBACK_PORT: 'fallbackPortNonSSL',
FALLBACK_SSL_PORT: 'fallbackPortSSL',
LONG_POLLING: 'fallbackLongPollingNonSSL',
LONG_POLLING_SSL: 'fallbackLongPollingSSL'
};
/**
* Function that sends a socket message over the socket connection to the Signaling.
* @method _sendChannelMessage
* @private
* @for Skylink
* @since 0.5.8
*/
Skylink.prototype._sendChannelMessage = function(message) {
var self = this;
var interval = 1000;
var throughput = 16;
if (!self._channelOpen || !self._user || !self._socket) {
log.warn([null, 'Socket', null, 'Dropping of message as Socket connection is not opened or is at ' +
'incorrect step ->'], message);
return;
}
if (self._user.sid && !self._peerMessagesStamps[self._user.sid]) {
self._peerMessagesStamps[self._user.sid] = {
userData: 0,
audioMuted: 0,
videoMuted: 0
};
}
var checkStampFn = function (statusMessage) {
if (statusMessage.type === self._SIG_MESSAGE_TYPE.UPDATE_USER) {
if (!self._user.sid) {
return false;
}
return statusMessage.stamp > self._peerMessagesStamps[self._user.sid].userData;
} else if (statusMessage.type === self._SIG_MESSAGE_TYPE.MUTE_VIDEO) {
if (!self._user.sid) {
return false;
}
return statusMessage.stamp > self._peerMessagesStamps[self._user.sid].videoMuted;
} else if (statusMessage.type === self._SIG_MESSAGE_TYPE.MUTE_AUDIO) {
if (!self._user.sid) {
return false;
}
return statusMessage.stamp > self._peerMessagesStamps[self._user.sid].audioMuted;
}
return true;
};
var setStampFn = function (statusMessage) {
if (statusMessage.type === self._SIG_MESSAGE_TYPE.UPDATE_USER) {
self._peerMessagesStamps[self._user.sid].userData = statusMessage.stamp;
} else if (statusMessage.type === self._SIG_MESSAGE_TYPE.MUTE_VIDEO) {
self._peerMessagesStamps[self._user.sid].videoMuted = statusMessage.stamp;
} else if (statusMessage.type === self._SIG_MESSAGE_TYPE.MUTE_AUDIO) {
self._peerMessagesStamps[self._user.sid].audioMuted = statusMessage.stamp;
}
};
var setQueueFn = function () {
log.debug([null, 'Socket', null, 'Starting queue timeout']);
self._socketMessageTimeout = setTimeout(function () {
if (((new Date ()).getTime() - self._timestamp.socketMessage) <= interval) {
log.debug([null, 'Socket', null, 'Restarting queue timeout']);
setQueueFn();
return;
}
startSendingQueuedMessageFn();
}, interval - ((new Date ()).getTime() - self._timestamp.socketMessage));
};
var triggerEventFn = function (eventMessage) {
if (eventMessage.type === self._SIG_MESSAGE_TYPE.PUBLIC_MESSAGE) {
self._trigger('incomingMessage', {
content: eventMessage.data,
isPrivate: false,
targetPeerId: null,
listOfPeers: Object.keys(self._peerInformations),
isDataChannel: false,
senderPeerId: self._user.sid
}, self._user.sid, self.getPeerInfo(), true);
} else if (eventMessage.type === self._SIG_MESSAGE_TYPE.ROOM_LOCK) {
self._roomLocked = !!eventMessage.lock;
self._trigger('roomLock', !!eventMessage.lock, self._user.sid, self.getPeerInfo(), true);
}
};
var sendGroupMessageFn = function (groupMessageList) {
self._socketMessageTimeout = null;
if (!self._channelOpen || !(self._user && self._user.sid) || !self._socket) {
log.warn([null, 'Socket', null, 'Dropping of group messages as Socket connection is not opened or is at ' +
'incorrect step ->'], groupMessageList);
return;
}
var strGroupMessageList = [];
var stamps = {
userData: 0,
audioMuted: 0,
videoMuted: 0
};
for (var k = 0; k < groupMessageList.length; k++) {
if (checkStampFn(groupMessageList[k])) {
if (groupMessageList[k].type === self._SIG_MESSAGE_TYPE.UPDATE_USER &&
groupMessageList[k].stamp > self._peerMessagesStamps[self._user.sid].userData &&
groupMessageList[k].stamp > stamps.userData) {
stamps.userData = groupMessageList[k].stamp;
} else if (groupMessageList[k].type === self._SIG_MESSAGE_TYPE.MUTE_AUDIO &&
groupMessageList[k].stamp > self._peerMessagesStamps[self._user.sid].audioMuted &&
groupMessageList[k].stamp > stamps.audioMuted) {
stamps.audioMuted = groupMessageList[k].stamp;
} else if (groupMessageList[k].type === self._SIG_MESSAGE_TYPE.MUTE_VIDEO &&
groupMessageList[k].stamp > self._peerMessagesStamps[self._user.sid].videoMuted &&
groupMessageList[k].stamp > stamps.videoMuted) {
stamps.videoMuted = groupMessageList[k].stamp;
}
}
}
for (var i = 0; i < groupMessageList.length; i++) {
if ((groupMessageList[i].type === self._SIG_MESSAGE_TYPE.UPDATE_USER &&
groupMessageList[i].stamp < stamps.userData) ||
(groupMessageList[i].type === self._SIG_MESSAGE_TYPE.MUTE_AUDIO &&
groupMessageList[i].stamp < stamps.audioMuted) ||
(groupMessageList[i].type === self._SIG_MESSAGE_TYPE.MUTE_VIDEO &&
groupMessageList[i].stamp < stamps.videoMuted)) {
log.warn([null, 'Socket', null, 'Dropping of outdated status message ->'], clone(groupMessageList[i]));
groupMessageList.splice(i, 1);
i--;
continue;
}
strGroupMessageList.push(JSON.stringify(groupMessageList[i]));
}
if (strGroupMessageList.length > 0) {
var groupMessage = {
type: self._SIG_MESSAGE_TYPE.GROUP,
lists: strGroupMessageList,
mid: self._user.sid,
rid: self._room.id
};
log.debug([null, 'Socket', null, 'Sending queued messages (max: 16 per group) ->'], groupMessage);
self._socket.send(JSON.stringify(groupMessage));
self._timestamp.socketMessage = (new Date()).getTime();
for (var j = 0; j < groupMessageList.length; j++) {
setStampFn(groupMessageList[j]);
triggerEventFn(groupMessageList[j]);
}
}
};
var startSendingQueuedMessageFn = function(){
if (self._socketMessageQueue.length > 0){
if (self._socketMessageQueue.length < throughput){
sendGroupMessageFn(self._socketMessageQueue.splice(0, self._socketMessageQueue.length));
} else {
sendGroupMessageFn(self._socketMessageQueue.splice(0, throughput));
setQueueFn();
}
}
};
if (self._groupMessageList.indexOf(message.type) > -1) {
if (!(self._timestamp.socketMessage && ((new Date ()).getTime() - self._timestamp.socketMessage) <= interval)) {
if (!checkStampFn(message)) {
log.warn([null, 'Socket', null, 'Dropping of outdated status message ->'], message);
return;
}
if (self._socketMessageTimeout) {
clearTimeout(self._socketMessageTimeout);
}
self._socket.send(JSON.stringify(message));
setStampFn(message);
triggerEventFn(message);
self._timestamp.socketMessage = (new Date()).getTime();
} else {
log.warn([null, 'Socket', null, 'Queueing socket message to prevent message drop ->'], message);
self._socketMessageQueue.push(message);
if (!self._socketMessageTimeout) {
setQueueFn();
}
}
} else {
self._socket.send(JSON.stringify(message));
}
};
/**
* Function that creates and opens a socket connection to the Signaling.
* @method _createSocket
* @private
* @for Skylink
* @since 0.5.10
*/
Skylink.prototype._createSocket = function (type) {
var self = this;
var options = {
forceNew: true,
//'sync disconnect on unload' : true,
reconnection: false,
timeout: self._socketTimeout
};
var ports = self._socketPorts[self._signalingServerProtocol];
var connectionType = null;
// just beginning
if (self._signalingServerPort === null) {
self._signalingServerPort = ports[0];
connectionType = self.SOCKET_FALLBACK.NON_FALLBACK;
// reached the end of the last port for the protocol type
} else if ( ports.indexOf(self._signalingServerPort) === ports.length - 1 ) {
// re-refresh to long-polling port
if (type === 'WebSocket') {
type = 'Polling';
self._signalingServerPort = ports[0];
} else if (type === 'Polling') {
options.reconnection = true;
options.reconnectionAttempts = 4;
options.reconectionDelayMax = 1000;
}
// move to the next port
} else {
self._signalingServerPort = ports[ ports.indexOf(self._signalingServerPort) + 1 ];
}
var url = self._signalingServerProtocol + '//' + self._signalingServer + ':' + self._signalingServerPort;
//'http://ec2-52-8-93-170.us-west-1.compute.amazonaws.com:6001';
if (type === 'WebSocket') {
options.transports = ['websocket'];
} else if (type === 'Polling') {
options.transports = ['xhr-polling', 'jsonp-polling', 'polling'];
}
// if socket instance already exists, exit
if (self._socket) {
self._socket.removeAllListeners('connect_error');
self._socket.removeAllListeners('reconnect_attempt');
self._socket.removeAllListeners('reconnect_error');
self._socket.removeAllListeners('reconnect_failed');
self._socket.removeAllListeners('connect');
self._socket.removeAllListeners('reconnect');
self._socket.removeAllListeners('error');
self._socket.removeAllListeners('disconnect');
self._socket.removeAllListeners('message');
self._socket.disconnect();
self._socket = null;
}
self._channelOpen = false;
log.log('Opening channel with signaling server url:', {
url: url,
useXDR: self._socketUseXDR,
options: options
});
self._socketSession = {
type: type,
options: options,
url: url
};
self._socket = io.connect(url, options);
if (connectionType === null) {
connectionType = self._signalingServerProtocol === 'http:' ?
(type === 'Polling' ? self.SOCKET_FALLBACK.LONG_POLLING :
self.SOCKET_FALLBACK.FALLBACK_PORT) :
(type === 'Polling' ? self.SOCKET_FALLBACK.LONG_POLLING_SSL :
self.SOCKET_FALLBACK.FALLBACK_SSL_PORT);
}
self._socket.on('connect_error', function (error) {
self._channelOpen = false;
self._trigger('socketError', self.SOCKET_ERROR.CONNECTION_FAILED,
error, connectionType);
self._trigger('channelRetry', connectionType, 1);
if (options.reconnection === false) {
self._createSocket(type);
}
});
self._socket.on('reconnect_attempt', function (attempt) {
self._channelOpen = false;
self._trigger('socketError', self.SOCKET_ERROR.RECONNECTION_ATTEMPT,
attempt, connectionType);
self._trigger('channelRetry', connectionType, attempt);
});
self._socket.on('reconnect_error', function (error) {
self._channelOpen = false;
self._trigger('socketError', self.SOCKET_ERROR.RECONNECTION_FAILED,
error, connectionType);
});
self._socket.on('reconnect_failed', function (error) {
self._channelOpen = false;
self._trigger('socketError', self.SOCKET_ERROR.RECONNECTION_ABORTED,
error, connectionType);
});
self._socket.on('connect', function () {
if (!self._channelOpen) {
self._channelOpen = true;
self._trigger('channelOpen');
log.log([null, 'Socket', null, 'Channel opened']);
}
});
self._socket.on('reconnect', function () {
if (!self._channelOpen) {
self._channelOpen = true;
self._trigger('channelOpen');
log.log([null, 'Socket', null, 'Channel opened']);
}
});
self._socket.on('error', function(error) {
self._channelOpen = false;
self._trigger('channelError', error);
log.error([null, 'Socket', null, 'Exception occurred:'], error);
});
self._socket.on('disconnect', function() {
self._channelOpen = false;
self._trigger('channelClose');
log.log([null, 'Socket', null, 'Channel closed']);
if (self._inRoom) {
self.leaveRoom(false);
self._trigger('sessionDisconnect', self._user.sid, self.getPeerInfo());
}
});
self._socket.on('message', function(message) {
log.log([null, 'Socket', null, 'Received message']);
self._processSigMessage(message);
});
};
/**
* Function that starts the socket connection to the Signaling.
* This starts creating the socket connection and called at first not when requiring to fallback.
* @method _openChannel
* @private
* @for Skylink
* @since 0.5.5
*/
Skylink.prototype._openChannel = function() {
var self = this;
if (self._channelOpen) {
log.error([null, 'Socket', null, 'Unable to instantiate a new channel connection ' +
'as there is already an ongoing channel connection']);
return;
}
if (self._readyState !== self.READY_STATE_CHANGE.COMPLETED) {
log.error([null, 'Socket', null, 'Unable to instantiate a new channel connection ' +
'as readyState is not ready']);
return;
}
// set if forceSSL
if (self._forceSSL) {
self._signalingServerProtocol = 'https:';
} else {
self._signalingServerProtocol = window.location.protocol;
}
var socketType = 'WebSocket';
// For IE < 9 that doesn't support WebSocket
if (!window.WebSocket) {
socketType = 'Polling';
}
self._signalingServerPort = null;
// Begin with a websocket connection
self._createSocket(socketType);
};
/**
* Function that stops the socket connection to the Signaling.
* @method _closeChannel
* @private
* @for Skylink
* @since 0.5.5
*/
Skylink.prototype._closeChannel = function() {
if (!this._channelOpen) {
return;
}
if (this._socket) {
this._socket.removeAllListeners('connect_error');
this._socket.removeAllListeners('reconnect_attempt');
this._socket.removeAllListeners('reconnect_error');
this._socket.removeAllListeners('reconnect_failed');
this._socket.removeAllListeners('connect');
this._socket.removeAllListeners('reconnect');
this._socket.removeAllListeners('error');
this._socket.removeAllListeners('disconnect');
this._socket.removeAllListeners('message');
this._socket.disconnect();
this._socket = null;
}
this._channelOpen = false;
this._trigger('channelClose');
};