/**
* 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 connection has errors.
* @type JSON
* @readOnly
* @for Skylink
* @since 0.1.0
*/
Skylink.prototype.DATA_CHANNEL_STATE = {
CONNECTING: 'connecting',
OPEN: 'open',
CLOSING: 'closing',
CLOSED: 'closed',
ERROR: 'error'
};
/**
* 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'
};
/**
* Stores the flag if Peers should have any Datachannel connections.
* @attribute _enableDataChannel
* @default true
* @type Boolean
* @private
* @for Skylink
* @since 0.3.0
*/
Skylink.prototype._enableDataChannel = true;
/**
* Stores the list of Peer Datachannel connections.
* @attribute _dataChannels
* @param {JSON} (#peerId) The list of Datachannels associated with Peer ID.
* @param {RTCDataChannel} (#peerId).<#channelLabel> The Datachannel connection.
* The property name <code>"main"</code> is reserved for messaging Datachannel type.
* @type JSON
* @private
* @for Skylink
* @since 0.2.0
*/
Skylink.prototype._dataChannels = {};
/**
* Function that starts a Datachannel connection with Peer.
* @method _createDataChannel
* @private
* @for Skylink
* @since 0.5.5
*/
Skylink.prototype._createDataChannel = function(peerId, channelType, dc, customChannelName) {
var self = this;
if (typeof dc === 'string') {
customChannelName = dc;
dc = null;
}
if (!customChannelName) {
log.error([peerId, 'RTCDataChannel', null, 'Aborting of creating Datachannel as no ' +
'channel name is provided for channel. Aborting of creating Datachannel'], {
channelType: channelType
});
return;
}
var channelName = (dc) ? dc.label : customChannelName;
var pc = self._peerConnections[peerId];
var SctpSupported =
!(window.webrtcDetectedBrowser === 'chrome' && window.webrtcDetectedVersion < 30 ||
window.webrtcDetectedBrowser === 'opera' && window.webrtcDetectedVersion < 20 );
if (!SctpSupported) {
log.warn([peerId, 'RTCDataChannel', channelName, 'SCTP not supported'], {
channelType: channelType
});
return;
}
var dcHasOpened = function () {
log.log([peerId, 'RTCDataChannel', channelName, 'Datachannel state ->'], {
readyState: 'open',
channelType: channelType
});
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.OPEN,
peerId, null, channelName, channelType);
};
if (!dc) {
try {
dc = pc.createDataChannel(channelName);
if (dc.readyState === self.DATA_CHANNEL_STATE.OPEN) {
// the datachannel was not defined in array before it was triggered
// set a timeout to allow the dc objec to be returned before triggering "open"
setTimeout(dcHasOpened, 500);
} else {
self._trigger('dataChannelState', dc.readyState, peerId, null,
channelName, channelType);
self._wait(function () {
log.log([peerId, 'RTCDataChannel', dc.label, 'Firing callback. ' +
'Datachannel state has opened ->'], dc.readyState);
dcHasOpened();
}, function () {
return dc.readyState === self.DATA_CHANNEL_STATE.OPEN;
});
}
log.debug([peerId, 'RTCDataChannel', channelName, 'Datachannel RTC object is created'], {
readyState: dc.readyState,
channelType: channelType
});
} catch (error) {
log.error([peerId, 'RTCDataChannel', channelName, 'Exception occurred in datachannel:'], {
channelType: channelType,
error: error
});
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.ERROR, peerId, error,
channelName, channelType);
return;
}
} else {
if (dc.readyState === self.DATA_CHANNEL_STATE.OPEN) {
// the datachannel was not defined in array before it was triggered
// set a timeout to allow the dc objec to be returned before triggering "open"
setTimeout(dcHasOpened, 500);
} else {
dc.onopen = dcHasOpened;
}
}
log.log([peerId, 'RTCDataChannel', channelName, 'Binary type support ->'], {
binaryType: dc.binaryType,
readyState: dc.readyState,
channelType: channelType
});
dc.dcType = channelType;
dc.onerror = function(error) {
log.error([peerId, 'RTCDataChannel', channelName, 'Exception occurred in datachannel:'], {
channelType: channelType,
readyState: dc.readyState,
error: error
});
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.ERROR, peerId, error,
channelName, channelType);
};
dc.onclose = function() {
log.debug([peerId, 'RTCDataChannel', channelName, 'Datachannel state ->'], {
readyState: 'closed',
channelType: channelType
});
dc.hasFiredClosed = true;
// give it some time to set the variable before actually closing and checking.
setTimeout(function () {
// redefine pc
pc = self._peerConnections[peerId];
// if closes because of firefox, reopen it again
// if it is closed because of a restart, ignore
var checkIfChannelClosedDuringConn = !!pc ? !pc.dataChannelClosed : false;
if (checkIfChannelClosedDuringConn && dc.dcType === self.DATA_CHANNEL_TYPE.MESSAGING) {
log.debug([peerId, 'RTCDataChannel', channelName, 'Re-opening closed datachannel in ' +
'on-going connection'], {
channelType: channelType,
readyState: dc.readyState,
isClosedDuringConnection: checkIfChannelClosedDuringConn
});
self._dataChannels[peerId].main =
self._createDataChannel(peerId, self.DATA_CHANNEL_TYPE.MESSAGING, null, peerId);
log.debug([peerId, 'RTCDataChannel', channelName, 'Re-opened closed datachannel'], {
channelType: channelType,
readyState: dc.readyState,
isClosedDuringConnection: checkIfChannelClosedDuringConn
});
} else {
self._closeDataChannel(peerId, channelName);
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.CLOSED, peerId, null,
channelName, channelType);
log.debug([peerId, 'RTCDataChannel', channelName, 'Datachannel has closed'], {
channelType: channelType,
readyState: dc.readyState,
isClosedDuringConnection: checkIfChannelClosedDuringConn
});
}
}, 100);
};
dc.onmessage = function(event) {
self._dataChannelProtocolHandler(event.data, peerId, channelName, channelType);
};
return dc;
};
/**
* Function that sends data over the Datachannel connection.
* @method _sendDataChannelMessage
* @private
* @for Skylink
* @since 0.5.2
*/
Skylink.prototype._sendDataChannelMessage = function(peerId, data, channelKey) {
var self = this;
var channelName;
if (!channelKey || channelKey === peerId) {
channelKey = 'main';
}
var dcList = self._dataChannels[peerId] || {};
var dc = dcList[channelKey];
if (!dc) {
log.error([peerId, 'RTCDataChannel', channelKey + '|' + channelName,
'Datachannel connection to peer does not exist'], {
enabledState: self._enableDataChannel,
dcList: dcList,
dc: dc,
type: (data.type || 'DATA'),
data: data,
channelKey: channelKey
});
return;
} else {
channelName = dc.label;
log.debug([peerId, 'RTCDataChannel', channelKey + '|' + channelName,
'Sending data using this channel key'], data);
if (dc.readyState === this.DATA_CHANNEL_STATE.OPEN) {
var dataString = (typeof data === 'object') ? JSON.stringify(data) : data;
log.debug([peerId, 'RTCDataChannel', channelKey + '|' + dc.label,
'Sending to peer ->'], {
readyState: dc.readyState,
type: (data.type || 'DATA'),
data: data
});
dc.send(dataString);
} else {
log.error([peerId, 'RTCDataChannel', channelKey + '|' + dc.label,
'Datachannel is not opened'], {
readyState: dc.readyState,
type: (data.type || 'DATA'),
data: data
});
this._trigger('dataChannelState', this.DATA_CHANNEL_STATE.ERROR,
peerId, 'Datachannel is not ready.\nState is: ' + dc.readyState);
}
}
};
/**
* Function that stops the Datachannel connection and removes object references.
* @method _closeDataChannel
* @private
* @for Skylink
* @since 0.1.0
*/
Skylink.prototype._closeDataChannel = function(peerId, channelName) {
var self = this;
var dcList = self._dataChannels[peerId] || {};
var dcKeysList = Object.keys(dcList);
if (channelName) {
dcKeysList = [channelName];
}
for (var i = 0; i < dcKeysList.length; i++) {
var channelKey = dcKeysList[i];
var dc = dcList[channelKey];
if (dc) {
if (dc.readyState !== self.DATA_CHANNEL_STATE.CLOSED) {
log.log([peerId, 'RTCDataChannel', channelKey + '|' + dc.label,
'Closing datachannel']);
dc.close();
} else {
if (!dc.hasFiredClosed && window.webrtcDetectedBrowser === 'firefox') {
log.log([peerId, 'RTCDataChannel', channelKey + '|' + dc.label,
'Closed Firefox datachannel']);
self._trigger('dataChannelState', self.DATA_CHANNEL_STATE.CLOSED, peerId,
null, channelName, channelKey === 'main' ? self.DATA_CHANNEL_TYPE.MESSAGING :
self.DATA_CHANNEL_TYPE.DATA);
}
}
delete self._dataChannels[peerId][channelKey];
log.log([peerId, 'RTCDataChannel', channelKey + '|' + dc.label,
'Sucessfully removed datachannel']);
} else {
log.log([peerId, 'RTCDataChannel', channelKey + '|' + channelName,
'Unable to close Datachannel as it does not exists'], {
dc: dc,
dcList: dcList
});
}
}
};