11

Is there any way to get information about the type of connection used in WebRTC in a programmatic way?

For example in my app I use local connections as well as STUN and TURN. From the ICE candidates I can gather if the type of the candidates is host or relay, and on the server I can see if the connection is attempted via STUN (connection initiation) or TURN (steady stream during connection).

Up until now I could not find a way to access the information about the finally used type of connection in the browser. There are the candidates, the browser stops gathering and then there is a working connection. Looking through the events I couldn't find any information though.

I know that Chrome supports getStats() on the peerconnection, which allows me access to most of the information found in chrome://webrtc-internals, however I didn't find this information there either.

Is there any way to access this information from javascript?

Thank you very much.

Duglum
  • 113
  • 1
  • 6
  • That's a good question. I looked at the spec very carefully but coumdn't found a way to determine the type of the connection. I guess it's not possibles but I cannot be sure. – Svetlin Mladenov Dec 11 '14 at 15:14
  • The spec is pretty lacking. It's possible to figure it out on Chrome using getStats(), looking for the googCandidatePair results with googActiveConnection === "true". The local and remote addresses will tell you which candidate was chosen, and use this to look up the type of candidate (assuming you keep all local and remote candidate types). – Bradley T. Hughes Dec 29 '14 at 21:48
  • Thanks for the hints, but unfortunately I couldn't find a way to access googActiveConnection from javascript. There seems to be some work in progress in Issue 2031 ([link](https://code.google.com/p/webrtc/issues/detail?id=2031)) to implement statistics according to a spec from September 2014 ([link](http://w3c.github.io/webrtc-stats/)), so maybe help is on the way – Duglum Jan 08 '15 at 09:48
  • take a look at this library: https://github.com/muaz-khan/getStats – Humoyun Ahmad Apr 25 '20 at 14:14
  • And this: https://testrtc.com/find-webrtc-active-connection – Humoyun Ahmad Dec 02 '20 at 01:59

4 Answers4

6

According to the specification, which is currently implemented in Firefox, but not in Chrome, you can indeed suss out the active candidate from the statistics available for candidate pairs, which are:

dictionary RTCIceCandidatePairStats : RTCStats {
    DOMString                     transportId;
    DOMString                     localCandidateId;
    DOMString                     remoteCandidateId;
    RTCStatsIceCandidatePairState state;
    unsigned long long            priority;
    boolean                       nominated;
    boolean                       writable;
    boolean                       readable;
    unsigned long long            bytesSent;
    unsigned long long            bytesReceived;
    double                        roundTripTime;
    double                        availableOutgoingBitrate;
    double                        availableIncomingBitrate;
};

Combined with the stats on the individual candidates:

dictionary RTCIceCandidateAttributes : RTCStats {
    DOMString                ipAddress;
    long                     portNumber;
    DOMString                transport;
    RTCStatsIceCandidateType candidateType;
    long                     priority;
    DOMString                addressSourceUrl;
};

Use peerConnection.getStats() to look for an ice candidate pair that is both nominated and has succeeded:

pc.getStats(null))
.then(function(stats) {
  return Object.keys(stats).forEach(function(key) {
    if (stats[key].type == "candidatepair" &&
        stats[key].nominated && stats[key].state == "succeeded") {
      var remote = stats[stats[key].remoteCandidateId];
      console.log("Connected to: " + remote.ipAddress +":"+
                  remote.portNumber +" "+ remote.transport +
                  " "+ remote.candidateType);
    }
  });
})
.catch(function(e) { console.log(e.name); });

This might output something like:

Connected to: 192.168.1.2:49190 udp host

which you could test against the LAN range. If instead it returned something like:

Connected to: 24.57.143.7:61102 udp relayed

then you'd have a TURN connection.

Here's a jsfiddle that shows this (requires Firefox Developer Edition for other reasons).

jib
  • 40,579
  • 17
  • 100
  • 158
  • This doesn't work in Chromium, if you observe the state of the `succeeded` candidate pair. It switches back to `in-progress` every few seconds. So there is a small chance that you won't get any candidate pair when iterating over the stats object this way. Instead you have to look for the stat with type `transport`. Look at my answer below for a better explanation. – Jespertheend Oct 02 '20 at 13:11
6

It took me a long time to get this right, so hopefully this helps someone.

The new way

You can now get the selected candidate pair from the RTCPeerConnection without the stats api:

const pair = rtcConnection.sctp.transport.iceTransport.getSelectedCandidatePair();
console.log(pair.remote.type);

At the time of writing (October 2, 2020) this only works in Chromium however. You can still use the stats api for other browsers. Also note the comment below by jib that this only works if you have a DataChannel.

For browsers without getSelectedCandidatePair() support

