text: /*! simple-peer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
  const debug = require('debug')('simple-peer-curltech')
  const getBrowserRTC = require('get-browser-rtc')
  const randombytes = require('randombytes')
  const stream = require('readable-stream')
  const queueMicrotask = require('queue-microtask') // TODO: remove when Node 10 is not supported
  const errCode = require('err-code')
  const { Buffer } = require('buffer')
  
  const MAX_BUFFERED_AMOUNT = 64 * 1024
  const ICECOMPLETE_TIMEOUT = 5 * 1000
  const CHANNEL_CLOSING_TIMEOUT = 5 * 1000
  
  // HACK: Filter trickle lines when trickle is disabled #354
  function filterTrickle (sdp) {
    return sdp.replace(/a=ice-options:trickle\s\n/g, '')
  }
  
  function warn (message) {
    console.warn(message)
  }
  
  /**
   * WebRTC peer connection. Same API as node core `net.Socket`, plus a few extra methods.
   * Duplex stream.
   * @param {Object} opts
   */
  class Peer extends stream.Duplex {
    constructor (opts) {
      opts = Object.assign({
        allowHalfOpen: false
      }, opts)
  
      super(opts)
  
      this._id = randombytes(4).toString('hex').slice(0, 7)
      this._debug('new peer %o', opts)
  
      this.channelName = opts.initiator
        ? opts.channelName || randombytes(20).toString('hex')
        : null
  
      this.initiator = opts.initiator || false
      this.channelConfig = opts.channelConfig || Peer.channelConfig
      this.channelNegotiated = this.channelConfig.negotiated
      this.config = Object.assign({}, Peer.config, opts.config)
      this.offerOptions = opts.offerOptions || {}
      this.answerOptions = opts.answerOptions || {}
      this.sdpTransform = opts.sdpTransform || (sdp => sdp)
      this.streams = opts.streams || (opts.stream ? [opts.stream] : []) // support old "stream" option
      this.trickle = opts.trickle !== undefined ? opts.trickle : true
      this.allowHalfTrickle = opts.allowHalfTrickle !== undefined ? opts.allowHalfTrickle : false
      this.iceCompleteTimeout = opts.iceCompleteTimeout || ICECOMPLETE_TIMEOUT
      this.extension = opts.extension || {}
      this.destroyed = false
      this.destroying = false
      this._connected = false
  
      this.remoteAddress = undefined
      this.remoteFamily = undefined
      this.remotePort = undefined
      this.localAddress = undefined
      this.localFamily = undefined
      this.localPort = undefined
  
      this._wrtc = (opts.wrtc && typeof opts.wrtc === 'object')
        ? opts.wrtc
        : getBrowserRTC()
  
      if (!this._wrtc) {
        if (typeof window === 'undefined') {
          throw errCode(new Error('No WebRTC support: Specify `opts.wrtc` option in this environment'), 'ERR_WEBRTC_SUPPORT')
        } else {
          throw errCode(new Error('No WebRTC support: Not a supported browser'), 'ERR_WEBRTC_SUPPORT')
        }
      }
  
      this._pcReady = false
      this._channelReady = false
      this._iceComplete = false // ice candidate trickle done (got null candidate)
      this._iceCompleteTimer = null // send an offer/answer anyway after some timeout
      this._channel = null
      this._pendingCandidates = []
  
      this._isNegotiating = false // is this peer waiting for negotiation to complete?
      this._firstNegotiation = true
      this._batchedNegotiation = false // batch synchronous negotiations
      this._queuedNegotiation = false // is there a queued negotiation request?
      this._sendersAwaitingStable = []
      this._senderMap = new Map()
      this._closingInterval = null
  
      this._remoteTracks = []
      this._remoteStreams = []
  
      this._chunk = null
      this._cb = null
      this._interval = null
  
      try {
        this._pc = new (this._wrtc.RTCPeerConnection)(this.config)
      } catch (err) {
        this.destroy(errCode(err, 'ERR_PC_CONSTRUCTOR'))
        return
      }
  
      // We prefer feature detection whenever possible, but sometimes that's not
      // possible for certain implementations.
      this._isReactNativeWebrtc = typeof this._pc._peerConnectionId === 'number'
  
      this._pc.oniceconnectionstatechange = () => {
        this._onIceStateChange()
      }
      this._pc.onicegatheringstatechange = () => {
        this._onIceStateChange()
      }
      this._pc.onconnectionstatechange = () => {
        this._onConnectionStateChange()
      }
      this._pc.onsignalingstatechange = () => {
        this._onSignalingStateChange()
      }
      this._pc.onicecandidate = event => {
        this._onIceCandidate(event)
      }
  
      // HACK: Fix for odd Firefox behavior, see: https://github.com/feross/simple-peer/pull/783
      if (typeof this._pc.peerIdentity === 'object') {
        this._pc.peerIdentity.catch(err => {
          this.destroy(errCode(err, 'ERR_PC_PEER_IDENTITY'))
        })
      }
  
      // Other spec events, unused by this implementation:
      // - onconnectionstatechange
      // - onicecandidateerror
      // - onfingerprintfailure
      // - onnegotiationneeded
  
      if (this.initiator || this.channelNegotiated) {
        this._setupData({
          channel: this._pc.createDataChannel(this.channelName, this.channelConfig)
        })
      } else {
        this._pc.ondatachannel = event => {
          this._setupData(event)
        }
      }
  
      if (this.streams) {
        this.streams.forEach(stream => {
          this.addStream(stream)
        })
      }
      this._pc.ontrack = event => {
        this._onTrack(event)
      }
  
      this._debug('initial negotiation')
      this._needsNegotiation()
  
      this._onFinishBound = () => {
        this._onFinish()
      }
      this.once('finish', this._onFinishBound)
    }
  
    get bufferSize () {
      return (this._channel && this._channel.bufferedAmount) || 0
    }
  
    // HACK: it's possible channel.readyState is "closing" before peer.destroy() fires
    // https://bugs.chromium.org/p/chromium/issues/detail?id=882743
    get connected () {
      return (this._connected && this._channel.readyState === 'open')
    }
  
    address () {
      return { port: this.localPort, family: this.localFamily, address: this.localAddress }
    }
  
    signal (data) {
      if (this.destroying) return
      if (this.destroyed) throw errCode(new Error('cannot signal after peer is destroyed'), 'ERR_DESTROYED')
      if (typeof data === 'string') {
        try {
          data = JSON.parse(data)
        } catch (err) {
          data = {}
        }
      }
      this._debug('signal()')
  
      if (data.renegotiate && this.initiator) {
        this._debug('got request to renegotiate')
        this._needsNegotiation()
      }
      if (data.transceiverRequest && this.initiator) {
        this._debug('got request for transceiver')
        this.addTransceiver(data.transceiverRequest.kind, data.transceiverRequest.init)
      }
      if (data.candidate) {
        if (this._pc.remoteDescription && this._pc.remoteDescription.type) {
          this._addIceCandidate(data.candidate)
        } else {
          this._pendingCandidates.push(data.candidate)
        }
      }
      if (data.sdp) {
        this._pc.setRemoteDescription(new (this._wrtc.RTCSessionDescription)(data))
          .then(() => {
            if (this.destroyed) return
  
            this._pendingCandidates.forEach(candidate => {
              this._addIceCandidate(candidate)
            })
            this._pendingCandidates = []
  
            if (this._pc.remoteDescription.type === 'offer') this._createAnswer()
          })
          .catch(err => {
            this.destroy(errCode(err, 'ERR_SET_REMOTE_DESCRIPTION'))
          })
      }
      if (!data.sdp && !data.candidate && !data.renegotiate && !data.transceiverRequest) {
        this.destroy(errCode(new Error('signal() called with invalid signal data'), 'ERR_SIGNALING'))
      }
    }
  
    _addIceCandidate (candidate) {
      const iceCandidateObj = new this._wrtc.RTCIceCandidate(candidate)
      this._pc.addIceCandidate(iceCandidateObj)
        .catch(err => {
          if (!iceCandidateObj.address || iceCandidateObj.address.endsWith('.local')) {
            warn('Ignoring unsupported ICE candidate.')
          } else {
            console.log(err)
            //this.destroy(errCode(err, 'ERR_ADD_ICE_CANDIDATE'))
          }
        })
    }
  
    /**
     * Send text/binary data to the remote peer.
     * @param {ArrayBufferView|ArrayBuffer|Buffer|string|Blob} chunk
     */
    send (chunk) {
      if (this.destroying) return
      if (this.destroyed) throw errCode(new Error('cannot send after peer is destroyed'), 'ERR_DESTROYED')
      this._channel.send(chunk)
    }
  
    /**
     * Add a Transceiver to the connection.
     * @param {String} kind
     * @param {Object} init
     */
    addTransceiver (kind, init) {
      if (this.destroying) return
      if (this.destroyed) throw errCode(new Error('cannot addTransceiver after peer is destroyed'), 'ERR_DESTROYED')
      this._debug('addTransceiver()')
  
      if (this.initiator) {
        try {
          if(this._pc.addTransceiver){
            this._pc.addTransceiver(kind, init)
          }
          this._needsNegotiation()
        } catch (err) {
          console.log(err)
          //this.destroy(errCode(err, 'ERR_ADD_TRANSCEIVER'))
        }
      } else {
        if(!this._pc.remoteStreams || JSON.stringify(this._pc.remoteStreams) == "{}"){
          this.emit('signal', { // request initiator to renegotiate
            type: 'transceiverRequest',
            transceiverRequest: { kind, init }
          })
        }
  
      }
    }
  
    /**
     * Add a MediaStream to the connection.
     * @param {MediaStream} stream
     */
    addStream (stream) {
      if (this.destroying) return
      if (this.destroyed) throw errCode(new Error('cannot addStream after peer is destroyed'), 'ERR_DESTROYED')
      this._debug('addStream()')
  
      stream.getTracks().forEach(track => {
        this.addTrack(track, stream)
      })
    }
  
    /**
     * Add a MediaStreamTrack to the connection.
     * @param {MediaStreamTrack} track
     * @param {MediaStream} stream
     */
    addTrack (track, stream) {
      if (this.destroying) return
      if (this.destroyed) throw errCode(new Error('cannot addTrack after peer is destroyed'), 'ERR_DESTROYED')
      this._debug('addTrack()')
  
      const submap = this._senderMap.get(track) || new Map() // nested Maps map [track, stream] to sender
      let sender = submap.get(stream)
      if (!sender) {
        sender = this._pc.addTrack(track, stream)
        submap.set(stream, sender)
        this._senderMap.set(track, submap)
        this._needsNegotiation()
      } else if (sender.removed) {
        throw errCode(new Error('Track has been removed. You should enable/disable tracks that you want to re-add.'), 'ERR_SENDER_REMOVED')
      } else {
        throw errCode(new Error('Track has already been added to that stream.'), 'ERR_SENDER_ALREADY_ADDED')
      }
    }
  
    /**
     * Replace a MediaStreamTrack by another in the connection.
     * @param {MediaStreamTrack} oldTrack
     * @param {MediaStreamTrack} newTrack
     * @param {MediaStream} stream
     */
    replaceTrack (oldTrack, newTrack, stream) {
      if (this.destroying) return
      if (this.destroyed) throw errCode(new Error('cannot replaceTrack after peer is destroyed'), 'ERR_DESTROYED')
      this._debug('replaceTrack()')
  
      const submap = this._senderMap.get(oldTrack)
      const sender = submap ? submap.get(stream) : null
      if (!sender) {
        throw errCode(new Error('Cannot replace track that was never added.'), 'ERR_TRACK_NOT_ADDED')
      }
      if (newTrack) this._senderMap.set(newTrack, submap)
  
      if (sender.replaceTrack != null) {
        sender.replaceTrack(newTrack)
      } else {
        this.destroy(errCode(new Error('replaceTrack is not supported in this browser'), 'ERR_UNSUPPORTED_REPLACETRACK'))
      }
    }
  
    /**
     * Remove a MediaStreamTrack from the connection.
     * @param {MediaStreamTrack} track
     * @param {MediaStream} stream
     */
    removeTrack (track, stream) {
      if (this.destroying) return
      if (this.destroyed) throw errCode(new Error('cannot removeTrack after peer is destroyed'), 'ERR_DESTROYED')
      this._debug('removeSender()')
  
      const submap = this._senderMap.get(track)
      const sender = submap ? submap.get(stream) : null
      if (!sender) {
        throw errCode(new Error('Cannot remove track that was never added.'), 'ERR_TRACK_NOT_ADDED')
      }
      try {
        sender.removed = true
        this._pc.removeTrack(sender)
      } catch (err) {
        if (err.name === 'NS_ERROR_UNEXPECTED') {
          this._sendersAwaitingStable.push(sender) // HACK: Firefox must wait until (signalingState === stable) https://bugzilla.mozilla.org/show_bug.cgi?id=1133874
        } else {
          this.destroy(errCode(err, 'ERR_REMOVE_TRACK'))
        }
      }
      this._needsNegotiation()
    }
  
    /**
     * Remove a MediaStream from the connection.
     * @param {MediaStream} stream
     */
    removeStream (stream) {
      if (this.destroying) return
      if (this.destroyed) throw errCode(new Error('cannot removeStream after peer is destroyed'), 'ERR_DESTROYED')
      this._debug('removeSenders()')
  
      stream.getTracks().forEach(track => {
        this.removeTrack(track, stream)
      })
    }
  
    _needsNegotiation () {
      this._debug('_needsNegotiation')
      if (this._batchedNegotiation) return // batch synchronous renegotiations
      this._batchedNegotiation = true
      queueMicrotask(() => {
        this._batchedNegotiation = false
        if (this.initiator || !this._firstNegotiation) {
          this._debug('starting batched negotiation')
          this.negotiate()
        } else {
          this._debug('non-initiator initial negotiation request discarded')
        }
        this._firstNegotiation = false
      })
    }
  
    negotiate () {
      if (this.destroying) return
      if (this.destroyed) throw errCode(new Error('cannot negotiate after peer is destroyed'), 'ERR_DESTROYED')
  
      if (this.initiator) {
        if (this._isNegotiating) {
          this._queuedNegotiation = true
          this._debug('already negotiating, queueing')
        } else {
          this._debug('start negotiation')
          setTimeout(() => { // HACK: Chrome crashes if we immediately call createOffer
            this._createOffer()
          }, 0)
        }
      } else {
        if (this._isNegotiating) {
          this._queuedNegotiation = true
          this._debug('already negotiating, queueing')
        } else {
          this._debug('requesting negotiation from initiator')
          this.emit('signal', { // request initiator to renegotiate
            type: 'renegotiate',
            renegotiate: true
          })
        }
      }
      this._isNegotiating = true
    }
  
    // TODO: Delete this method once readable-stream is updated to contain a default
    // implementation of destroy() that automatically calls _destroy()
    // See: https://github.com/nodejs/readable-stream/issues/283
    destroy (err) {
      this._destroy(err, () => {})
    }
  
    _destroy (err, cb) {
      if (this.destroyed || this.destroying) return
      this.destroying = true
  
      this._debug('destroying (error: %s)', err && (err.message || err))
  
      queueMicrotask(() => { // allow events concurrent with the call to _destroy() to fire (see #692)
        this.destroyed = true
        this.destroying = false
  
        this._debug('destroy (error: %s)', err && (err.message || err))
  
        this.readable = this.writable = false
  
        if (!this._readableState.ended) this.push(null)
        if (!this._writableState.finished) this.end()
  
        this._connected = false
        this._pcReady = false
        this._channelReady = false
        this._remoteTracks = null
        this._remoteStreams = null
        this._senderMap = null
  
        clearInterval(this._closingInterval)
        this._closingInterval = null
  
        clearInterval(this._interval)
        this._interval = null
        this._chunk = null
        this._cb = null
  
        if (this._onFinishBound) this.removeListener('finish', this._onFinishBound)
        this._onFinishBound = null
  
        if (this._channel) {
          try {
            this._channel.close()
          } catch (err) {}
  
          // allow events concurrent with destruction to be handled
          this._channel.onmessage = null
          this._channel.onopen = null
          this._channel.onclose = null
          this._channel.onerror = null
        }
        if (this._pc) {
          try {
            this._pc.close()
          } catch (err) {}
  
          // allow events concurrent with destruction to be handled
          this._pc.oniceconnectionstatechange = null
          this._pc.onicegatheringstatechange = null
          this._pc.onsignalingstatechange = null
          this._pc.onicecandidate = null
          this._pc.ontrack = null
          this._pc.ondatachannel = null
        }
        this._pc = null
        this._channel = null
  
        if (err) this.emit('error', err)
        this.emit('close')
        cb()
      })
    }
  
    _setupData (event) {
      if (!event.channel) {
        // In some situations `pc.createDataChannel()` returns `undefined` (in wrtc),
        // which is invalid behavior. Handle it gracefully.
        // See: https://github.com/feross/simple-peer/issues/163
        return this.destroy(errCode(new Error('Data channel event is missing `channel` property'), 'ERR_DATA_CHANNEL'))
      }
  
      this._channel = event.channel
      this._channel.binaryType = 'arraybuffer'
  
      if (typeof this._channel.bufferedAmountLowThreshold === 'number') {
        this._channel.bufferedAmountLowThreshold = MAX_BUFFERED_AMOUNT
      }
  
      this.channelName = this._channel.label
  
      this._channel.onmessage = event => {
        this._onChannelMessage(event)
      }
      this._channel.onbufferedamountlow = () => {
        this._onChannelBufferedAmountLow()
      }
      this._channel.onopen = () => {
        this._onChannelOpen()
      }
      this._channel.onclose = () => {
        this._onChannelClose()
      }
      this._channel.onerror = err => {
        //this.destroy(errCode(err, 'ERR_DATA_CHANNEL'))
      }
  
      // HACK: Chrome will sometimes get stuck in readyState "closing", let's check for this condition
      // https://bugs.chromium.org/p/chromium/issues/detail?id=882743
      let isClosing = false
      this._closingInterval = setInterval(() => { // No "onclosing" event
        if (this._channel && this._channel.readyState === 'closing') {
          if (isClosing) this._onChannelClose() // closing timed out: equivalent to onclose firing
          isClosing = true
        } else {
          isClosing = false
        }
      }, CHANNEL_CLOSING_TIMEOUT)
    }
  
    _read () {}
  
    _write (chunk, encoding, cb) {
      if (this.destroyed) return cb(errCode(new Error('cannot write after peer is destroyed'), 'ERR_DATA_CHANNEL'))
  
      if (this._connected) {
        try {
          this.send(chunk)
        } catch (err) {
          return this.destroy(errCode(err, 'ERR_DATA_CHANNEL'))
        }
        if (this._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) {
          this._debug('start backpressure: bufferedAmount %d', this._channel.bufferedAmount)
          this._cb = cb
        } else {
          cb(null)
        }
      } else {
        this._debug('write before connect')
        this._chunk = chunk
        this._cb = cb
      }
    }
  
    // When stream finishes writing, close socket. Half open connections are not
    // supported.
    _onFinish () {
      if (this.destroyed) return
  
      // Wait a bit before destroying so the socket flushes.
      // TODO: is there a more reliable way to accomplish this?
      const destroySoon = () => {
        setTimeout(() => this.destroy(), 1000)
      }
  
      if (this._connected) {
        destroySoon()
      } else {
        this.once('connect', destroySoon)
      }
    }
  
    _startIceCompleteTimeout () {
      if (this.destroyed) return
      if (this._iceCompleteTimer) return
      this._debug('started iceComplete timeout')
      this._iceCompleteTimer = setTimeout(() => {
        if (!this._iceComplete) {
          this._iceComplete = true
          this._debug('iceComplete timeout completed')
          this.emit('iceTimeout')
          this.emit('_iceComplete')
        }
      }, this.iceCompleteTimeout)
    }
  
    _createOffer () {
      if (this.destroyed) return
  
      this._pc.createOffer(this.offerOptions)
        .then(offer => {
          if (this.destroyed) return
          if (!this.trickle && !this.allowHalfTrickle) offer.sdp = filterTrickle(offer.sdp)
          offer.sdp = this.sdpTransform(offer.sdp)
  
          const sendOffer = () => {
            if (this.destroyed) return
            const signal = this._pc.localDescription || offer
            this._debug('signal')
            this.emit('signal', {
              type: signal.type,
              sdp: signal.sdp,
              extension : this.extension
            })
          }
  
          const onSuccess = () => {
            this._debug('createOffer success')
            if (this.destroyed) return
            if (this.trickle || this._iceComplete) sendOffer()
            else this.once('_iceComplete', sendOffer) // wait for candidates
          }
  
          const onError = err => {
            this.destroy(errCode(err, 'ERR_SET_LOCAL_DESCRIPTION'))
          }
  
          this._pc.setLocalDescription(offer)
            .then(onSuccess)
            .catch(onError)
        })
        .catch(err => {
          this.destroy(errCode(err, 'ERR_CREATE_OFFER'))
        })
    }
  
    _requestMissingTransceivers () {
      if (this._pc.getTransceivers) {
        this._pc.getTransceivers().forEach(transceiver => {
          if (!transceiver.mid && transceiver.sender && transceiver.sender.track && !transceiver.requested) {
            transceiver.requested = true // HACK: Safari returns negotiated transceivers with a null mid
            this.addTransceiver(transceiver.sender.track.kind)
          }
        })
      }
    }
  
    _createAnswer () {
      if (this.destroyed) return
  
      this._pc.createAnswer(this.answerOptions)
        .then(answer => {
          if (this.destroyed) return
          if (!this.trickle && !this.allowHalfTrickle) answer.sdp = filterTrickle(answer.sdp)
          answer.sdp = this.sdpTransform(answer.sdp)
  
          const sendAnswer = () => {
            if (this.destroyed) return
            const signal = this._pc.localDescription || answer
            this._debug('signal')
            this.emit('signal', {
              type: signal.type,
              sdp: signal.sdp,
              extension: this.extension
            })
            //if (!this.initiator) this._requestMissingTransceivers() //ios unSupport
          }
  
          const onSuccess = () => {
            if (this.destroyed) return
            if (this.trickle || this._iceComplete) sendAnswer()
            else this.once('_iceComplete', sendAnswer)
          }
  
          const onError = err => {
            this.destroy(errCode(err, 'ERR_SET_LOCAL_DESCRIPTION'))
          }
  
          this._pc.setLocalDescription(answer)
            .then(onSuccess)
            .catch(onError)
        })
        .catch(err => {
          this.destroy(errCode(err, 'ERR_CREATE_ANSWER'))
        })
    }
  
    _onConnectionStateChange () {
      if (this.destroyed) return
      if (this._pc.connectionState === 'failed') {
        this.destroy(errCode(new Error('Connection failed.'), 'ERR_CONNECTION_FAILURE'))
      }
    }
  
    _onIceStateChange () {
      if (this.destroyed) return
      const iceConnectionState = this._pc.iceConnectionState
      const iceGatheringState = this._pc.iceGatheringState
  
      this._debug(
        'iceStateChange (connection: %s) (gathering: %s)',
        iceConnectionState,
        iceGatheringState
      )
      this.emit('iceStateChange', iceConnectionState, iceGatheringState)
  
      if (iceConnectionState === 'connected' || iceConnectionState === 'completed') {
        this._pcReady = true
        this._maybeReady()
      }
      if (iceConnectionState === 'failed') {
        this.destroy(errCode(new Error('Ice connection failed.'), 'ERR_ICE_CONNECTION_FAILURE'))
      }
      if (iceConnectionState === 'closed') {
        this.destroy(errCode(new Error('Ice connection closed.'), 'ERR_ICE_CONNECTION_CLOSED'))
      }
    }
  
    getStats (cb) {
      // statreports can come with a value array instead of properties
      const flattenValues = report => {
        if (Object.prototype.toString.call(report.values) === '[object Array]') {
          report.values.forEach(value => {
            Object.assign(report, value)
          })
        }
        return report
      }
  
      // Promise-based getStats() (standard)
      if (this._pc.getStats.length === 0 || this._isReactNativeWebrtc) {
        this._pc.getStats()
          .then(res => {
            const reports = []
            res.forEach(report => {
              reports.push(flattenValues(report))
            })
            cb(null, reports)
          }, err => cb(err))
  
        // Single-parameter callback-based getStats() (non-standard)
      } else if (this._pc.getStats.length > 0) {
        this._pc.getStats(res => {
          // If we destroy connection in `connect` callback this code might happen to run when actual connection is already closed
          if (this.destroyed) return
  
          const reports = []
          res.result().forEach(result => {
            const report = {}
            result.names().forEach(name => {
              report[name] = result.stat(name)
            })
            report.id = result.id
            report.type = result.type
            report.timestamp = result.timestamp
            reports.push(flattenValues(report))
          })
          cb(null, reports)
        }, err => cb(err))
  
        // Unknown browser, skip getStats() since it's anyone's guess which style of
        // getStats() they implement.
      } else {
        cb(null, [])
      }
    }
  
    _maybeReady () {
      this._debug('maybeReady pc %s channel %s', this._pcReady, this._channelReady)
      if (this._connected || this._connecting || !this._pcReady || !this._channelReady) return
  
      this._connecting = true
  
      // HACK: We can't rely on order here, for details see https://github.com/js-platform/node-webrtc/issues/339
      const findCandidatePair = () => {
        if (this.destroyed) return
  
        this.getStats((err, items) => {
          if (this.destroyed) return
  
          // Treat getStats error as non-fatal. It's not essential.
          if (err) items = []
  
          const remoteCandidates = {}
          const localCandidates = {}
          const candidatePairs = {}
          let foundSelectedCandidatePair = false
  
          items.forEach(item => {
            // TODO: Once all browsers support the hyphenated stats report types, remove
            // the non-hypenated ones
            if (item.type === 'remotecandidate' || item.type === 'remote-candidate') {
              remoteCandidates[item.id] = item
            }
            if (item.type === 'localcandidate' || item.type === 'local-candidate') {
              localCandidates[item.id] = item
            }
            if (item.type === 'candidatepair' || item.type === 'candidate-pair') {
              candidatePairs[item.id] = item
            }
          })
  
          const setSelectedCandidatePair = selectedCandidatePair => {
            foundSelectedCandidatePair = true
  
            let local = localCandidates[selectedCandidatePair.localCandidateId]
  
            if (local && (local.ip || local.address)) {
              // Spec
              this.localAddress = local.ip || local.address
              this.localPort = Number(local.port)
            } else if (local && local.ipAddress) {
              // Firefox
              this.localAddress = local.ipAddress
              this.localPort = Number(local.portNumber)
            } else if (typeof selectedCandidatePair.googLocalAddress === 'string') {
              // TODO: remove this once Chrome 58 is released
              local = selectedCandidatePair.googLocalAddress.split(':')
              this.localAddress = local[0]
              this.localPort = Number(local[1])
            }
            if (this.localAddress) {
              this.localFamily = this.localAddress.includes(':') ? 'IPv6' : 'IPv4'
            }
  
            let remote = remoteCandidates[selectedCandidatePair.remoteCandidateId]
  
            if (remote && (remote.ip || remote.address)) {
              // Spec
              this.remoteAddress = remote.ip || remote.address
              this.remotePort = Number(remote.port)
            } else if (remote && remote.ipAddress) {
              // Firefox
              this.remoteAddress = remote.ipAddress
              this.remotePort = Number(remote.portNumber)
            } else if (typeof selectedCandidatePair.googRemoteAddress === 'string') {
              // TODO: remove this once Chrome 58 is released
              remote = selectedCandidatePair.googRemoteAddress.split(':')
              this.remoteAddress = remote[0]
              this.remotePort = Number(remote[1])
            }
            if (this.remoteAddress) {
              this.remoteFamily = this.remoteAddress.includes(':') ? 'IPv6' : 'IPv4'
            }
  
            this._debug(
              'connect local: %s:%s remote: %s:%s',
              this.localAddress,
              this.localPort,
              this.remoteAddress,
              this.remotePort
            )
          }
  
          items.forEach(item => {
            // Spec-compliant
            if (item.type === 'transport' && item.selectedCandidatePairId) {
              setSelectedCandidatePair(candidatePairs[item.selectedCandidatePairId])
            }
  
            // Old implementations
            if (
              (item.type === 'googCandidatePair') ||
              ((item.type === 'candidatepair' || item.type === 'candidate-pair') && item.selected)
            ) {
              setSelectedCandidatePair(item)
            }
          })
  
          // Ignore candidate pair selection in browsers like Safari 11 that do not have any local or remote candidates
          // But wait until at least 1 candidate pair is available
          if (!foundSelectedCandidatePair && (!Object.keys(candidatePairs).length || Object.keys(localCandidates).length)) {
            setTimeout(findCandidatePair, 100)
            return
          } else {
            this._connecting = false
            this._connected = true
          }
  
          if (this._chunk) {
            try {
              this.send(this._chunk)
            } catch (err) {
              return this.destroy(errCode(err, 'ERR_DATA_CHANNEL'))
            }
            this._chunk = null
            this._debug('sent chunk from "write before connect"')
  
            const cb = this._cb
            this._cb = null
            cb(null)
          }
  
          // If `bufferedAmountLowThreshold` and 'onbufferedamountlow' are unsupported,
          // fallback to using setInterval to implement backpressure.
          if (typeof this._channel.bufferedAmountLowThreshold !== 'number') {
            this._interval = setInterval(() => this._onInterval(), 150)
            if (this._interval.unref) this._interval.unref()
          }
  
          this._debug('connect')
          this.emit('connect')
        })
      }
      findCandidatePair()
    }
  
    _onInterval () {
      if (!this._cb || !this._channel || this._channel.bufferedAmount > MAX_BUFFERED_AMOUNT) {
        return
      }
      this._onChannelBufferedAmountLow()
    }
  
    _onSignalingStateChange () {
      if (this.destroyed) return
  
      if (this._pc.signalingState === 'stable') {
        this._isNegotiating = false
  
        // HACK: Firefox doesn't yet support removing tracks when signalingState !== 'stable'
        this._debug('flushing sender queue', this._sendersAwaitingStable)
        this._sendersAwaitingStable.forEach(sender => {
          this._pc.removeTrack(sender)
          this._queuedNegotiation = true
        })
        this._sendersAwaitingStable = []
  
        if (this._queuedNegotiation) {
          this._debug('flushing negotiation queue')
          this._queuedNegotiation = false
          this._needsNegotiation() // negotiate again
        } else {
          this._debug('negotiated')
          this.emit('negotiated')
        }
      }
  
      this._debug('signalingStateChange %s', this._pc.signalingState)
      this.emit('signalingStateChange', this._pc.signalingState)
    }
  
    _onIceCandidate (event) {
      if (this.destroyed) return
      if (event.candidate && this.trickle) {
        this.emit('signal', {
          type: 'candidate',
          candidate: {
            candidate: event.candidate.candidate,
            sdpMLineIndex: event.candidate.sdpMLineIndex,
            sdpMid: event.candidate.sdpMid
          }
        })
      } else if (!event.candidate && !this._iceComplete) {
        this._iceComplete = true
        this.emit('_iceComplete')
      }
      // as soon as we've received one valid candidate start timeout
      if (event.candidate) {
        this._startIceCompleteTimeout()
      }
    }
  
    _onChannelMessage (event) {
      if (this.destroyed) return
      let data = event.data
      if (data instanceof ArrayBuffer) data = Buffer.from(data)
      this.push(data)
    }
  
    _onChannelBufferedAmountLow () {
      if (this.destroyed || !this._cb) return
      this._debug('ending backpressure: bufferedAmount %d', this._channel.bufferedAmount)
      const cb = this._cb
      this._cb = null
      cb(null)
    }
  
    _onChannelOpen () {
      if (this._connected || this.destroyed) return
      this._debug('on channel open')
      this._channelReady = true
      this._maybeReady()
    }
  
    _onChannelClose () {
      if (this.destroyed) return
      this._debug('on channel close')
      this.destroy()
    }
  
    _onTrack (event) {
      if (this.destroyed) return
  
      event.streams.forEach(eventStream => {
        this._debug('on track')
        this.emit('track', event.track, eventStream)
  
        this._remoteTracks.push({
          track: event.track,
          stream: eventStream
        })
  
        if (this._remoteStreams.some(remoteStream => {
          return remoteStream.id === eventStream.id
        })) return // Only fire one 'stream' event, even though there may be multiple tracks per stream
  
        this._remoteStreams.push(eventStream)
        queueMicrotask(() => {
          this._debug('on stream')
          this.emit('stream', eventStream) // ensure all tracks have been added
        })
      })
    }
  
    _debug () {
      const args = [].slice.call(arguments)
      args[0] = '[' + this._id + '] ' + args[0]
      debug.apply(null, args)
    }
  }
  
  Peer.WEBRTC_SUPPORT = !!getBrowserRTC()
  
  /**
   * Expose peer and data channel config for overriding all Peer
   * instances. Otherwise, just set opts.config or opts.channelConfig
   * when constructing a Peer.
   */
  Peer.config = {
    iceServers: [
      {
        urls: [
          'stun:stun.l.google.com:19302',
          'stun:global.stun.twilio.com:3478'
        ]
      }
    ],
    sdpSemantics: 'unified-plan'
  }
  
  Peer.channelConfig = {}
  
  module.exports = Peer
  