diff --git a/WebRTCControllerWeb/src/components/StatsBar.vue b/WebRTCControllerWeb/src/components/StatsBar.vue
index d996132..c6f974f 100644
--- a/WebRTCControllerWeb/src/components/StatsBar.vue
+++ b/WebRTCControllerWeb/src/components/StatsBar.vue
@@ -6,8 +6,14 @@ import { store } from '../store/controllerStore';
分辨率: {{ store.stats ? store.stats.width + '×' + store.stats.height : '-' }}
帧率: {{ store.stats ? store.stats.fps : '-' }}
- 解码: {{ store.stats ? store.stats.codec : '-' }}
+ 编码: {{ store.stats ? store.stats.codec : '-' }}
+ 延迟: {{ store.stats ? store.stats.latency : '-' }}
+ 抖动: {{ store.stats ? store.stats.jitter : '-' }}
+ 丢帧: {{ store.stats ? store.stats.framesDropped : '-' }}
+ 解码耗时: {{ store.stats ? store.stats.decodeMs : '-' }}
+ ↑上传: {{ store.stats ? store.stats.upSpeed : '-' }}
↓下载: {{ store.stats ? store.stats.downSpeed : '-' }}
+ 丢包: {{ store.stats ? store.stats.packetLoss : '-' }}
时长: {{ store.stats ? store.stats.duration : '-' }}
控制通道: {{ store.dataChannelOpen ? '已连接' : '未连接' }}
目标: {{ store.targetDeviceId || '-' }}
diff --git a/WebRTCControllerWeb/src/services/WebRtcController.js b/WebRTCControllerWeb/src/services/WebRtcController.js
index 565f70b..7dbc5fe 100644
--- a/WebRTCControllerWeb/src/services/WebRtcController.js
+++ b/WebRTCControllerWeb/src/services/WebRtcController.js
@@ -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';
+}