如何获取关于WebRTC PeerConnection连接类型的信息?

11

有没有一种以编程方式获取WebRTC中使用连接类型信息的方法?

例如,在我的应用程序中,我使用本地连接以及STUN和TURN。 从ICE候选者中,我可以了解候选者的类型是主机还是中继,并且在服务器上,我可以看到是否通过STUN(连接启动)或TURN(连接期间稳定流)尝试进行连接。

到目前为止,我无法找到一种访问浏览器中最终使用的连接类型信息的方法。 有了候选者,浏览器停止收集,然后建立工作连接。 虽然浏览事件,但我找不到任何信息。

我知道Chrome支持peerconnection上的getStats(),它允许我访问在chrome:// webrtc-internals中找到的大部分信息,但我在那里也没有找到这些信息。

是否有任何途径从Javascript访问此信息?

非常感谢。


这是一个很好的问题。我仔细查看了规范,但找不到确定连接类型的方法。我猜可能不可能,但我不能确定。 - Svetlin Mladenov
规范非常缺乏。可以使用 getStats() 在 Chrome 上找到 googCandidatePair 结果,查找 googActiveConnection === "true" 的结果。本地和远程地址将告诉您选择了哪个候选项,并使用此信息查找候选项的类型(假设您保留了所有本地和远程候选项类型)。 - Bradley T. Hughes
谢谢您的提示,但不幸的是我无法从JavaScript中访问googActiveConnection。根据2014年9月的规范(链接),问题2031(链接)似乎正在进行一些工作以实现统计信息,因此也许有所帮助。 - Duglum
请查看这个库:https://github.com/muaz-khan/getStats - Humoyun Ahmad
还有这个:https://testrtc.com/find-webrtc-active-connection - Humoyun Ahmad
4个回答

6
根据目前在Firefox中实现但未在Chrome中实现的规范,您可以从可用候选对的统计数据中找出活动候选者。这些统计数据包括:
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;
};

结合个别候选人的统计数据:

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

使用peerConnection.getStats()查找已被提名且成功的ice候选对。
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); });

这可能会输出类似以下内容的结果:
Connected to: 192.168.1.2:49190 udp host

您可以针对局域网范围进行测试。如果返回的结果类似于:

Connected to: 24.57.143.7:61102 udp relayed

那么您将拥有一个TURN连接。

这里有一个jsfiddle演示了这一点(由于其他原因需要使用Firefox Developer Edition)。


如果您观察succeeded候选对的状态,您会发现它在Chromium中无法正常工作。它每隔几秒钟就会切换回“in-progress”。因此,在迭代统计对象时,有很小的机会您无法获得任何候选对。相反,您必须查找类型为“transport”的统计信息。请参阅下面的答案以获得更好的解释。 - Jespertheend

6

我花了很长时间才弄清楚这个问题,希望这能帮助到某些人。

新方法

现在,您可以在没有统计信息 API 的情况下从 RTCPeerConnection 中获取所选的候选对:

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

在撰写本文时(2020年10月2日),这仅在Chromium中有效。您仍然可以在其他浏览器中使用统计API。还请注意以下评论jib,即如果您有一个DataChannel,则只有此功能才能正常工作。
对于不支持getSelectedCandidatePair()的浏览器
根据https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidatePairStats(在页面底部的selected属性).
引用: 规范兼容的确定所选候选者对的方法是查找类型为传输的统计对象,该对象是一个RTCTransportStats对象。该对象的selectedCandidatePairId属性指示指定的传输是否正在使用。
因此,尝试使用stat.nominated && stats.state ==“ succeeded”查找所选对并不是正确的方法。
相反,您可以通过查看transport stat中的所选对来获取它。Firefox不支持此功能,但是幸运的是,Firefox的候选人对中存在一个非标准的selected属性。
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;
            }
        }
    }
}

很棒的答案!请注意 key == stat.id,因此您可以直接查找 candidatePair 而无需第二个 for 循环:candidatePair = stats.get(stat.selectedCandidatePairId)。另外,remoteId 似乎未被使用。 - jib
1
另外值得指出的是,如果您未协商数据通道,sctp 将为 null,因此如果您只进行媒体,您可能需要在传输器上查找传输。 - jib
@jib 哇,真不错,我以前不知道 stats.get() 这个方法,我一直觉得要迭代对象才能取到键名很奇怪哈哈。还有谢谢你提醒 sctp 可能为空的事情,因为我只使用数据通道(根本没有媒体流),所以这个值对我来说从来没有为空过。 - Jespertheend

5

2015年3月Jib的回答非常有帮助,但在2019年3月,在Windows上使用Firefox v65和Chrome v72无法正常工作。需要进行两次更新:

1)现在两个浏览器中,“stats”值的类型为RTCStatsReport,是一个可迭代的对象,没有键。因此,请使用forEach(report => {...})对其进行迭代,而“report”将是一个具有与Jib所显示的“stats”相似的键的对象。

2)“candidatepair”不是报告类型的有效值,而“candidate-pair”是有效值。


4

感谢 @DavidP 和他更详细的回答,我写了下面的代码来获取 ICE Candidates 的类型。

更新代码: 使用 conncectionStats 获取 ICE Candidates

    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]
    }

读取IP:

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

旧版本:

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); });
}

您可能不需要同时使用这两个候选者,具体取决于需要提取的信息。

1
你可能需要使用 report.address || report.ip -- 规范在这些年中已经发生了变化。 - Philipp Hancke
规格:RTCIceCandidateStats所有类型的RTCStates - Noah Studach

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接