File: source/ice-candidate.js

                      /**
                       * <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 (candidateType === 'endOfCandidates' || !(self._peerConnections[targetMid] &&
                            self._peerConnections[targetMid].localDescription && self._peerConnections[targetMid].localDescription.sdp &&
                            self._peerConnections[targetMid].localDescription.sdp.indexOf('\r\na=mid:' + candidate.sdpMid + '\r\n') > -1)) {
                            log.warn([targetMid, 'RTCIceCandidate', candidateType, 'Dropping of sending ICE candidate ' +
                              'end-of-candidates signal or unused ICE candidates to prevent errors ->'], candidate);
                            return;
                          }
                      
                          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.']);
                      
                          if (pc.gathered) {
                            return;
                          }
                      
                          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(targetMid),
                              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 &&
                            AdapterJS && !this._isLowerThanVersion(AdapterJS.VERSION, '0.14.0')) {
                            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 &&
                          self._peerConnections[targetMid].remoteDescription &&
                          self._peerConnections[targetMid].remoteDescription.sdp &&
                          self._peerConnections[targetMid].remoteDescription.sdp.indexOf('\r\na=mid:' + candidate.sdpMid + '\r\n') > -1)) {
                          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);
                      };