feat: 支持手动切换采集帧率

在 Android、Web 及 Flutter 控制端新增帧率选择 UI,被控端按屏幕刷新率生成支持的帧率档位并上报,允许在保持分辨率不变的情况下仅切换帧率。

另附带更新信号服务器的数据库配置。
This commit is contained in:
TongTongStudio
2026-07-31 03:26:40 +08:00
parent c3602493dd
commit 362b9f238a
23 changed files with 483 additions and 13 deletions

View File

@@ -69,6 +69,13 @@ public class MainActivity extends AppCompatActivity {
private TextView tvStatusControl;
private TextView tvStats;
private Spinner spinnerResolution;
private Spinner spinnerFps;
/** 帧率下拉框当前档位(以被控端上报的 supported_fps 为准)。 */
private final List<Integer> fpsOptions = new ArrayList<>();
/** 被控端最近上报的实际采集分辨率/帧率(仅切帧率时保持分辨率不变)。 */
private int lastReportedWidth = 0;
private int lastReportedHeight = 0;
private int lastKnownFps = 0;
private Button btnDisconnectControl;
// 屏幕串流"自编码"模式相关 UI 与解码器
@@ -131,6 +138,7 @@ public class MainActivity extends AppCompatActivity {
tvStatusControl = findViewById(R.id.tv_status_control);
tvStats = findViewById(R.id.tv_stats);
spinnerResolution = findViewById(R.id.spinner_resolution);
spinnerFps = findViewById(R.id.spinner_fps);
btnDisconnectControl = findViewById(R.id.btn_disconnect_control);
// 默认服务器地址
@@ -192,6 +200,7 @@ public class MainActivity extends AppCompatActivity {
});
setupResolutionSpinner();
setupFpsSpinner();
updateUI(false);
setupSelfCodecUi();
@@ -414,6 +423,13 @@ public class MainActivity extends AppCompatActivity {
webRtcClient.setSelfCodecDecoder(selfCodecDecoder);
webRtcClient.setRecorder(videoRecorder);
webRtcClient.setStreamModeReportListener(mode -> runOnUiThread(() -> syncStreamModeUi(mode)));
// 被控端上报当前分辨率/帧率与支持的帧率档位 -> 同步帧率下拉框
webRtcClient.setResolutionReportListener((w, h, fps, supportedFps) -> runOnUiThread(() -> {
if (w > 0) lastReportedWidth = w;
if (h > 0) lastReportedHeight = h;
if (fps > 0) lastKnownFps = fps;
applyFpsOptions(supportedFps, fps);
}));
webRtcClient.setConnectionListener(new WebRtcClient.ConnectionListener() {
@Override
public void onConnectionEstablished() {
@@ -689,6 +705,61 @@ public class MainActivity extends AppCompatActivity {
});
}
/** 构建帧率下拉框档位以被控端上报REPORT_RESOLUTION.supported_fps为准未上报前使用常用档位。 */
private void setupFpsSpinner() {
List<Integer> defaults = new ArrayList<>();
defaults.add(15);
defaults.add(24);
defaults.add(30);
defaults.add(60);
applyFpsOptions(defaults, 0);
spinnerFps.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position < 0 || position >= fpsOptions.size()) return;
int fps = fpsOptions.get(position);
// 与被控端当前帧率一致时不重复发送(含上报同步 setSelection 触发的回调)
if (fps <= 0 || fps == lastKnownFps || webRtcClient == null) return;
// 仅切换帧率:分辨率沿用被控端最近上报的实际采集尺寸
webRtcClient.requestResolutionChange(lastReportedWidth, lastReportedHeight, fps);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
}
/** 更新帧率档位并把选中项同步为最接近 currentFps 的档位。 */
private void applyFpsOptions(List<Integer> options, int currentFps) {
if (spinnerFps == null) return;
if (options != null && !options.isEmpty() && !options.equals(fpsOptions)) {
fpsOptions.clear();
fpsOptions.addAll(options);
List<String> labels = new ArrayList<>();
for (int f : fpsOptions) labels.add(f + "fps");
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this, android.R.layout.simple_spinner_item, labels);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerFps.setAdapter(adapter);
}
if (currentFps > 0 && !fpsOptions.isEmpty()) {
int idx = 0;
int best = Integer.MAX_VALUE;
for (int i = 0; i < fpsOptions.size(); i++) {
int diff = Math.abs(fpsOptions.get(i) - currentFps);
if (diff < best) {
best = diff;
idx = i;
}
}
if (spinnerFps.getSelectedItemPosition() != idx) {
spinnerFps.setSelection(idx);
}
}
}
private void updateUI(boolean connected) {
setupPanel.setVisibility(connected ? View.GONE : View.VISIBLE);
controlPanel.setVisibility(connected ? View.VISIBLE : View.GONE);
@@ -697,6 +768,9 @@ public class MainActivity extends AppCompatActivity {
if (spinnerResolution != null) {
spinnerResolution.setEnabled(connected);
}
if (spinnerFps != null) {
spinnerFps.setEnabled(connected);
}
if (tvStatusControl != null) {
tvStatusControl.setText(connected ? "远程控制中" : "未连接");
}

View File

@@ -71,6 +71,17 @@ public class WebRtcClient {
void onStreamModeReported(int mode);
}
/** 被控端上报当前采集分辨率/帧率与支持的帧率档位REPORT_RESOLUTION。 */
public interface ResolutionReportListener {
void onResolutionReported(int width, int height, int fps, List<Integer> supportedFps);
}
private ResolutionReportListener resolutionReportListener;
public void setResolutionReportListener(ResolutionReportListener l) {
this.resolutionReportListener = l;
}
private ConnectionListener connectionListener;
public WebRtcClient(Context context, WebSocketClient wsClient, String deviceId) {
@@ -371,10 +382,16 @@ public class WebRtcClient {
streamModeReportListener.onStreamModeReported(msg.getStreamMode());
}
} else if (msg.getAction() == Action.REPORT_RESOLUTION) {
Log.i(TAG, "Received resolution report: " + msg.getWidth() + "x" + msg.getHeight());
Log.i(TAG, "Received resolution report: " + msg.getWidth() + "x" + msg.getHeight()
+ " @ " + msg.getFps() + "fps, supportedFps=" + msg.getSupportedFpsList());
if (selfCodecDecoder != null) {
selfCodecDecoder.updateResolution(msg.getWidth(), msg.getHeight());
}
if (resolutionReportListener != null) {
resolutionReportListener.onResolutionReported(
msg.getWidth(), msg.getHeight(), msg.getFps(),
new ArrayList<>(msg.getSupportedFpsList()));
}
}
} catch (Exception ignored) {
}

View File

@@ -51,4 +51,7 @@ message ControlMessage {
int32 height = 13;
int32 fps = 14;
int32 stream_mode = 15; // 串流模式0=WebRTC 内置媒体流1=自编码(MediaCodec 硬编 + DataChannel 透传)
// 被控端支持的帧率档位列表REPORT_RESOLUTION 上报,按屏幕刷新率筛选,升序)。
repeated int32 supported_fps = 16;
}

View File

@@ -152,6 +152,13 @@
android:layout_marginTop="4dp"
android:backgroundTint="#80FFFFFF" />
<Spinner
android:id="@+id/spinner_fps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:backgroundTint="#80FFFFFF" />
<Button
android:id="@+id/btn_disconnect_control"
android:layout_width="wrap_content"