feat: 连接显示更多信息
This commit is contained in:
@@ -146,6 +146,7 @@ class WebRtcController {
|
||||
// ignore: avoid_print
|
||||
print('ICE connection state: $state');
|
||||
if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) {
|
||||
_connectedAt = DateTime.now().millisecondsSinceEpoch;
|
||||
onConnectionEstablished?.call();
|
||||
} else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) {
|
||||
onIceDisconnected?.call('ICE 连接已断开');
|
||||
@@ -191,7 +192,19 @@ class WebRtcController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 上一次统计采样的字节数与时间戳,用于计算每秒网速。
|
||||
int _prevBytesReceived = 0;
|
||||
int _prevBytesSent = 0;
|
||||
int _prevStatsTimestamp = 0;
|
||||
|
||||
/// 连接建立时的时间戳(毫秒),用于计算连接时长。
|
||||
int? _connectedAt;
|
||||
|
||||
/// 采集连接统计信息,回调格式化后的文本(对应 Android 端 updateStats)。
|
||||
///
|
||||
/// 包含:分辨率/帧率/延迟/解码格式、抖动、丢包率、丢帧、
|
||||
/// 每秒上下行网速、连接类型(P2P/TURN)+候选协议、连接时长、
|
||||
/// 控制 DataChannel 状态与收发消息数。
|
||||
Future<String> getStatsText() async {
|
||||
if (_pc == null) return '';
|
||||
final reports = await _pc!.getStats();
|
||||
@@ -201,14 +214,41 @@ class WebRtcController {
|
||||
dynamic fps = '-';
|
||||
dynamic delay = '-';
|
||||
dynamic codec = '-';
|
||||
dynamic jitter = '-';
|
||||
dynamic lossRate = '-';
|
||||
dynamic framesDropped = '-';
|
||||
|
||||
int bytesReceived = 0;
|
||||
int bytesSent = 0;
|
||||
String? localCandidateId;
|
||||
String? remoteCandidateId;
|
||||
bool hasNominatedPair = false;
|
||||
|
||||
// 控制 DataChannel 统计(仅统计与对端的控制通道)。
|
||||
int dcMessagesSent = 0;
|
||||
int dcMessagesReceived = 0;
|
||||
|
||||
for (final report in reports) {
|
||||
final values = report.values;
|
||||
if (report.type == 'inbound-rtp' && values['kind'] == 'video') {
|
||||
final type = report.type;
|
||||
if (type == 'inbound-rtp' && values['kind'] == 'video') {
|
||||
width = values['frameWidth'] ?? '-';
|
||||
height = values['frameHeight'] ?? '-';
|
||||
fps = values['framesPerSecond'] ?? '-';
|
||||
|
||||
final j = values['jitter'];
|
||||
if (j is num) jitter = (j * 1000).toStringAsFixed(0);
|
||||
|
||||
final pr = values['packetsReceived'];
|
||||
final pl = values['packetsLost'];
|
||||
if (pr is num && pl is num) {
|
||||
final total = pr + pl;
|
||||
lossRate = total > 0 ? ((pl / total) * 100).toStringAsFixed(1) : '0.0';
|
||||
}
|
||||
|
||||
final fd = values['framesDropped'];
|
||||
if (fd is num) framesDropped = fd;
|
||||
|
||||
final codecId = values['codecId'];
|
||||
if (codecId != null) {
|
||||
final codecReport = reports.where((r) => r.id == codecId).firstOrNull;
|
||||
@@ -217,18 +257,136 @@ class WebRtcController {
|
||||
codec = mime.substring(6);
|
||||
}
|
||||
}
|
||||
} else if (report.type == 'candidate-pair') {
|
||||
} else if (type == 'candidate-pair') {
|
||||
if (values['nominated'] == true) {
|
||||
hasNominatedPair = true;
|
||||
final rtt = values['currentRoundTripTime'];
|
||||
if (rtt is num) {
|
||||
delay = (rtt * 1000).toStringAsFixed(0);
|
||||
}
|
||||
// candidate-pair 的 bytesReceived/bytesSent 为整条连接(视频+DataChannel+ICE)的累计值。
|
||||
final br = values['bytesReceived'];
|
||||
final bs = values['bytesSent'];
|
||||
if (br is num) bytesReceived = br.toInt();
|
||||
if (bs is num) bytesSent = bs.toInt();
|
||||
final lc = values['localCandidateId'];
|
||||
final rc = values['remoteCandidateId'];
|
||||
if (lc is String) localCandidateId = lc;
|
||||
if (rc is String) remoteCandidateId = rc;
|
||||
}
|
||||
} else if (type == 'data-channel') {
|
||||
if (values['label'] == kDataChannelLabel) {
|
||||
final ms = values['messagesSent'];
|
||||
final mr = values['messagesReceived'];
|
||||
if (ms is num) dcMessagesSent = ms.toInt();
|
||||
if (mr is num) dcMessagesReceived = mr.toInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算每秒网速(与上次采样差值)。
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
String downSpeed = '-';
|
||||
String upSpeed = '-';
|
||||
if (_prevStatsTimestamp != 0) {
|
||||
final dt = (now - _prevStatsTimestamp) / 1000.0;
|
||||
if (dt > 0) {
|
||||
final downBps = (bytesReceived - _prevBytesReceived) / dt;
|
||||
final upBps = (bytesSent - _prevBytesSent) / dt;
|
||||
downSpeed = _formatSpeed(downBps);
|
||||
upSpeed = _formatSpeed(upBps);
|
||||
}
|
||||
}
|
||||
_prevBytesReceived = bytesReceived;
|
||||
_prevBytesSent = bytesSent;
|
||||
_prevStatsTimestamp = now;
|
||||
|
||||
// 连接类型(P2P/TURN)与候选协议(udp/tcp)。
|
||||
String connType = '-';
|
||||
String protocol = '-';
|
||||
if (hasNominatedPair) {
|
||||
connType =
|
||||
_resolveConnectionType(reports, localCandidateId, remoteCandidateId);
|
||||
protocol = _resolveProtocol(reports, localCandidateId, remoteCandidateId);
|
||||
}
|
||||
|
||||
// 连接时长。
|
||||
String duration = '-';
|
||||
if (_connectedAt != null) {
|
||||
final secs = ((now - _connectedAt!) / 1000).floor();
|
||||
final m = (secs ~/ 60).toString().padLeft(2, '0');
|
||||
final s = (secs % 60).toString().padLeft(2, '0');
|
||||
duration = '$m:$s';
|
||||
}
|
||||
|
||||
// 控制 DataChannel 状态。
|
||||
final dcState = _dataChannel?.state;
|
||||
final dcStatus = dcState == RTCDataChannelState.RTCDataChannelOpen
|
||||
? '已连接'
|
||||
: (dcState == null ? '未连接' : dcState.name);
|
||||
|
||||
return '分辨率: ${width}x$height 帧率: $fps 延迟: $delay ms\n'
|
||||
'解码格式: $codec';
|
||||
'解码格式: $codec 抖动: $jitter ms 丢包: $lossRate%\n'
|
||||
'丢帧: $framesDropped ↓下载: $downSpeed ↑上传: $upSpeed\n'
|
||||
'连接类型: $connType ($protocol) 时长: $duration\n'
|
||||
'控制通道: $dcStatus 收发: $dcMessagesSent/$dcMessagesReceived';
|
||||
}
|
||||
|
||||
/// 依据选定候选对的本地/远端候选类型,返回 P2P / TURN 中继描述。
|
||||
String _resolveConnectionType(
|
||||
List<StatsReport> reports,
|
||||
String? localCandidateId,
|
||||
String? remoteCandidateId,
|
||||
) {
|
||||
String? typeOf(String? id) {
|
||||
if (id == null) return null;
|
||||
final report = reports.where((r) => r.id == id).firstOrNull;
|
||||
final t = report?.values['candidateType'];
|
||||
return t is String ? t : null;
|
||||
}
|
||||
|
||||
final local = typeOf(localCandidateId);
|
||||
final remote = typeOf(remoteCandidateId);
|
||||
final isRelay = local == 'relay' || remote == 'relay';
|
||||
|
||||
if (isRelay) {
|
||||
// 进一步用本地候选具体类型细化(优先说明本端是否经 TURN 中继)。
|
||||
if (local == 'relay') return 'TURN 中继 (本端经服务器)';
|
||||
return 'TURN 中继';
|
||||
}
|
||||
// host: 同一局域网直连; srflx/prflx: 经 NAT 打洞的 P2P。
|
||||
return 'P2P 直连';
|
||||
}
|
||||
|
||||
/// 返回选定候选对的传输协议(udp/tcp)。
|
||||
String _resolveProtocol(
|
||||
List<StatsReport> reports,
|
||||
String? localCandidateId,
|
||||
String? remoteCandidateId,
|
||||
) {
|
||||
String protoOf(String? id) {
|
||||
if (id == null) return '-';
|
||||
final report = reports.where((r) => r.id == id).firstOrNull;
|
||||
final p = report?.values['protocol'];
|
||||
return p is String ? p : '-';
|
||||
}
|
||||
|
||||
final local = protoOf(localCandidateId);
|
||||
final remote = protoOf(remoteCandidateId);
|
||||
if (local != '-' && remote != '-' && local != remote) {
|
||||
return '$local/$remote';
|
||||
}
|
||||
return local != '-' ? local : remote;
|
||||
}
|
||||
|
||||
/// 将字节/秒格式化为人类可读字符串(B/s、KB/s、MB/s)。
|
||||
String _formatSpeed(double bytesPerSecond) {
|
||||
if (bytesPerSecond >= 1024 * 1024) {
|
||||
return '${(bytesPerSecond / (1024 * 1024)).toStringAsFixed(1)} MB/s';
|
||||
} else if (bytesPerSecond >= 1024) {
|
||||
return '${(bytesPerSecond / 1024).toStringAsFixed(1)} KB/s';
|
||||
}
|
||||
return '${bytesPerSecond.toStringAsFixed(0)} B/s';
|
||||
}
|
||||
|
||||
/// 释放所有资源。
|
||||
@@ -244,5 +402,10 @@ class WebRtcController {
|
||||
} catch (_) {}
|
||||
_dataChannel = null;
|
||||
_pc = null;
|
||||
// 重置网速统计基线,避免下次连接首帧显示错误的瞬时速率。
|
||||
_prevBytesReceived = 0;
|
||||
_prevBytesSent = 0;
|
||||
_prevStatsTimestamp = 0;
|
||||
_connectedAt = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user