/**
* The list of Datachannel connection states.
* @attribute DATA_CHANNEL_STATE
* @param {String} CONNECTING <small>Value <code>"connecting"</code></small>
* The value of the state when Datachannel is attempting to establish a connection.
* @param {String} OPEN <small>Value <code>"open"</code></small>
* The value of the state when Datachannel has established a connection.
* @param {String} CLOSING <small>Value <code>"closing"</code></small>
* The value of the state when Datachannel connection is closing.
* @param {String} CLOSED <small>Value <code>"closed"</code></small>
* The value of the state when Datachannel connection has closed.
* @param {String} ERROR <small>Value <code>"error"</code></small>
* The value of the state when Datachannel has encountered an exception during connection.
* @param {String} CREATE_ERROR <small>Value <code>"createError"</code></small>
* The value of the state when Datachannel has failed to establish a connection.
* @param {String} BUFFERED_AMOUNT_LOW <small>Value <code>"bufferedAmountLow"</code></small>
* The value of the state when Datachannel when the amount of data buffered to be sent
* falls below the Datachannel threshold.
* <small>This state should occur only during after <a href="#method_sendBlobData">
* <code>sendBlobData()</code> method</a> or <a href="#method_sendURLData"><code>sendURLData()</code> method</a> or
* <a href="#method_sendP2PMessage"><code>sendP2PMessage()</code> method</a>.</small>
* @param {String} SEND_MESSAGE_ERROR <small>Value <code>"sendMessageError"</code></small>
* The value of the state when Datachannel when data transfer packets or P2P message fails to send.
* <small>This state should occur only during after <a href="#method_sendBlobData">
* <code>sendBlobData()</code> method</a> or <a href="#method_sendURLData"><code>sendURLData()</code> method</a> or
* <a href="#method_sendP2PMessage"><code>sendP2PMessage()</code> method</a>.</small>
* @type JSON
* @readOnly
* @for Skylink
* @since 0.1.0
*/
Skylink.prototype.DATA_CHANNEL_STATE = {
CONNECTING: 'connecting',
OPEN: 'open',
CLOSING: 'closing',
CLOSED: 'closed',
ERROR: 'error',
CREATE_ERROR: 'createError',
BUFFERED_AMOUNT_LOW: 'bufferedAmountLow',
SEND_MESSAGE_ERROR: 'sendMessageError'
};
/**
* The list of Datachannel types.
* @attribute DATA_CHANNEL_TYPE
* @param {String} MESSAGING <small>Value <code>"messaging"</code></small>
* The value of the Datachannel type that is used only for messaging in
* <a href="#method_sendP2PMessage"><code>sendP2PMessage()</code> method</a>.
* <small>However for Peers that do not support simultaneous data transfers, this Datachannel
* type will be used to do data transfers (1 at a time).</small>
* <small>Each Peer connections will only have one of this Datachannel type and the
* connection will only close when the Peer connection is closed (happens when <a href="#event_peerConnectionState">
* <code>peerConnectionState</code> event</a> triggers parameter payload <code>state</code> as
* <code>CLOSED</code> for Peer).</small>
* @param {String} DATA <small>Value <code>"data"</code></small>
* The value of the Datachannel type that is used only for a data transfer in
* <a href="#method_sendURLData"><code>sendURLData()</code> method</a> and
* <a href="#method_sendBlobData"><code>sendBlobData()</code> method</a>.
* <small>The connection will close after the data transfer has been completed or terminated (happens when
* <a href="#event_dataTransferState"><code>dataTransferState</code> event</a> triggers parameter payload
* <code>state</code> as <code>DOWNLOAD_COMPLETED</code>, <code>UPLOAD_COMPLETED</code>,
* <code>REJECTED</code>, <code>CANCEL</code> or <code>ERROR</code> for Peer).</small>
* @type JSON
* @readOnly
* @for Skylink
* @since 0.6.1
*/
Skylink.prototype.DATA_CHANNEL_TYPE = {
MESSAGING: 'messaging',
DATA: 'data'
};
/**
* The list of Datachannel sending message error types.
* @attribute DATA_CHANNEL_MESSAGE_ERROR
* @param {String} MESSAGE <small>Value <code>"message"</code></small>
* The value of the Datachannel sending message error type when encountered during
* sending P2P message from <a href="#method_sendP2PMessage"><code>sendP2PMessage()</code> method</a>.
* @param {String} TRANSFER <small>Value <code>"transfer"</code></small>
* The value of the Datachannel sending message error type when encountered during
* data transfers from <a href="#method_sendURLData"><code>sendURLData()</code> method</a> or
* <a href="#method_sendBlobData"><code>sendBlobData()</code> method</a>.
* @type JSON
* @readOnly
* @for Skylink
* @since 0.6.16
*/
Skylink.prototype.DATA_CHANNEL_MESSAGE_ERROR = {
MESSAGE: 'message',
TRANSFER: 'transfer'
};
/**
* Function that starts a Datachannel connection with Peer.
* @method _createDataChannel
* @private
* @for Skylink
* @since 0.5.5
*/
Skylink.prototype._createDataChannel = function(peerId, dataChannel, createAsMessagingChannel) {
var self = this;
if (!self._user) {
log.error([peerId, 'RTCDataChannel', null,
'Aborting of creating or initializing Datachannel as User does not have Room session']);
return;
}
if (!(self._peerConnections[peerId] &&
self._peerConnections[peerId].signalingState !== self.PEER_CONNECTION_STATE.CLOSED)) {
log.error([peerId, 'RTCDataChannel', null,
'Aborting of creating or initializing Datachannel as Peer connection does not exists']);
return;
}
var channelName = self._user.sid + '_' + peerId;
var channelType = createAsMessagingChannel ? self.DATA_CHANNEL_TYPE.MESSAGING : self.DATA_CHANNEL_TYPE.DATA;
if (dataChannel && typeof dataChannel === 'object') {
channelName = dataChannel.label;
} else if (typeof dataChannel === 'string') {
channelName = dataChannel;
dataChannel = null;
}
if (!dataChannel) {
try {
dataChannel = self._peerConnections[peerId].createDataChannel(channelName);
} catch (error) {
log.error([peerId, 'RTCDataChannel', channelName, 'Failed creating Datachannel ->'], error);
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.CREATE_ERROR, peerId, error, channelName, channelType, null);
return;
}
}
if (!self._dataChannels[peerId]) {
log.debug([peerId, 'RTCDataChannel', channelName, 'initializing main DataChannel']);
channelType = self.DATA_CHANNEL_TYPE.MESSAGING;
self._dataChannels[peerId] = {};
} else if (self._dataChannels[peerId].main && self._dataChannels[peerId].main.channel.label === channelName) {
channelType = self.DATA_CHANNEL_TYPE.MESSAGING;
}
/**
* Subscribe to events
*/
dataChannel.onerror = function (evt) {
var channelError = evt.error || evt;
log.error([peerId, 'RTCDataChannel', channelName, 'Datachannel has an exception ->'], channelError);
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.ERROR, peerId, channelError, channelName, channelType, null);
};
dataChannel.onbufferedamountlow = function () {
log.debug([peerId, 'RTCDataChannel', channelName, 'Datachannel buffering data transfer low']);
// TODO: Should we add an event here
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.BUFFERED_AMOUNT_LOW, peerId, null, channelName, channelType, null);
};
dataChannel.onmessage = function(event) {
self._processDataChannelData(event.data, peerId, channelName, channelType);
};
var onOpenHandlerFn = function () {
log.debug([peerId, 'RTCDataChannel', channelName, 'Datachannel has opened']);
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.OPEN, peerId, null, channelName, channelType, null);
};
if (dataChannel.readyState === self.DATA_CHANNEL_STATE.OPEN) {
setTimeout(onOpenHandlerFn, 500);
} else {
self._trigger('dataChannelState', dataChannel.readyState, peerId, null, channelName, channelType, null);
dataChannel.onopen = onOpenHandlerFn;
}
var onCloseHandlerFn = function () {
log.debug([peerId, 'RTCDataChannel', channelName, 'Datachannel has closed']);
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.CLOSED, peerId, null, channelName, channelType, null);
if (channelType === self.DATA_CHANNEL_TYPE.MESSAGING) {
setTimeout(function () {
if (self._peerConnections[peerId] &&
self._peerConnections[peerId].signalingState !== self.PEER_CONNECTION_STATE.CLOSED &&
(self._peerConnections[peerId].localDescription &&
self._peerConnections[peerId].localDescription.type === self.HANDSHAKE_PROGRESS.OFFER)) {
log.debug([peerId, 'RTCDataChannel', channelName, 'Reviving Datachannel connection']);
self._createDataChannel(peerId, channelName, true);
}
}, 100);
}
};
// Fixes for Firefox bug (49 is working) -> https://bugzilla.mozilla.org/show_bug.cgi?id=1118398
if (window.webrtcDetectedBrowser === 'firefox') {
var hasTriggeredClose = false;
var timeBlockAfterClosing = 0;
dataChannel.onclose = function () {
if (!hasTriggeredClose) {
hasTriggeredClose = true;
onCloseHandlerFn();
}
};
var onFFClosed = setInterval(function () {
if (dataChannel.readyState === self.DATA_CHANNEL_STATE.CLOSED ||
hasTriggeredClose || timeBlockAfterClosing === 5) {
clearInterval(onFFClosed);
if (!hasTriggeredClose) {
hasTriggeredClose = true;
onCloseHandlerFn();
}
// After 5 seconds from CLOSING state and Firefox is not rendering to close, we have to assume to close it.
// It is dead! This fixes the case where if it's Firefox who closes the Datachannel, the connection will
// still assume as CLOSING..
} else if (dataChannel.readyState === self.DATA_CHANNEL_STATE.CLOSING) {
timeBlockAfterClosing++;
}
}, 1000);
} else {
dataChannel.onclose = onCloseHandlerFn;
}
if (channelType === self.DATA_CHANNEL_TYPE.MESSAGING) {
self._dataChannels[peerId].main = {
channelName: channelName,
channelType: channelType,
transferId: null,
channel: dataChannel
};
} else {
self._dataChannels[peerId][channelName] = {
channelName: channelName,
channelType: channelType,
transferId: channelName,
channel: dataChannel
};
}
};
/**
* Function that sends data over the Datachannel connection.
* @method _sendMessageToDataChannel
* @private
* @for Skylink
* @since 0.5.2
*/
Skylink.prototype._sendMessageToDataChannel = function(peerId, data, channelProp, doNotConvert) {
var self = this;
// Set it as "main" (MESSAGING) Datachannel
if (!channelProp || channelProp === peerId) {
channelProp = 'main';
}
// TODO: What happens when we want to send binary data over or ArrayBuffers?
if (!(typeof data === 'object' && data) && !(data && typeof data === 'string')) {
log.warn([peerId, 'RTCDataChannel', 'prop:' + channelProp, 'Dropping invalid data ->'], data);
return;
}
if (!(self._peerConnections[peerId] &&
self._peerConnections[peerId].signalingState !== self.PEER_CONNECTION_STATE.CLOSED)) {
log.warn([peerId, 'RTCDataChannel', 'prop:' + channelProp,
'Dropping for sending message as Peer connection does not exists or is closed ->'], data);
return;
}
if (!(self._dataChannels[peerId] && self._dataChannels[peerId][channelProp])) {
log.warn([peerId, 'RTCDataChannel', 'prop:' + channelProp,
'Dropping for sending message as Datachannel connection does not exists ->'], data);
return;
}
var channelName = self._dataChannels[peerId][channelProp].channelName;
var channelType = self._dataChannels[peerId][channelProp].channelType;
var readyState = self._dataChannels[peerId][channelProp].channel.readyState;
var messageType = typeof data === 'object' && data.type === self._DC_PROTOCOL_TYPE.MESSAGE ?
self.DATA_CHANNEL_MESSAGE_ERROR.MESSAGE : self.DATA_CHANNEL_MESSAGE_ERROR.TRANSFER;
if (readyState !== self.DATA_CHANNEL_STATE.OPEN) {
var notOpenError = 'Failed sending message as Datachannel connection state is not opened. Current ' +
'readyState is "' + readyState + '"';
log.error([peerId, 'RTCDataChannel', 'prop:' + channelProp, notOpenError + ' ->'], data);
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.SEND_MESSAGE_ERROR, peerId,
new Error(notOpenError), channelName, channelType, messageType);
throw new Error(notOpenError);
}
try {
if (!doNotConvert && typeof data === 'object') {
log.debug([peerId, 'RTCDataChannel', 'prop:' + channelProp, 'Sending message ->'], data);
self._dataChannels[peerId][channelProp].channel.send(JSON.stringify(data));
} else {
log.debug([peerId, 'RTCDataChannel', 'prop:' + channelProp, 'Sending data with size ->'], data.size || data.length);
self._dataChannels[peerId][channelProp].channel.send(data);
}
} catch (error) {
log.error([peerId, 'RTCDataChannel', 'prop:' + channelProp, 'Failed sending message ->'], error);
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.SEND_MESSAGE_ERROR, peerId,
error, channelName, channelType, messageType);
throw error;
}
};
/**
* Function that stops the Datachannel connection and removes object references.
* @method _closeDataChannel
* @private
* @for Skylink
* @since 0.1.0
*/
Skylink.prototype._closeDataChannel = function(peerId, channelProp) {
var self = this;
if (!self._dataChannels[peerId]) {
log.warn([peerId, 'RTCDataChannel', channelProp || null,
'Aborting closing Datachannels as Peer connection does not have Datachannel sessions']);
return;
}
var closeFn = function (rChannelProp) {
var channelName = self._dataChannels[peerId][rChannelProp].channelName;
var channelType = self._dataChannels[peerId][rChannelProp].channelType;
if (self._dataChannels[peerId][rChannelProp].readyState !== self.DATA_CHANNEL_STATE.CLOSED) {
log.debug([peerId, 'RTCDataChannel', channelName, 'Closing Datachannel']);
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.CLOSING, peerId, null, channelName, channelType, null);
self._dataChannels[peerId][rChannelProp].channel.close();
delete self._dataChannels[peerId][rChannelProp];
}
};
if (!channelProp) {
for (var channelNameProp in self._dataChannels) {
if (self._dataChannels[peerId].hasOwnProperty(channelNameProp)) {
if (self._dataChannels[peerId][channelNameProp]) {
closeFn(channelNameProp);
}
}
}
} else {
if (!self._dataChannels[peerId][channelProp]) {
log.warn([peerId, 'RTCDataChannel', channelProp, 'Aborting closing Datachannel as it does not exists']);
return;
}
closeFn(channelProp);
}
};