According to https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidatePairStats (at the bottom of the page at the selected property.

The spec-compliant way to determine the selected candidate pair is to look for a stats object of type transport, which is an RTCTransportStats object. That object's selectedCandidatePairId property indicates whether or not the specified transport is the one being used.

So trying to find the selected pair using stat.nominated && stats.state == "succeeded" is not the right way to do it.

Instead, you can get it by looking at the selected pair in the transport stat. Firefox doesn't support this, but fortunately there is a non-standard selected property in candidate pairs for firefox.

const stats = await rtcConnection.getStats();
if(stats){
    let selectedPairId = null;
    for(const [key, stat] of stats){
        if(stat.type == "transport"){
            selectedPairId = stat.selectedCandidatePairId;
            break;
        }
    }
    let candidatePair = stats.get(selectedPairId);
    if(!candidatePair){
        for(const [key, stat] of stats){
            if(stat.type == "candidate-pair" && stat.selected){
                candidatePair = stat;
                break;
            }
        }
    }

    if(candidatePair){
        for(const [key, stat] of stats){
            if(key == candidatePair.remoteCandidateId){
                return stat.candidateType;
            }
        }
    }
}
Jespertheend
  • 1,814
  • 19
  • 27
  • Great answer! Note `key` == `stat.id`, so you can look up `candidatePair` directly wo/2nd `for` loop: `candidatePair = stats.get(stat.selectedCandidatePairId)`. Also, `remoteId` appears unused. – jib Oct 04 '20 at 14:26
  • 1
    It also might be worth pointing out that [`sctp`](https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-sctp) will be `null` unless you've negotiated data channels, so you might have to find the transport on a transceiver if you're only doing media. – jib Oct 04 '20 at 14:28
  • @jib ah neat, I wasn't aware of `stats.get()` I always found it weird you had to iterate over the object to get a key haha. Also thanks for the headsup on sctp being null. I'm only using data channels (no media at all) so this value has never been null for me. – Jespertheend Oct 04 '20 at 14:32
5

jib's answer from March 2015 is very helpful, but doesn't work with Firefox v65 nor Chrome v72 (on Windows) in March 2019. Two updates are needed:

1) The "stats" value has type RTCStatsReport in both browsers now, and it's an iterable object with no keys. So, iterate it with forEach(report => {...}) and "report" will be an object with keys like those that jib shows for "stats".

2) "candidatepair" isn't a valid value of report.type but "candidate-pair" is.

DavidP
  • 61
  • 1
  • 2
4

Thanks to @DavidP and a more in-depth answer I wrote the code below to get the ICE Candidates type.

Updated Code: Getting the ICE candidates with conncectionStats

    function getCandidateIds(stats) {
        let ids = {}
        stats.forEach(report => {
            if (report.type == "candidate-pair" && report.nominated && report.state == "succeeded") {
                //console.log("Found IDs")
                ids = {
                    localId: report.localCandidateId,
                    remoteId: report.remoteCandidateId
                }
            }
        });
        return ids
    }

    function getCandidateInfo(stats, candidateId) {
        let info = null
        stats.forEach(report => {
            if (report.id == candidateId) {
                console.log("Found Candidate")
                info = report
            }
        })
        return info
    }

    async function conncectionStats() {
        const stats = await this.pc.getStats(null)
        const candidates = await this.getCandidateIds(stats)
        console.log("candidates: ", candidates)
        if (candidates !== {}) {
            const localCadidate = await this.getCandidateInfo(stats, candidates.localId)
            const remoteCadidate = await this.getCandidateInfo(stats, candidates.remoteId)
            if (localCadidate !== null && remoteCadidate !== null) {
                return [localCadidate, remoteCadidate]
            }
        }
        // we did not find the candidates for whatever reeason
        return [null, null]
    }

reading out IP:

  let myAddress = ""
  let peerAddress = ""
  if (localCadidate.hasOwnProperty("ip")){
    myAddress = localCadidate.ip
    peerAddress = remoteCadidate.ip
  } else {
    myAddress = localCadidate.address
    peerAddress = remoteCadidate.address
  }

old version:

function getConnectionDetails(pc){
  pc.getStats(null)
  .then(function(stats) {
        stats.forEach(report => {
          if (report.type == "candidate-pair" 
              && report.nominated 
              && report.state == "succeeded")
          {
            console.log( "Local ICE:", report.localCandidateId)
            console.log( "Remote ICE:",report.remoteCandidateId)
            getCandidates(pc, report.localCandidateId, report.remoteCandidateId)
          }
      });
  })
  .catch(function(e) { console.log(e.name); });
};

function getCandidates(pc, localId, remoteId){
  //console.log("looking for candidates")
  pc.getStats(null)
  .then(function(stats) {
        stats.forEach(report => {
          if (report.id == localId) {
              console.log("Local: Type:", report.candidateType," IP:", report.ip)
          } else if (report.id == remoteId){
              console.log("Remote: Type:", report.candidateType," IP:", report.ip)
          }
      })
  })
  .catch(function(e) { console.log(e.name); });
}

You may not need both candidates depending on what information should be extracted.

Noah Studach
  • 405
  • 4
  • 14
  • 1
    you might want report.address || report.ip -- the spec changed over the years. – Philipp Hancke May 07 '19 at 17:23
  • Specs: [RTCIceCandidateStats](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidateStats#Properties) and [all types of RTCStates](https://developer.mozilla.org/en-US/docs/Web/API/RTCStatsType#Values) – Noah Studach May 17 '19 at 16:36