/**
* <blockquote class="info">
* Learn more about how ICE works in this
* <a href="https://temasys.com.sg/ice-what-is-this-sorcery/">article here</a>.
* </blockquote>
* The list of Peer connection ICE gathering states.
* @attribute CANDIDATE_GENERATION_STATE
* @param {String} GATHERING <small>Value <code>"gathering"</code></small>
* The value of the state when Peer connection is gathering ICE candidates.
* <small>These ICE candidates are sent to Peer for its connection to check for a suitable matching
* pair of ICE candidates to establish an ICE connection for stream audio, video and data.
* See <a href="#event_iceConnectionState"><code>iceConnectionState</code> event</a> for ICE connection status.</small>
* <small>This state cannot happen until Peer connection remote <code>"offer"</code> / <code>"answer"</code>
* session description is set. See <a href="#event_peerConnectionState">
* <code>peerConnectionState</code> event</a> for session description exchanging status.</small>
* @param {String} COMPLETED <small>Value <code>"completed"</code></small>
* The value of the state when Peer connection gathering of ICE candidates has completed.
* @type JSON
* @readOnly
* @for Skylink
* @since 0.4.1
*/
Skylink.prototype.CANDIDATE_GENERATION_STATE = {
NEW: 'new',
GATHERING: 'gathering',
COMPLETED: 'completed'
};
/**
* <blockquote class="info">
* Learn more about how ICE works in this
* <a href="https://temasys.com.sg/ice-what-is-this-sorcery/">article here</a>.
* </blockquote>
* The list of Peer connection remote ICE candidate processing states for trickle ICE connections.
* @attribute CANDIDATE_PROCESSING_STATE
* @param {String} RECEIVED <small>Value <code>"received"</code></small>
* The value of the state when the remote ICE candidate was received.
* @param {String} DROPPED <small>Value <code>"received"</code></small>
* The value of the state when the remote ICE candidate is dropped.
* @param {String} BUFFERED <small>Value <code>"buffered"</code></small>
* The value of the state when the remote ICE candidate is buffered.
* @param {String} PROCESSING <small>Value <code>"processing"</code></small>
* The value of the state when the remote ICE candidate is being processed.
* @param {String} PROCESS_SUCCESS <small>Value <code>"processSuccess"</code></small>
* The value of the state when the remote ICE candidate has been processed successfully.
* <small>The ICE candidate that is processed will be used to check against the list of
* locally generated ICE candidate to start matching for the suitable pair for the best ICE connection.</small>
* @param {String} PROCESS_ERROR <small>Value <code>"processError"</code></small>
* The value of the state when the remote ICE candidate has failed to be processed.
* @type JSON
* @readOnly
* @for Skylink
* @since 0.6.16
*/
Skylink.prototype.CANDIDATE_PROCESSING_STATE = {
RECEIVED: 'received',
DROPPED: 'dropped',
BUFFERED: 'buffered',
PROCESSING: 'processing',
PROCESS_SUCCESS: 'processSuccess',
PROCESS_ERROR: 'processError'
};
/**
* Function that handles the Peer connection gathered ICE candidate to be sent.
* @method _onIceCandidate
* @private
* @for Skylink
* @since 0.1.0
*/
Skylink.prototype._onIceCandidate = function(targetMid, candidate) {
var self = this;
var pc = self._peerConnections[targetMid];
if (!pc) {
log.warn([targetMid, 'RTCIceCandidate', null, 'Ignoring of ICE candidate event as ' +
'Peer connection does not exists ->'], candidate);
return;
}
if (candidate.candidate) {
if (!pc.gathering) {
log.log([targetMid, 'RTCIceCandidate', null, 'ICE gathering has started.']);
pc.gathering = true;
pc.gathered = false;
self._trigger('candidateGenerationState', self.CANDIDATE_GENERATION_STATE.GATHERING, targetMid);
}
var candidateType = candidate.candidate.split(' ')[7];
log.debug([targetMid, 'RTCIceCandidate', candidateType, 'Generated ICE candidate ->'], candidate);
if (self._filterCandidatesType[candidateType]) {
if (!(self._hasMCU && self._forceTURN)) {
log.warn([targetMid, 'RTCIceCandidate', candidateType, 'Dropping of sending ICE candidate as ' +
'it matches ICE candidate filtering flag ->'], candidate);
return;
}
log.warn([targetMid, 'RTCIceCandidate', candidateType, 'Not dropping of sending ICE candidate as ' +
'TURN connections are enforced as MCU is present (and act as a TURN itself) so filtering of ICE candidate ' +
'flags are not honoured ->'], candidate);
}
if (!self._gatheredCandidates[targetMid]) {
self._gatheredCandidates[targetMid] = {
sending: { host: [], srflx: [], relay: [] },
receiving: { host: [], srflx: [], relay: [] }
};
}
self._gatheredCandidates[targetMid].sending[candidateType].push({
sdpMid: candidate.sdpMid,
sdpMLineIndex: candidate.sdpMLineIndex,
candidate: candidate.candidate
});
if (!self._enableIceTrickle) {
log.warn([targetMid, 'RTCIceCandidate', candidateType, 'Dropping of sending ICE candidate as ' +
'trickle ICE is disabled ->'], candidate);
return;
}
log.debug([targetMid, 'RTCIceCandidate', candidateType, 'Sending ICE candidate ->'], candidate);
self._sendChannelMessage({
type: self._SIG_MESSAGE_TYPE.CANDIDATE,
label: candidate.sdpMLineIndex,
id: candidate.sdpMid,
candidate: candidate.candidate,
mid: self._user.sid,
target: targetMid,
rid: self._room.id
});
} else {
log.log([targetMid, 'RTCIceCandidate', null, 'ICE gathering has completed.']);
pc.gathering = false;
pc.gathered = true;
self._trigger('candidateGenerationState', self.CANDIDATE_GENERATION_STATE.COMPLETED, targetMid);
// Disable Ice trickle option
if (!self._enableIceTrickle) {
var sessionDescription = self._peerConnections[targetMid].localDescription;
if (!(sessionDescription && sessionDescription.type && sessionDescription.sdp)) {
log.warn([targetMid, 'RTCSessionDescription', null, 'Not sending any session description after ' +
'ICE gathering completed as it is not present.']);
return;
}
// a=end-of-candidates should present in non-trickle ICE connections so no need to send endOfCandidates message
self._sendChannelMessage({
type: sessionDescription.type,
sdp: self._addSDPMediaStreamTrackIDs(targetMid, sessionDescription),
mid: self._user.sid,
userInfo: self._getUserInfo(),
target: targetMid,
rid: self._room.id
});
} else if (self._gatheredCandidates[targetMid]) {
self._sendChannelMessage({
type: self._SIG_MESSAGE_TYPE.END_OF_CANDIDATES,
noOfExpectedCandidates: self._gatheredCandidates[targetMid].sending.srflx.length +
self._gatheredCandidates[targetMid].sending.host.length +
self._gatheredCandidates[targetMid].sending.relay.length,
mid: self._user.sid,
target: targetMid,
rid: self._room.id
});
}
}
};
/**
* Function that buffers the Peer connection ICE candidate when received
* before remote session description is received and set.
* @method _addIceCandidateToQueue
* @private
* @for Skylink
* @since 0.5.2
*/
Skylink.prototype._addIceCandidateToQueue = function(targetMid, canId, candidate) {
var candidateType = candidate.candidate.split(' ')[7];
log.debug([targetMid, 'RTCIceCandidate', canId + ':' + candidateType, 'Buffering ICE candidate.']);
this._trigger('candidateProcessingState', this.CANDIDATE_PROCESSING_STATE.BUFFERED,
targetMid, canId, candidateType, {
candidate: candidate.candidate,
sdpMid: candidate.sdpMid,
sdpMLineIndex: candidate.sdpMLineIndex
}, null);
this._peerCandidatesQueue[targetMid] = this._peerCandidatesQueue[targetMid] || [];
this._peerCandidatesQueue[targetMid].push([canId, candidate]);
};
/**
* Function that adds all the Peer connection buffered ICE candidates received.
* This should be called only after the remote session description is received and set.
* @method _addIceCandidateFromQueue
* @private
* @for Skylink
* @since 0.5.2
*/
Skylink.prototype._addIceCandidateFromQueue = function(targetMid) {
this._peerCandidatesQueue[targetMid] = this._peerCandidatesQueue[targetMid] || [];
for (var i = 0; i < this._peerCandidatesQueue[targetMid].length; i++) {
var canArray = this._peerCandidatesQueue[targetMid][i];
if (canArray) {
var candidateType = canArray[1].candidate.split(' ')[7];
log.debug([targetMid, 'RTCIceCandidate', canArray[0] + ':' + candidateType, 'Adding buffered ICE candidate.']);
this._addIceCandidate(targetMid, canArray[0], canArray[1]);
} else if (this._peerConnections[targetMid] &&
this._peerConnections[targetMid].signalingState !== this.PEER_CONNECTION_STATE.CLOSED) {
log.debug([targetMid, 'RTCPeerConnection', null, 'Signaling of end-of-candidates remote ICE gathering.']);
this._peerConnections[targetMid].addIceCandidate(null);
}
}
delete this._peerCandidatesQueue[targetMid];
this._signalingEndOfCandidates(targetMid);
};
/**
* Function that adds the ICE candidate to Peer connection.
* @method _addIceCandidate
* @private
* @for Skylink
* @since 0.6.16
*/
Skylink.prototype._addIceCandidate = function (targetMid, canId, candidate) {
var self = this;
var candidateType = candidate.candidate.split(' ')[7];
var onSuccessCbFn = function () {
log.log([targetMid, 'RTCIceCandidate', canId + ':' + candidateType,
'Added ICE candidate successfully.']);
self._trigger('candidateProcessingState', self.CANDIDATE_PROCESSING_STATE.PROCESS_SUCCESS,
targetMid, canId, candidateType, {
candidate: candidate.candidate,
sdpMid: candidate.sdpMid,
sdpMLineIndex: candidate.sdpMLineIndex
}, null);
};
var onErrorCbFn = function (error) {
log.error([targetMid, 'RTCIceCandidate', canId + ':' + candidateType,
'Failed adding ICE candidate ->'], error);
self._trigger('candidateProcessingState', self.CANDIDATE_PROCESSING_STATE.PROCESS_ERROR,
targetMid, canId, candidateType, {
candidate: candidate.candidate,
sdpMid: candidate.sdpMid,
sdpMLineIndex: candidate.sdpMLineIndex
}, error);
};
log.debug([targetMid, 'RTCIceCandidate', canId + ':' + candidateType, 'Adding ICE candidate.']);
self._trigger('candidateProcessingState', self.CANDIDATE_PROCESSING_STATE.PROCESSING,
targetMid, canId, candidateType, {
candidate: candidate.candidate,
sdpMid: candidate.sdpMid,
sdpMLineIndex: candidate.sdpMLineIndex
}, null);
if (!(self._peerConnections[targetMid] &&
self._peerConnections[targetMid].signalingState !== self.PEER_CONNECTION_STATE.CLOSED)) {
log.warn([targetMid, 'RTCIceCandidate', canId + ':' + candidateType, 'Dropping ICE candidate ' +
'as Peer connection does not exists or is closed']);
self._trigger('candidateProcessingState', self.CANDIDATE_PROCESSING_STATE.DROPPED,
targetMid, canId, candidateType, {
candidate: candidate.candidate,
sdpMid: candidate.sdpMid,
sdpMLineIndex: candidate.sdpMLineIndex
}, new Error('Failed processing ICE candidate as Peer connection does not exists or is closed.'));
return;
}
self._peerConnections[targetMid].addIceCandidate(candidate, onSuccessCbFn, onErrorCbFn);
};