/**
* These are the list of DataChannel connection states that Skylink would trigger.
* - Some of the state references the [w3c WebRTC Specification Draft](http://w3c.github.io/webrtc-pc/#idl-def-RTCDataChannelState),
* except the <code>ERROR</code> state, which is an addition provided state by Skylink
* to inform exception during the DataChannel connection with Peers.
* @attribute DATA_CHANNEL_STATE
* @type JSON
* @param {String} CONNECTING <small>Value <code>"connecting"</code></small>
* The state when DataChannel is attempting to establish a connection.<br>
* This is the initial state when a DataChannel connection is created.
* @param {String} OPEN <small>Value <code>"open"</code></small>
* The state when DataChannel connection is established.<br>
* This happens usually after <code>CONNECTING</code> state, or not when DataChannel connection
* is from initializing Peer (the one who begins the DataChannel connection).
* @param {String} CLOSING <small>Value <code>"closing"</code></small>
* The state when DataChannel connection is closing.<br>
* This happens when DataChannel connection is closing and happens after <code>OPEN</code>.
* @param {String} CLOSED <small>Value <code>"closed"</code></small>
* The state when DataChannel connection is closed.<br>
* This happens when DataChannel connection has closed and happens after <code>CLOSING</code>
* (or sometimes <code>OPEN</code> depending on the browser implementation).
* @param {String} ERROR <small>Value <code>"error"</code></small>
* The state when DataChannel connection have met with an exception.<br>
* This may happen during any state not after <code>CLOSED</code>.
* @readOnly
* @component DataChannel
* @for Skylink
* @since 0.1.0
*/
Skylink.prototype.DATA_CHANNEL_STATE = {
CONNECTING: 'connecting',
OPEN: 'open',
CLOSING: 'closing',
CLOSED: 'closed',
ERROR: 'error'
};
/**
* These are the types of DataChannel connection that Skylink provides.
* - Different channels serves different functionalities.
* @attribute DATA_CHANNEL_TYPE
* @type JSON
* @param {String} MESSAGING <small><b>MAIN connection</b> | Value <code>"messaging"</code></small>
* This DataChannel connection is used for P2P messaging only, as used in
* {{#crossLink "Skylink/sendP2PMessage:method"}}sendP2PMessage(){{/crossLink}}.<br>
* Unless if self connects with Peers connecting from the mobile SDK platform applications,
* this connection would be used for data transfers as used in
* {{#crossLink "Skylink/sendBlobData:method"}}sendBlobData(){{/crossLink}} and
* and {{#crossLink "Skylink/sendURLData:method"}}sendURLData(){{/crossLink}}, which allows
* only one outgoing and incoming data transfer one at a time (no multi-transfer support).<br>
* This connection will always be kept alive until the Peer connection has ended.
* @param {String} DATA <small>Value <code>"data"</code></small>
* This DataChannel connection is used for a data transfer, as used in
* {{#crossLink "Skylink/sendBlobData:method"}}sendBlobData(){{/crossLink}}
* and {{#crossLink "Skylink/sendURLData:method"}}sendURLData(){{/crossLink}}.<br>
* If self connects with Peers with DataChannel connections of this type,
* it indicates that multi-transfer is supported.<br>
* This connection will be closed once the data transfer has completed or terminated.
* @readOnly
* @component DataChannel
* @for Skylink
* @since 0.6.1
*/
Skylink.prototype.DATA_CHANNEL_TYPE = {
MESSAGING: 'messaging',
DATA: 'data'
};
/**
* The flag that indicates if Peers connection should have any
* DataChannel connections.
* @attribute _enableDataChannel
* @type Boolean
* @default true
* @private
* @component DataChannel
* @for Skylink
* @since 0.3.0
*/
Skylink.prototype._enableDataChannel = true;
/**
* Stores the list of DataChannel connections.
* @attribute _dataChannels
* @param {Array} (#peerId) The Peer ID associated with the list of
* DataChannel connections.
* @param {Object} (#peerId).main The DataChannel connection object
* that is used for messaging only associated with the Peer connection.
* This is the sole channel for sending P2P messages in
* {{#crossLink "Skylink/sendP2PMessage:method"}}sendP2PMessage(){{/crossLink}}.
* This connection will always be kept alive until the Peer connection has
* ended. The <code>channelName</code> for this reserved key is <code>"main"</code>.
* @param {Object} (#peerId).(#channelName) The DataChannel connection
* object that is used temporarily for a data transfer associated with the
* Peer connection. This is using caused by methods
* {{#crossLink "Skylink/sendBlobData:method"}}sendBlobData(){{/crossLink}}
* and {{#crossLink "Skylink/sendURLData:method"}}sendURLData(){{/crossLink}}.
* This connection will be closed once the transfer has completed or terminated.
* The <code>channelName</code> is usually the data transfer ID.
* @type JSON
* @private
* @component DataChannel
* @for Skylink
* @since 0.2.0
*/
Skylink.prototype._dataChannels = {};
/**
* Starts a DataChannel connection with a Peer connection. If the
* DataChannel is provided in the parameter, it simply appends
* event handlers to check the current state of the DataChannel.
* @method _createDataChannel
* @param {String} peerId The Peer ID to start the
* DataChannel with or associate the provided DataChannel object
* connection with.
* @param {String} channelType The DataChannel functionality type.
* [Rel: Skylink.DATA_CHANNEL_TYPE]
* @param {Object} [dataChannel] The RTCDataChannel object received
* in the Peer connection <code>.ondatachannel</code> event.
* @param {String} customChannelName The custom RTCDataChannel label
* name to identify the different opened channels.
* @trigger dataChannelState
* @return {Object} The DataChannel connection object associated with
* the provided Peer ID.
* @private
* @component DataChannel
* @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];
if (window.webrtcDetectedDCSupport !== 'SCTP' &&
window.webrtcDetectedDCSupport !== 'plugin') {
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;
};
/**
* Sends data over the DataChannel connection associated
* with the Peer connection.
* The current supported data type is <code>string</code>. <code>Blob</code>,
* <code>ArrayBuffer</code> types support is not yet currently handled or
* implemented.
* @method _sendDataChannelMessage
* @param {String} peerId The Peer ID to send the data to the
* associated DataChannel connection.
* @param {JSON|String} data The data to send over. <code>string</code> is only
* used to send binary data string over. <code>JSON</code> is primarily used
* for the {{#crossLink "Skylink/DT_PROTOCOL_VERSION:attribute"}}DT Protocol{{/crossLink}}
* that Skylink follows for P2P messaging and transfers.
* @param {String} [channelName="main"] The DataChannel channelName of the connection
* to send the data over to. The datachannel to send messages to. By default,
* if the DataChannel <code>channelName</code> is not provided,
* the DataChannel connection associated with the channelName <code>"main"</code> would be used.
* @trigger dataChannelState
* @private
* @component DataChannel
* @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);
}
}
};
/**
* Stops DataChannel connections associated with a Peer connection
* and remove any object references to the DataChannel connection(s).
* @method _closeDataChannel
* @param {String} peerId The Peer ID associated with the DataChannel
* connection(s) to close.
* @param {String} [channelName] The targeted DataChannel <code>channelName</code>
* to close the connection with. If <code>channelName</code> is not provided,
* all associated DataChannel connections with the Peer connection would be closed.
* @trigger dataChannelState
* @private
* @component DataChannel
* @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
});
}
}
};