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
|
||||
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();
|
||||
|
||||
@@ -319,4 +319,9 @@ public class WebRtcClient {
|
||||
peerConnection.getStats(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/// 控制 DataChannel 是否已打开(用于状态显示)。
|
||||
public boolean isDataChannelOpen() {
|
||||
return dataChannel != null && dataChannel.state() == DataChannel.State.OPEN;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user