feat: 连接显示更多信息

This commit is contained in:
2026-07-15 18:15:18 +08:00
parent 97ef11a025
commit 839a980b45
3 changed files with 339 additions and 7 deletions

View File

@@ -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
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -239,6 +247,7 @@ public class MainActivity extends AppCompatActivity {
webRtcClient.setConnectionListener(new WebRtcClient.ConnectionListener() {
@Override
public void onConnectionEstablished() {
connectionStartTime = System.currentTimeMillis();
runOnUiThread(() -> {
tvStatus.setText("状态: 已连接 - 远程控制中");
updateUI(true);
@@ -394,6 +403,11 @@ public class MainActivity extends AppCompatActivity {
wsClient.disconnect();
wsClient = null;
}
// 重置网速统计基线,避免下次连接首帧显示错误速率。
prevBytesReceived = 0;
prevBytesSent = 0;
prevStatsTime = 0;
connectionStartTime = 0;
remoteVideoView.clearImage();
updateUI(false);
tvStatus.setText("状态: 已停止");
@@ -422,8 +436,25 @@ public class MainActivity extends AppCompatActivity {
String codecName = "-";
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()) {
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");
if (w instanceof Number) width = ((Number) w).longValue();
@@ -466,20 +497,104 @@ public class MainActivity extends AppCompatActivity {
String d = (String) decoder;
codecName = codecName.equals("-") ? d : codecName + " (" + d + ")";
}
// 抖动(秒 -> 毫秒)
Object jitter = stats.getMembers().get("jitter");
if (jitter instanceof Number) {
jitterMs = ((Number) jitter).doubleValue() * 1000;
}
// 丢包率
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(stats.getType())) {
if ("candidate-pair".equals(type)) {
Object nominated = stats.getMembers().get("nominated");
if (Boolean.TRUE.equals(nominated)) {
hasNominatedPair = true;
Object rtt = stats.getMembers().get("currentRoundTripTime");
if (rtt instanceof Number) {
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(),
"分辨率: %dx%d 帧率: %d 延迟: %dms\n解码格式: %s 平均解码耗时: %.1fms",
width, height, fps, delay, codecName, avgDecodeTimeMs);
"分辨率: %dx%d 帧率: %d 延迟: %dms\n"
+ "解码格式: %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(() -> {
if (tvStats != null) {
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
protected void onDestroy() {
super.onDestroy();

View File

@@ -319,4 +319,9 @@ public class WebRtcClient {
peerConnection.getStats(callback);
}
}
/// 控制 DataChannel 是否已打开(用于状态显示)。
public boolean isDataChannelOpen() {
return dataChannel != null && dataChannel.state() == DataChannel.State.OPEN;
}
}