您困惑的原因可能是Google Chrome的
getStats()
实现早于标准,并且尚未更新(您提供的示例是特定于Chrome的,因此我推测您正在使用Chrome)。
如果您尝试Firefox,您会发现它实现了符合
标准的
getStats()
(但它还不支持标准中的所有统计信息,而且总体上比Chrome的旧API少)。
由于您没有指定浏览器,我将描述标准,并使用Firefox提供一个示例。您可能已经了解了
getStats()
,但标准可以让您过滤特定的MediaStreamTrack,或传递
null
以获取与连接关联的所有数据。
var pc = new RTCPeerConnection(config)
...
pc.getStats(null, function(stats) { ...}, function(error) { ... });
还有一个更新的 promise 版本。
返回的数据在 stats
中,它是一个大的 snowball 对象,并且每个记录都有唯一的 id。每个记录都有以下基类:
dictionary RTCStats {
DOMHiResTimeStamp timestamp;
RTCStatsType type;
DOMString id;
};
在访问记录时,id
重复了属性名称。这些派生类型在此处描述。
通常需要枚举记录,直到找到感兴趣的RTCStatsType,例如 "inbound-rtp"
,如下所示:
dictionary RTCRTPStreamStats : RTCStats {
DOMString ssrc;
DOMString remoteId;
boolean isRemote = false;
DOMString mediaTrackId;
DOMString transportId;
DOMString codecId;
unsigned long firCount;
unsigned long pliCount;
unsigned long nackCount;
unsigned long sliCount;
};
dictionary RTCInboundRTPStreamStats : RTCRTPStreamStats {
unsigned long packetsReceived;
unsigned long long bytesReceived;
unsigned long packetsLost;
double jitter;
double fractionLost;
};
对于 RTCOutboundRTPStreamStats,也有相应的内容。
您还可以跟随交叉引用到其他记录。任何以Id
结尾的成员都是一个外键,您可以使用它来查找另一条记录。例如,mediaTrackId
链接到 RTCMediaStreamTrackStats,表示该RTP数据所属的轨道。
一个特别棘手的情况是存储在与上述相同字典中的RTCP数据,这意味着您必须检查 isRemote == false
才能知道您正在查看RTP数据而不是RTCP数据。使用 remoteId
查找另一个(请注意,这是最近的更名,因此Firefox仍在此处使用较旧的 remoteId
)。出站RTP的关联RTCP统计信息存储在入站字典中,反之亦然(有道理)。
这里是在 Firefox 中运行的 示例:
var pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection();
var add = (pc, can) => can && pc.addIceCandidate(can).catch(log);
pc1.onicecandidate = e => add(pc2, e.candidate);
pc2.onicecandidate = e => add(pc1, e.candidate);
pc2.oniceconnectionstatechange = () => update(statediv, pc2.iceConnectionState);
pc2.onaddstream = e => v2.srcObject = e.stream;
navigator.mediaDevices.getUserMedia({ video: true })
.then(stream => pc1.addStream(v1.srcObject = stream))
.then(() => pc1.createOffer())
.then(offer => pc1.setLocalDescription(offer))
.then(() => pc2.setRemoteDescription(pc1.localDescription))
.then(() => pc2.createAnswer())
.then(answer => pc2.setLocalDescription(answer))
.then(() => pc1.setRemoteDescription(pc2.localDescription))
.then(() => repeat(10, () => Promise.all([pc1.getStats(), pc2.getStats()])
.then(([s1, s2]) => {
var s = "";
s1.forEach(stat => {
if (stat.type == "outbound-rtp" && !stat.isRemote) {
s += "<h4>Sender side</h4>" + dumpStats(stat);
}
});
s2.forEach(stat => {
if (stat.type == "inbound-rtp" && !stat.isRemote) {
s += "<h4>Receiver side</h4>" + dumpStats(stat);
}
});
update(statsdiv, "<small>"+ s +"</small>");
})))
.catch(failed);
function dumpStats(o) {
var s = "";
if (o.mozAvSyncDelay !== undefined || o.mozJitterBufferDelay !== undefined) {
if (o.mozAvSyncDelay !== undefined) s += "A/V sync: " + o.mozAvSyncDelay + " ms";
if (o.mozJitterBufferDelay !== undefined) {
s += " Jitter buffer delay: " + o.mozJitterBufferDelay + " ms";
}
s += "<br>";
}
s += "Timestamp: "+ new Date(o.timestamp).toTimeString() +" Type: "+ o.type +"<br>";
if (o.ssrc !== undefined) s += "SSRC: " + o.ssrc + " ";
if (o.packetsReceived !== undefined) {
s += "Recvd: " + o.packetsReceived + " packets";
if (o.bytesReceived !== undefined) {
s += " ("+ (o.bytesReceived/1024000).toFixed(2) +" MB)";
}
if (o.packetsLost !== undefined) s += " Lost: "+ o.packetsLost;
} else if (o.packetsSent !== undefined) {
s += "Sent: " + o.packetsSent + " packets";
if (o.bytesSent !== undefined) s += " ("+ (o.bytesSent/1024000).toFixed(2) +" MB)";
} else {
s += "<br><br>";
}
s += "<br>";
if (o.bitrateMean !== undefined) {
s += " Avg. bitrate: "+ (o.bitrateMean/1000000).toFixed(2) +" Mbps";
if (o.bitrateStdDev !== undefined) {
s += " ("+ (o.bitrateStdDev/1000000).toFixed(2) +" StdDev)";
}
if (o.discardedPackets !== undefined) {
s += " Discarded packts: "+ o.discardedPackets;
}
}
s += "<br>";
if (o.framerateMean !== undefined) {
s += " Avg. framerate: "+ (o.framerateMean).toFixed(2) +" fps";
if (o.framerateStdDev !== undefined) {
s += " ("+ o.framerateStdDev.toFixed(2) +" StdDev)";
}
}
if (o.droppedFrames !== undefined) s += " Dropped frames: "+ o.droppedFrames;
if (o.jitter !== undefined) s += " Jitter: "+ o.jitter;
return s;
}
var wait = ms => new Promise(r => setTimeout(r, ms));
var repeat = (ms, func) => new Promise(r => (setInterval(func, ms), wait(ms).then(r)));
var log = msg => div.innerHTML = div.innerHTML + msg +"<br>";
var update = (div, msg) => div.innerHTML = msg;
var failed = e => log(e.name +": "+ e.message +", line "+ e.lineNumber);
<table><tr><td>
<video id="v1" width="124" height="75" autoplay></video><br>
<video id="v2" width="124" height="75" autoplay></video><br>
<div id="statediv"></div></td>
<td><div id="div"></div><br><div id="statsdiv"></div></td>
</tr></table>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
要查看支持的内容,请执行 stats.forEach(stat => console.log(JSON.stringify(stat)))
以转储所有内容。难以阅读,但所有内容都在其中。
我认为很快就会计划为adapter.js提供polyfill,以填补直到Chrome更新其实现的差距。
更新: 我已更新示例以使用新的maplike语法,并更改了类型名称以包括破折号,以符合最新规范。