[feat] add mobile codec status and scrolling codec names

This commit is contained in:
dijunkun
2026-10-27 04:16:41 +08:00
parent 1687816bde
commit e123d5f2cc
20 changed files with 241 additions and 48 deletions
@@ -195,6 +195,10 @@ final class NativeSession implements AutoCloseable {
fail(status >= 0 && status < reasons.length ? reasons[status] : "连接已结束");
}
} else if (type == 4) {
if(event.optInt("type",-1)==14){
networkStatistics.receiveRemoteEncoder(event.getJSONObject("video_codec").getString("encoder"));
return;
}
JSONObject info=event.optJSONObject("host_info");
if(info!=null)supportsTextInput=info.optBoolean("supports_text_input",false);
ui(() -> {
@@ -12,15 +12,18 @@ final class RemoteNetworkStatistics {
}
static final class Report {
final Traffic[] traffic=new Traffic[4];final int mode;final boolean srtp;final double rtt;
final String decoder;
Report(JSONObject json){
String[] keys={"video","audio","data","total"};
for(int i=0;i<keys.length;i++){JSONObject item=json.optJSONObject(keys[i]);traffic[i]=item==null?null:new Traffic(item);}
mode=json.optInt("mode",2);srtp=json.optBoolean("srtp");rtt=json.optDouble("rtt",Double.NaN);
decoder=json.optString("decoder","");
}
}
static final class Snapshot {
final Report report;final int fps,width,height;final double latency,rtt;
Snapshot(Report report,int fps,int width,int height,double latency,double rtt){this.report=report;this.fps=fps;this.width=width;this.height=height;this.latency=latency;this.rtt=rtt;}
final String remoteEncoder,localDecoder;
Snapshot(Report report,int fps,int width,int height,double latency,double rtt,String remoteEncoder,String localDecoder){this.report=report;this.fps=fps;this.width=width;this.height=height;this.latency=latency;this.rtt=rtt;this.remoteEncoder=remoteEncoder;this.localDecoder=localDecoder;}
String mode(){return report==null?"—":report.mode==0?"P2P 直连":report.mode==1?"TURN 中继":"—";}
String resolution(){return width>0&&height>0?width+" × "+height:"—";}
String summary(){
@@ -32,11 +35,13 @@ final class RemoteNetworkStatistics {
final double time,latency;Frame(double time,double latency){this.time=time;this.latency=latency;}
}
private Report report;
private String remoteEncoder="";
private double reportTime,rttTime,averageRtt=Double.NaN;
private long lastFrameId;
private int width,height;
private final ArrayDeque<Frame> frames=new ArrayDeque<>();
static double now(){return System.nanoTime()/1_000_000_000.0;}
synchronized void receiveRemoteEncoder(String encoder){remoteEncoder=encoder;}
synchronized void receive(Report value,double now){
report=value;reportTime=now;
if(!Double.isFinite(value.rtt)||value.rtt<0||value.rtt>2000)return;
@@ -53,7 +58,7 @@ final class RemoteNetworkStatistics {
synchronized Snapshot snapshot(double now){
prune(now);double total=0;int count=0;
for(Frame frame:frames)if(Double.isFinite(frame.latency)){total+=frame.latency;count++;}
return new Snapshot(now-reportTime<3?report:null,frames.size(),width,height,count==0?Double.NaN:total/count,now-rttTime<3?averageRtt:Double.NaN);
return new Snapshot(now-reportTime<3?report:null,frames.size(),width,height,count==0?Double.NaN:total/count,now-rttTime<3?averageRtt:Double.NaN,remoteEncoder,report==null?"":report.decoder);
}
private void prune(double now){while(!frames.isEmpty()&&now-frames.peekFirst().time>=1)frames.removeFirst();}
static String bitrate(long value){return value<0?"—":value>=1_000_000?String.format(Locale.ROOT,"%.1f Mbps",value/1_000_000.0):value>=1000?String.format(Locale.ROOT,"%.0f Kbps",value/1000.0):value+" bps";}
@@ -107,7 +107,7 @@ final class SessionControls extends FrameLayout {
}
void setMuted(boolean value){muted=value;if(menu!=null&&menuPage.equals("controls"))openMenu("controls");}
void videoSize(int width,int height){waiting.setVisibility(GONE);positionMouse();}
void resetVideoStatistics(){statistics(new RemoteNetworkStatistics.Snapshot(network.report,0,0,0,Double.NaN,network.rtt));}
void resetVideoStatistics(){statistics(new RemoteNetworkStatistics.Snapshot(network.report,0,0,0,Double.NaN,network.rtt,network.remoteEncoder,network.localDecoder));}
void statistics(RemoteNetworkStatistics.Snapshot value){network=value;renderNetwork();}
void videoSettings(RemoteVideoSettings value){settings=value;renderVideoSettings();}
void privacyScreen(RemotePrivacyScreen value){privacy=value;renderPrivacy();}
@@ -200,10 +200,12 @@ final class SessionControls extends FrameLayout {
for(int i=0;i<heading.getChildCount();i++)((TextView)heading.getChildAt(i)).setTextColor(SECONDARY);
for(int i=0;i<4;i++)trafficRows[i]=trafficRow(body,names[i],trafficCells[i],i==3);
ui.gap(body,10);ui.divider(body);ui.gap(body,10);
String[] titles={"帧率","分辨率","画面延时","连接延时(RTT)","连接方式","媒体加密"};networkDetails=new TextView[titles.length];
for(int i=0;i<titles.length;i++){LinearLayout row=ui.row();TextView title=ui.text(titles[i],12,false);title.setTextColor(SECONDARY);row.addView(title,new LinearLayout.LayoutParams(0,-2,1));
String[] titles={"帧率","分辨率","画面延时","连接延时(RTT)","连接方式","媒体加密","被控端编码器","控制端解码器"};networkDetails=new TextView[titles.length];
for(int i=0;i<titles.length;i++){LinearLayout row=ui.row();TextView title=ui.text(titles[i],12,false);title.setTextColor(SECONDARY);row.addView(title,i>=6?new LinearLayout.LayoutParams(-2,-2):new LinearLayout.LayoutParams(0,-2,1));
if(i==5){encryptionLock=ui.icon("lock",GREEN);encryptionOpen=ui.icon("unlock",SECONDARY);for(View icon:new View[]{encryptionLock,encryptionOpen}){LinearLayout.LayoutParams p=ui.size(14,14);p.rightMargin=ui.dp(4);row.addView(icon,p);}}
TextView value=ui.text("—",12,false);value.setFontFeatureSettings("tnum");row.addView(value);networkDetails[i]=value;body.addView(row);if(i<titles.length-1)ui.gap(body,7);}
TextView value=ui.text("—",12,false);value.setFontFeatureSettings("tnum");
if(i>=6){value.setGravity(Gravity.END);value.setSingleLine();value.setEllipsize(android.text.TextUtils.TruncateAt.MARQUEE);value.setMarqueeRepeatLimit(-1);value.setSelected(true);LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,-2,1);p.leftMargin=ui.dp(8);row.addView(value,p);}else row.addView(value);
networkDetails[i]=value;body.addView(row);if(i<titles.length-1)ui.gap(body,7);}
renderNetwork();
}
private LinearLayout trafficRow(LinearLayout body,String title,TextView[] cells,boolean total){
@@ -222,8 +224,9 @@ final class SessionControls extends FrameLayout {
String in=RemoteNetworkStatistics.bitrate(traffic==null?-1:traffic.inbound),out=RemoteNetworkStatistics.bitrate(traffic==null?-1:traffic.outbound),loss=RemoteNetworkStatistics.loss(traffic==null?Double.NaN:traffic.loss);
trafficCells[i][0].setText(in);trafficCells[i][1].setText(out);trafficCells[i][2].setText(loss);trafficRows[i].setContentDescription(names[i]+",接收 "+in+",发送 "+out+",丢包率 "+loss);
}
String[] values={network.fps+" FPS",network.resolution(),RemoteNetworkStatistics.latency(network.latency),RemoteNetworkStatistics.latency(network.rtt),network.mode(),network.report==null?"—":network.report.srtp?"SRTP 已启用":"未启用"};
for(int i=0;i<values.length;i++)networkDetails[i].setText(values[i]);networkDetails[5].setTextColor(network.report!=null&&network.report.srtp?GREEN:SECONDARY);
String[] values={network.fps+" FPS",network.resolution(),RemoteNetworkStatistics.latency(network.latency),RemoteNetworkStatistics.latency(network.rtt),network.mode(),network.report==null?"—":network.report.srtp?"SRTP 已启用":"未启用",network.remoteEncoder.isEmpty()?"—":network.remoteEncoder,network.localDecoder.isEmpty()?"—":network.localDecoder};
// Reassigning identical text every statistics tick restarts the marquee.
for(int i=0;i<values.length;i++)if(!android.text.TextUtils.equals(networkDetails[i].getText(),values[i]))networkDetails[i].setText(values[i]);networkDetails[5].setTextColor(network.report!=null&&network.report.srtp?GREEN:SECONDARY);
encryptionLock.setVisibility(network.report!=null&&network.report.srtp?VISIBLE:GONE);encryptionOpen.setVisibility(network.report!=null&&!network.report.srtp?VISIBLE:GONE);
}
private int controlColumns(){return getMeasuredHeight()<ui.dp(400)&&getMeasuredWidth()>=ui.dp(400)?4:3;}