feat: 扩展 WebRTC 统计信息
在 StatsBar 中新增延迟、抖动、丢帧、解码耗时、上传速率和丢包率等指标展示。 重构速率计算逻辑,并从 inbound-rtp 和 candidate-pair 报告中提取相关数据。
This commit is contained in:
@@ -6,8 +6,14 @@ import { store } from '../store/controllerStore';
|
||||
<div class="stats-bar">
|
||||
<span>分辨率: <b>{{ store.stats ? store.stats.width + '×' + store.stats.height : '-' }}</b></span>
|
||||
<span>帧率: <b>{{ store.stats ? store.stats.fps : '-' }}</b></span>
|
||||
<span>解码: <b>{{ store.stats ? store.stats.codec : '-' }}</b></span>
|
||||
<span>编码: <b>{{ store.stats ? store.stats.codec : '-' }}</b></span>
|
||||
<span>延迟: <b>{{ store.stats ? store.stats.latency : '-' }}</b></span>
|
||||
<span>抖动: <b>{{ store.stats ? store.stats.jitter : '-' }}</b></span>
|
||||
<span>丢帧: <b>{{ store.stats ? store.stats.framesDropped : '-' }}</b></span>
|
||||
<span>解码耗时: <b>{{ store.stats ? store.stats.decodeMs : '-' }}</b></span>
|
||||
<span>↑上传: <b>{{ store.stats ? store.stats.upSpeed : '-' }}</b></span>
|
||||
<span>↓下载: <b>{{ store.stats ? store.stats.downSpeed : '-' }}</b></span>
|
||||
<span>丢包: <b>{{ store.stats ? store.stats.packetLoss : '-' }}</b></span>
|
||||
<span>时长: <b>{{ store.stats ? store.stats.duration : '-' }}</b></span>
|
||||
<span>控制通道: <b>{{ store.dataChannelOpen ? '已连接' : '未连接' }}</b></span>
|
||||
<span>目标: <b>{{ store.targetDeviceId || '-' }}</b></span>
|
||||
|
||||
@@ -23,6 +23,8 @@ export class WebRtcController {
|
||||
this._statsTimer = null;
|
||||
this._prevBytes = 0;
|
||||
this._prevTs = 0;
|
||||
this._prevSentBytes = 0;
|
||||
this._prevSentTs = 0;
|
||||
this._connectedAt = 0;
|
||||
this._gatheredRelay = false;
|
||||
this._gatheredSrflx = false;
|
||||
@@ -239,7 +241,10 @@ export class WebRtcController {
|
||||
|
||||
async collectStats() {
|
||||
const stats = await this.pc.getStats();
|
||||
let width = '-', height = '-', fps = '-', codec = '-', bytesReceived = 0;
|
||||
let width = '-', height = '-', fps = '-', codec = '-';
|
||||
let bytesReceived = 0, bytesSent = 0;
|
||||
let jitter = 0, framesDropped = 0, packetsLost = 0, packetsReceived = 0;
|
||||
let framesDecoded = 0, totalDecodeTime = 0, rtt = null;
|
||||
const codecs = {};
|
||||
stats.forEach((r) => {
|
||||
if (r.type === 'inbound-rtp' && r.kind === 'video') {
|
||||
@@ -247,30 +252,63 @@ export class WebRtcController {
|
||||
height = r.frameHeight ?? '-';
|
||||
fps = r.framesPerSecond ?? '-';
|
||||
if (r.codecId && codecs[r.codecId]) codec = codecs[r.codecId];
|
||||
jitter = r.jitter ?? 0; // 接收抖动(秒)
|
||||
framesDropped = r.framesDropped ?? 0; // 累计丢帧数
|
||||
packetsLost = r.packetsLost ?? 0; // 累计丢包数
|
||||
packetsReceived = r.packetsReceived ?? 0; // 累计接收包数
|
||||
framesDecoded = r.framesDecoded ?? 0; // 累计解码帧数
|
||||
totalDecodeTime = r.totalDecodeTime ?? 0; // 累计解码耗时(秒)
|
||||
} else if (r.type === 'codec' && r.mimeType && r.mimeType.startsWith('video/')) {
|
||||
codecs[r.id] = r.mimeType.substring(6);
|
||||
} else if (r.type === 'candidate-pair' && r.nominated) {
|
||||
bytesReceived = r.bytesReceived ?? 0;
|
||||
bytesSent = r.bytesSent ?? 0;
|
||||
if (typeof r.currentRoundTripTime === 'number') rtt = r.currentRoundTripTime; // 往返时延(秒)
|
||||
}
|
||||
});
|
||||
const now = Date.now();
|
||||
// 下载速率
|
||||
let downSpeed = '-';
|
||||
if (this._prevTs) {
|
||||
const dt = (now - this._prevTs) / 1000;
|
||||
if (dt > 0) {
|
||||
const bps = (bytesReceived - this._prevBytes) / dt;
|
||||
downSpeed = bps >= 1048576 ? (bps / 1048576).toFixed(1) + ' MB/s'
|
||||
: bps >= 1024 ? (bps / 1024).toFixed(1) + ' KB/s' : bps.toFixed(0) + ' B/s';
|
||||
downSpeed = formatSpeed(bps);
|
||||
}
|
||||
}
|
||||
this._prevBytes = bytesReceived;
|
||||
this._prevTs = now;
|
||||
// 上传速率
|
||||
let upSpeed = '-';
|
||||
if (this._prevSentTs) {
|
||||
const dt = (now - this._prevSentTs) / 1000;
|
||||
if (dt > 0) {
|
||||
const bps = (bytesSent - this._prevSentBytes) / dt;
|
||||
upSpeed = formatSpeed(bps);
|
||||
}
|
||||
}
|
||||
this._prevSentBytes = bytesSent;
|
||||
this._prevSentTs = now;
|
||||
let duration = '-';
|
||||
if (this._connectedAt) {
|
||||
const secs = Math.floor((now - this._connectedAt) / 1000);
|
||||
duration = String(Math.floor(secs / 60)).padStart(2, '0') + ':' + String(secs % 60).padStart(2, '0');
|
||||
}
|
||||
return { width, height, fps, codec, downSpeed, duration, dcState: this.dataChannel ? this.dataChannel.readyState : 'none' };
|
||||
// 延迟(往返时延)
|
||||
const latency = rtt != null ? (rtt * 1000).toFixed(0) + ' ms' : '-';
|
||||
// 抖动
|
||||
const jitterMs = jitter ? (jitter * 1000).toFixed(1) + ' ms' : '-';
|
||||
// 解码耗时(平均每帧解码时间)
|
||||
const decodeMs = (framesDecoded > 0 && totalDecodeTime > 0)
|
||||
? (totalDecodeTime / framesDecoded * 1000).toFixed(1) + ' ms' : '-';
|
||||
// 丢包率
|
||||
const totalPackets = packetsLost + packetsReceived;
|
||||
const lossRate = totalPackets > 0 ? (packetsLost / totalPackets * 100).toFixed(1) + '%' : '-';
|
||||
return {
|
||||
width, height, fps, codec, downSpeed, upSpeed, duration,
|
||||
latency, jitter: jitterMs, framesDropped, decodeMs, packetLoss: lossRate,
|
||||
dcState: this.dataChannel ? this.dataChannel.readyState : 'none',
|
||||
};
|
||||
}
|
||||
|
||||
async close() {
|
||||
@@ -283,3 +321,10 @@ export class WebRtcController {
|
||||
this._pendingCandidates = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 将字节/秒格式化为可读速率
|
||||
function formatSpeed(bps) {
|
||||
if (!bps || bps <= 0) return '0 B/s';
|
||||
return bps >= 1048576 ? (bps / 1048576).toFixed(1) + ' MB/s'
|
||||
: bps >= 1024 ? (bps / 1024).toFixed(1) + ' KB/s' : bps.toFixed(0) + ' B/s';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user