feat: 连接显示更多信息
This commit is contained in:
@@ -64,6 +64,14 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 网速统计基线(累计字节数与上次采样时间戳)
|
||||||
|
private long prevBytesReceived = 0;
|
||||||
|
private long prevBytesSent = 0;
|
||||||
|
private long prevStatsTime = 0;
|
||||||
|
|
||||||
|
// 连接建立时的时间戳(毫秒),用于计算连接时长。
|
||||||
|
private long connectionStartTime = 0;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -239,6 +247,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
webRtcClient.setConnectionListener(new WebRtcClient.ConnectionListener() {
|
webRtcClient.setConnectionListener(new WebRtcClient.ConnectionListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onConnectionEstablished() {
|
public void onConnectionEstablished() {
|
||||||
|
connectionStartTime = System.currentTimeMillis();
|
||||||
runOnUiThread(() -> {
|
runOnUiThread(() -> {
|
||||||
tvStatus.setText("状态: 已连接 - 远程控制中");
|
tvStatus.setText("状态: 已连接 - 远程控制中");
|
||||||
updateUI(true);
|
updateUI(true);
|
||||||
@@ -394,6 +403,11 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
wsClient.disconnect();
|
wsClient.disconnect();
|
||||||
wsClient = null;
|
wsClient = null;
|
||||||
}
|
}
|
||||||
|
// 重置网速统计基线,避免下次连接首帧显示错误速率。
|
||||||
|
prevBytesReceived = 0;
|
||||||
|
prevBytesSent = 0;
|
||||||
|
prevStatsTime = 0;
|
||||||
|
connectionStartTime = 0;
|
||||||
remoteVideoView.clearImage();
|
remoteVideoView.clearImage();
|
||||||
updateUI(false);
|
updateUI(false);
|
||||||
tvStatus.setText("状态: 已停止");
|
tvStatus.setText("状态: 已停止");
|
||||||
@@ -422,8 +436,25 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
String codecName = "-";
|
String codecName = "-";
|
||||||
double avgDecodeTimeMs = 0;
|
double avgDecodeTimeMs = 0;
|
||||||
|
|
||||||
|
// 网速 / 连接类型相关
|
||||||
|
long bytesReceived = 0;
|
||||||
|
long bytesSent = 0;
|
||||||
|
String localCandidateId = null;
|
||||||
|
String remoteCandidateId = null;
|
||||||
|
boolean hasNominatedPair = false;
|
||||||
|
|
||||||
|
// 抖动 / 丢包 / 丢帧
|
||||||
|
double jitterMs = 0;
|
||||||
|
String lossRate = "-";
|
||||||
|
long framesDropped = 0;
|
||||||
|
|
||||||
|
// 控制 DataChannel 收发消息数
|
||||||
|
long dcMessagesSent = 0;
|
||||||
|
long dcMessagesReceived = 0;
|
||||||
|
|
||||||
for (org.webrtc.RTCStats stats : report.getStatsMap().values()) {
|
for (org.webrtc.RTCStats stats : report.getStatsMap().values()) {
|
||||||
if ("inbound-rtp".equals(stats.getType()) && "video".equals(stats.getMembers().get("kind"))) {
|
String type = stats.getType();
|
||||||
|
if ("inbound-rtp".equals(type) && "video".equals(stats.getMembers().get("kind"))) {
|
||||||
// 分辨率
|
// 分辨率
|
||||||
Object w = stats.getMembers().get("frameWidth");
|
Object w = stats.getMembers().get("frameWidth");
|
||||||
if (w instanceof Number) width = ((Number) w).longValue();
|
if (w instanceof Number) width = ((Number) w).longValue();
|
||||||
@@ -466,20 +497,104 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
String d = (String) decoder;
|
String d = (String) decoder;
|
||||||
codecName = codecName.equals("-") ? d : codecName + " (" + d + ")";
|
codecName = codecName.equals("-") ? d : codecName + " (" + d + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 抖动(秒 -> 毫秒)
|
||||||
|
Object jitter = stats.getMembers().get("jitter");
|
||||||
|
if (jitter instanceof Number) {
|
||||||
|
jitterMs = ((Number) jitter).doubleValue() * 1000;
|
||||||
}
|
}
|
||||||
if ("candidate-pair".equals(stats.getType())) {
|
|
||||||
|
// 丢包率
|
||||||
|
Object pr = stats.getMembers().get("packetsReceived");
|
||||||
|
Object pl = stats.getMembers().get("packetsLost");
|
||||||
|
if (pr instanceof Number && pl instanceof Number) {
|
||||||
|
long total = ((Number) pr).longValue() + ((Number) pl).longValue();
|
||||||
|
lossRate = total > 0
|
||||||
|
? String.format(Locale.getDefault(), "%.1f", ((Number) pl).doubleValue() / total * 100)
|
||||||
|
: "0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 丢帧
|
||||||
|
Object fd = stats.getMembers().get("framesDropped");
|
||||||
|
if (fd instanceof Number) framesDropped = ((Number) fd).longValue();
|
||||||
|
}
|
||||||
|
if ("candidate-pair".equals(type)) {
|
||||||
Object nominated = stats.getMembers().get("nominated");
|
Object nominated = stats.getMembers().get("nominated");
|
||||||
if (Boolean.TRUE.equals(nominated)) {
|
if (Boolean.TRUE.equals(nominated)) {
|
||||||
|
hasNominatedPair = true;
|
||||||
Object rtt = stats.getMembers().get("currentRoundTripTime");
|
Object rtt = stats.getMembers().get("currentRoundTripTime");
|
||||||
if (rtt instanceof Number) {
|
if (rtt instanceof Number) {
|
||||||
delay = (long) (((Number) rtt).doubleValue() * 1000);
|
delay = (long) (((Number) rtt).doubleValue() * 1000);
|
||||||
}
|
}
|
||||||
|
// candidate-pair 的 bytesReceived/bytesSent 为整条连接
|
||||||
|
// (视频 + DataChannel + ICE)的累计值,用于计算每秒网速。
|
||||||
|
Object br = stats.getMembers().get("bytesReceived");
|
||||||
|
if (br instanceof Number) bytesReceived = ((Number) br).longValue();
|
||||||
|
Object bs = stats.getMembers().get("bytesSent");
|
||||||
|
if (bs instanceof Number) bytesSent = ((Number) bs).longValue();
|
||||||
|
Object lc = stats.getMembers().get("localCandidateId");
|
||||||
|
if (lc instanceof String) localCandidateId = (String) lc;
|
||||||
|
Object rc = stats.getMembers().get("remoteCandidateId");
|
||||||
|
if (rc instanceof String) remoteCandidateId = (String) rc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("data-channel".equals(type)) {
|
||||||
|
// 仅统计与对端的控制通道
|
||||||
|
Object label = stats.getMembers().get("label");
|
||||||
|
if ("control_channel".equals(label)) {
|
||||||
|
Object ms = stats.getMembers().get("messagesSent");
|
||||||
|
if (ms instanceof Number) dcMessagesSent = ((Number) ms).longValue();
|
||||||
|
Object mr = stats.getMembers().get("messagesReceived");
|
||||||
|
if (mr instanceof Number) dcMessagesReceived = ((Number) mr).longValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算每秒上下行网速(与上次采样差值)。
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
String downSpeed = "-";
|
||||||
|
String upSpeed = "-";
|
||||||
|
if (prevStatsTime != 0) {
|
||||||
|
double dt = (now - prevStatsTime) / 1000.0;
|
||||||
|
if (dt > 0) {
|
||||||
|
double downBps = (bytesReceived - prevBytesReceived) / dt;
|
||||||
|
double upBps = (bytesSent - prevBytesSent) / dt;
|
||||||
|
downSpeed = formatSpeed(downBps);
|
||||||
|
upSpeed = formatSpeed(upBps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevBytesReceived = bytesReceived;
|
||||||
|
prevBytesSent = bytesSent;
|
||||||
|
prevStatsTime = now;
|
||||||
|
|
||||||
|
// 判定连接类型与传输协议:两端候选任一为 relay 即走 TURN 中继,否则为 P2P 直连。
|
||||||
|
String connType = "-";
|
||||||
|
String protocol = "-";
|
||||||
|
if (hasNominatedPair) {
|
||||||
|
connType = resolveConnectionType(report, localCandidateId, remoteCandidateId);
|
||||||
|
protocol = resolveProtocol(report, localCandidateId, remoteCandidateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接时长(mm:ss)
|
||||||
|
String duration = "-";
|
||||||
|
if (connectionStartTime != 0) {
|
||||||
|
long secs = (now - connectionStartTime) / 1000;
|
||||||
|
duration = String.format(Locale.getDefault(), "%02d:%02d", secs / 60, secs % 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制 DataChannel 状态
|
||||||
|
String dcStatus = (webRtcClient != null && webRtcClient.isDataChannelOpen())
|
||||||
|
? "已连接" : "未连接";
|
||||||
|
|
||||||
final String statsText = String.format(Locale.getDefault(),
|
final String statsText = String.format(Locale.getDefault(),
|
||||||
"分辨率: %dx%d 帧率: %d 延迟: %dms\n解码格式: %s 平均解码耗时: %.1fms",
|
"分辨率: %dx%d 帧率: %d 延迟: %dms\n"
|
||||||
width, height, fps, delay, codecName, avgDecodeTimeMs);
|
+ "解码格式: %s 抖动: %.0fms 丢包: %s%% 解码耗时: %.1fms\n"
|
||||||
|
+ "丢帧: %d ↓下载: %s ↑上传: %s\n"
|
||||||
|
+ "连接类型: %s (%s) 时长: %s\n"
|
||||||
|
+ "控制通道: %s 收发: %d/%d",
|
||||||
|
width, height, fps, delay, codecName, jitterMs, lossRate, avgDecodeTimeMs,
|
||||||
|
framesDropped, downSpeed, upSpeed, connType, protocol,
|
||||||
|
duration, dcStatus, dcMessagesSent, dcMessagesReceived);
|
||||||
runOnUiThread(() -> {
|
runOnUiThread(() -> {
|
||||||
if (tvStats != null) {
|
if (tvStats != null) {
|
||||||
tvStats.setText(statsText);
|
tvStats.setText(statsText);
|
||||||
@@ -489,6 +604,55 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 依据选定候选对的本地/远端候选类型,返回 P2P / TURN 中继描述。
|
||||||
|
private String resolveConnectionType(org.webrtc.RTCStatsReport report,
|
||||||
|
String localCandidateId,
|
||||||
|
String remoteCandidateId) {
|
||||||
|
String local = candidateTypeOf(report, localCandidateId);
|
||||||
|
String remote = candidateTypeOf(report, remoteCandidateId);
|
||||||
|
boolean isRelay = "relay".equals(local) || "relay".equals(remote);
|
||||||
|
if (isRelay) {
|
||||||
|
if ("relay".equals(local)) return "TURN 中继 (本端经服务器)";
|
||||||
|
return "TURN 中继";
|
||||||
|
}
|
||||||
|
// host: 同一局域网直连; srflx/prflx: 经 NAT 打洞的 P2P。
|
||||||
|
return "P2P 直连";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回选定候选对的传输协议(udp/tcp)。
|
||||||
|
private String resolveProtocol(org.webrtc.RTCStatsReport report,
|
||||||
|
String localCandidateId,
|
||||||
|
String remoteCandidateId) {
|
||||||
|
String protoOf = candidateMemberOf(report, localCandidateId, "protocol");
|
||||||
|
String protoOfRemote = candidateMemberOf(report, remoteCandidateId, "protocol");
|
||||||
|
if (!"-".equals(protoOf) && !"-".equals(protoOfRemote) && !protoOf.equals(protoOfRemote)) {
|
||||||
|
return protoOf + "/" + protoOfRemote;
|
||||||
|
}
|
||||||
|
return !"-".equals(protoOf) ? protoOf : protoOfRemote;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String candidateTypeOf(org.webrtc.RTCStatsReport report, String candidateId) {
|
||||||
|
return candidateMemberOf(report, candidateId, "candidateType");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String candidateMemberOf(org.webrtc.RTCStatsReport report, String candidateId, String key) {
|
||||||
|
if (candidateId == null) return "-";
|
||||||
|
org.webrtc.RTCStats candidate = report.getStatsMap().get(candidateId);
|
||||||
|
if (candidate == null) return "-";
|
||||||
|
Object value = candidate.getMembers().get(key);
|
||||||
|
return value instanceof String ? (String) value : "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将字节/秒格式化为人类可读字符串(B/s、KB/s、MB/s)。
|
||||||
|
private String formatSpeed(double bytesPerSecond) {
|
||||||
|
if (bytesPerSecond >= 1024 * 1024) {
|
||||||
|
return String.format(Locale.getDefault(), "%.1f MB/s", bytesPerSecond / (1024 * 1024));
|
||||||
|
} else if (bytesPerSecond >= 1024) {
|
||||||
|
return String.format(Locale.getDefault(), "%.1f KB/s", bytesPerSecond / 1024);
|
||||||
|
}
|
||||||
|
return String.format(Locale.getDefault(), "%.0f B/s", bytesPerSecond);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
|||||||
@@ -319,4 +319,9 @@ public class WebRtcClient {
|
|||||||
peerConnection.getStats(callback);
|
peerConnection.getStats(callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 控制 DataChannel 是否已打开(用于状态显示)。
|
||||||
|
public boolean isDataChannelOpen() {
|
||||||
|
return dataChannel != null && dataChannel.state() == DataChannel.State.OPEN;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ class WebRtcController {
|
|||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print('ICE connection state: $state');
|
print('ICE connection state: $state');
|
||||||
if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) {
|
if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) {
|
||||||
|
_connectedAt = DateTime.now().millisecondsSinceEpoch;
|
||||||
onConnectionEstablished?.call();
|
onConnectionEstablished?.call();
|
||||||
} else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) {
|
} else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) {
|
||||||
onIceDisconnected?.call('ICE 连接已断开');
|
onIceDisconnected?.call('ICE 连接已断开');
|
||||||
@@ -191,7 +192,19 @@ class WebRtcController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 上一次统计采样的字节数与时间戳,用于计算每秒网速。
|
||||||
|
int _prevBytesReceived = 0;
|
||||||
|
int _prevBytesSent = 0;
|
||||||
|
int _prevStatsTimestamp = 0;
|
||||||
|
|
||||||
|
/// 连接建立时的时间戳(毫秒),用于计算连接时长。
|
||||||
|
int? _connectedAt;
|
||||||
|
|
||||||
/// 采集连接统计信息,回调格式化后的文本(对应 Android 端 updateStats)。
|
/// 采集连接统计信息,回调格式化后的文本(对应 Android 端 updateStats)。
|
||||||
|
///
|
||||||
|
/// 包含:分辨率/帧率/延迟/解码格式、抖动、丢包率、丢帧、
|
||||||
|
/// 每秒上下行网速、连接类型(P2P/TURN)+候选协议、连接时长、
|
||||||
|
/// 控制 DataChannel 状态与收发消息数。
|
||||||
Future<String> getStatsText() async {
|
Future<String> getStatsText() async {
|
||||||
if (_pc == null) return '';
|
if (_pc == null) return '';
|
||||||
final reports = await _pc!.getStats();
|
final reports = await _pc!.getStats();
|
||||||
@@ -201,14 +214,41 @@ class WebRtcController {
|
|||||||
dynamic fps = '-';
|
dynamic fps = '-';
|
||||||
dynamic delay = '-';
|
dynamic delay = '-';
|
||||||
dynamic codec = '-';
|
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) {
|
for (final report in reports) {
|
||||||
final values = report.values;
|
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'] ?? '-';
|
width = values['frameWidth'] ?? '-';
|
||||||
height = values['frameHeight'] ?? '-';
|
height = values['frameHeight'] ?? '-';
|
||||||
fps = values['framesPerSecond'] ?? '-';
|
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'];
|
final codecId = values['codecId'];
|
||||||
if (codecId != null) {
|
if (codecId != null) {
|
||||||
final codecReport = reports.where((r) => r.id == codecId).firstOrNull;
|
final codecReport = reports.where((r) => r.id == codecId).firstOrNull;
|
||||||
@@ -217,18 +257,136 @@ class WebRtcController {
|
|||||||
codec = mime.substring(6);
|
codec = mime.substring(6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (report.type == 'candidate-pair') {
|
} else if (type == 'candidate-pair') {
|
||||||
if (values['nominated'] == true) {
|
if (values['nominated'] == true) {
|
||||||
|
hasNominatedPair = true;
|
||||||
final rtt = values['currentRoundTripTime'];
|
final rtt = values['currentRoundTripTime'];
|
||||||
if (rtt is num) {
|
if (rtt is num) {
|
||||||
delay = (rtt * 1000).toStringAsFixed(0);
|
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'
|
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 (_) {}
|
} catch (_) {}
|
||||||
_dataChannel = null;
|
_dataChannel = null;
|
||||||
_pc = null;
|
_pc = null;
|
||||||
|
// 重置网速统计基线,避免下次连接首帧显示错误的瞬时速率。
|
||||||
|
_prevBytesReceived = 0;
|
||||||
|
_prevBytesSent = 0;
|
||||||
|
_prevStatsTimestamp = 0;
|
||||||
|
_connectedAt = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user