feat: 支持手动切换采集帧率
在 Android、Web 及 Flutter 控制端新增帧率选择 UI,被控端按屏幕刷新率生成支持的帧率档位并上报,允许在保持分辨率不变的情况下仅切换帧率。 另附带更新信号服务器的数据库配置。
This commit is contained in:
@@ -65,6 +65,7 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
screenCaptureService.setStateListener(MainActivity.this);
|
||||
setupLocalPreview();
|
||||
setupResolutionSpinner();
|
||||
setupFpsSpinner();
|
||||
// 重新绑定(如 Activity 销毁后重新打开)时,依据服务实际状态恢复界面显示为“运行中”。
|
||||
if (screenCaptureService.isStreaming()) {
|
||||
updateUI(true);
|
||||
@@ -318,6 +319,7 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
binding.etServerUrl.setEnabled(!running);
|
||||
binding.etDeviceId.setEnabled(!running);
|
||||
binding.spinnerResolution.setEnabled(running && isBound);
|
||||
binding.spinnerFps.setEnabled(running && isBound);
|
||||
binding.tvStatus.setText(running ? "状态: 运行中" : "状态: 已停止");
|
||||
}
|
||||
|
||||
@@ -368,9 +370,12 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
binding.spinnerResolution.setSelection(selectIdx);
|
||||
updateCurrentResolutionText(cur[0], cur[1], cur[2]);
|
||||
|
||||
// 注册监听:本地或远端切换分辨率后刷新当前分辨率文本
|
||||
// 注册监听:本地或远端切换分辨率/帧率后刷新当前分辨率文本并同步帧率下拉框
|
||||
screenCaptureService.setResolutionListener((w, h, fps, fromRemote) ->
|
||||
runOnUiThread(() -> updateCurrentResolutionText(w, h, fps)));
|
||||
runOnUiThread(() -> {
|
||||
updateCurrentResolutionText(w, h, fps);
|
||||
syncFpsSpinnerSelection(fps);
|
||||
}));
|
||||
|
||||
// 注意:先 setSelection 再设置监听,避免初始化时触发一次多余的切换。
|
||||
binding.spinnerResolution.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@@ -395,4 +400,65 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
binding.tvCurrentResolution.setText(
|
||||
String.format("当前: %dx%d @ %dfps", w, h, fps));
|
||||
}
|
||||
|
||||
/** 帧率下拉框可选档位(由服务按屏幕刷新率给出)。 */
|
||||
private List<Integer> fpsSpinnerOptions;
|
||||
|
||||
/** 构建帧率下拉框;本地选择后仅切换帧率(保持当前分辨率)。 */
|
||||
private void setupFpsSpinner() {
|
||||
if (screenCaptureService == null) return;
|
||||
|
||||
List<Integer> options = screenCaptureService.getSupportedFpsList();
|
||||
if (options == null || options.isEmpty()) {
|
||||
options = new ArrayList<>();
|
||||
options.add(15);
|
||||
options.add(24);
|
||||
options.add(30);
|
||||
}
|
||||
fpsSpinnerOptions = options;
|
||||
|
||||
List<String> labels = new ArrayList<>();
|
||||
for (int f : options) 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);
|
||||
binding.spinnerFps.setAdapter(adapter);
|
||||
|
||||
int[] cur = screenCaptureService.getCurrentCaptureResolution();
|
||||
syncFpsSpinnerSelection(cur[2]);
|
||||
|
||||
// 注意:先 setSelection 再设置监听,避免初始化时触发一次多余的切换。
|
||||
binding.spinnerFps.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (screenCaptureService == null || fpsSpinnerOptions == null) return;
|
||||
int fps = fpsSpinnerOptions.get(position);
|
||||
int[] now = screenCaptureService.getCurrentCaptureResolution();
|
||||
// 与当前一致则不重复切换,避免无谓的提示与编码重启
|
||||
if (fps == now[2]) return;
|
||||
screenCaptureService.changeFps(fps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 将帧率下拉框选中项同步为最接近 fps 的档位(本地/远端切换后调用)。 */
|
||||
private void syncFpsSpinnerSelection(int fps) {
|
||||
if (fpsSpinnerOptions == null || fpsSpinnerOptions.isEmpty()) return;
|
||||
int idx = 0;
|
||||
int bestDiff = Integer.MAX_VALUE;
|
||||
for (int i = 0; i < fpsSpinnerOptions.size(); i++) {
|
||||
int diff = Math.abs(fpsSpinnerOptions.get(i) - fps);
|
||||
if (diff < bestDiff) {
|
||||
bestDiff = diff;
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
if (binding.spinnerFps.getSelectedItemPosition() != idx) {
|
||||
binding.spinnerFps.setSelection(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ import org.webrtc.EglBase;
|
||||
import org.webrtc.IceCandidate;
|
||||
import org.webrtc.ScreenCapturerAndroid;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ScreenCaptureService extends Service {
|
||||
|
||||
private static final String TAG = "ScreenCaptureService";
|
||||
@@ -112,6 +115,10 @@ public class ScreenCaptureService extends Service {
|
||||
private int currentCaptureWidth;
|
||||
private int currentCaptureHeight;
|
||||
private int currentCaptureFps;
|
||||
/** 屏幕最大刷新率(远程/本地请求的帧率不会超过该值)。 */
|
||||
private int maxCaptureFps = 60;
|
||||
/** 按屏幕刷新率筛选出的适合帧率档位(升序)。 */
|
||||
private List<Integer> supportedFpsList = new ArrayList<>();
|
||||
/**
|
||||
* 主线程 Handler,用于从 WebRTC 回调线程切回主线程执行 UI 提示。
|
||||
*/
|
||||
@@ -210,11 +217,15 @@ public class ScreenCaptureService extends Service {
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
int fps = Math.round(refreshRate);
|
||||
// 屏幕控制 30fps 已足够跟手,并显著降低 H264 硬编延迟(影响“跟手”体验)。
|
||||
// 更高的帧率会加重编码缓冲与带宽压力,得不偿失。
|
||||
if (fps > 30) fps = 30;
|
||||
if (fps <= 0) fps = 30;
|
||||
int maxFps = Math.round(refreshRate);
|
||||
if (maxFps < 30) maxFps = 30;
|
||||
this.maxCaptureFps = maxFps;
|
||||
// 根据屏幕刷新率生成适合的帧率档位,供本地 UI 与控制端选择。
|
||||
this.supportedFpsList = buildSupportedFpsList(maxFps);
|
||||
|
||||
// 初始默认 30fps:屏幕控制 30fps 已足够跟手,并显著降低 H264 硬编延迟(影响“跟手”体验)。
|
||||
// 更高的帧率会加重编码缓冲与带宽压力,用户可按需通过帧率档位手动调高。
|
||||
int fps = Math.min(30, maxFps);
|
||||
|
||||
Log.i(TAG, "Real resolution: " + realWidth + "x" + realHeight);
|
||||
Log.i(TAG, "Capture resolution: " + captureWidth + "x" + captureHeight + " @ " + fps + "fps");
|
||||
@@ -334,6 +345,36 @@ public class ScreenCaptureService extends Service {
|
||||
requestResolutionChange(longEdge, 0, 0, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅切换采集帧率(保持当前分辨率),供本地 UI 使用。
|
||||
*/
|
||||
public void changeFps(int fps) {
|
||||
requestResolutionChange(currentCaptureWidth, currentCaptureHeight, fps, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回按屏幕刷新率筛选出的适合帧率档位(升序)。
|
||||
*/
|
||||
public List<Integer> getSupportedFpsList() {
|
||||
return supportedFpsList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据屏幕最大刷新率生成帧率档位列表:从常用档位中筛选不超过刷新率的值,
|
||||
* 若刷新率本身不在常用档位中则追加(如 75/144Hz 屏幕)。
|
||||
*/
|
||||
private static List<Integer> buildSupportedFpsList(int maxFps) {
|
||||
List<Integer> list = new ArrayList<>();
|
||||
int[] candidates = {15, 24, 30, 60, 90, 120};
|
||||
for (int c : candidates) {
|
||||
if (c <= maxFps) list.add(c);
|
||||
}
|
||||
if (list.isEmpty() || list.get(list.size() - 1) < maxFps) {
|
||||
list.add(maxFps);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前采集分辨率(宽/高/帧率)。
|
||||
*/
|
||||
@@ -374,6 +415,8 @@ public class ScreenCaptureService extends Service {
|
||||
int targetH = size[1];
|
||||
int targetFps = fps > 0 ? fps : currentCaptureFps;
|
||||
if (targetFps <= 0) targetFps = 30;
|
||||
// 帧率不超过屏幕刷新率(超过无意义,只会浪费编码/带宽资源)。
|
||||
if (maxCaptureFps > 0 && targetFps > maxCaptureFps) targetFps = maxCaptureFps;
|
||||
|
||||
if (webRtcClient == null) {
|
||||
Log.w(TAG, "requestResolutionChange ignored: WebRTC not ready");
|
||||
@@ -658,6 +701,8 @@ public class ScreenCaptureService extends Service {
|
||||
// 初始化 WebRTC
|
||||
webRtcClient = new WebRtcClient(this, wsClient, deviceId);
|
||||
webRtcClient.initialize(eglBase);
|
||||
// 供 REPORT_RESOLUTION 上报给控制端,控制端据此展示帧率档位。
|
||||
webRtcClient.setSupportedFpsList(supportedFpsList);
|
||||
webRtcClient.setInputCallback(commandBytes -> inputHandler.handleCommand(commandBytes));
|
||||
webRtcClient.setIceEventListener(() -> {
|
||||
mainHandler.post(() -> {
|
||||
|
||||
@@ -220,18 +220,28 @@ public class WebRtcClient {
|
||||
* 通过 DataChannel 把当前实际采集分辨率上报给控制端(action=REPORT_RESOLUTION)。
|
||||
* 控制端(网页/Android/Flutter)据此同步展示当前分辨率,避免 UI 与实际不一致。
|
||||
*/
|
||||
/** 被控端支持的帧率档位(随 REPORT_RESOLUTION 上报给控制端)。 */
|
||||
private List<Integer> supportedFpsList;
|
||||
|
||||
public void setSupportedFpsList(List<Integer> fpsList) {
|
||||
this.supportedFpsList = fpsList;
|
||||
}
|
||||
|
||||
public void sendResolutionReport(int width, int height, int fps) {
|
||||
if (dataChannel == null || dataChannel.state() != DataChannel.State.OPEN) {
|
||||
Log.d(TAG, "sendResolutionReport skipped: dataChannel not open");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ControlMessage report = ControlMessage.newBuilder()
|
||||
ControlMessage.Builder builder = ControlMessage.newBuilder()
|
||||
.setAction(Action.REPORT_RESOLUTION)
|
||||
.setWidth(width)
|
||||
.setHeight(height)
|
||||
.setFps(fps)
|
||||
.build();
|
||||
.setFps(fps);
|
||||
if (supportedFpsList != null && !supportedFpsList.isEmpty()) {
|
||||
builder.addAllSupportedFps(supportedFpsList);
|
||||
}
|
||||
ControlMessage report = builder.build();
|
||||
ByteBuffer buffer = ByteBuffer.wrap(report.toByteArray());
|
||||
// binary=true:必须作为二进制发送,控制端按 protobuf 二进制解析;
|
||||
// 若传 false 会被当作文本,Web 端收到的将是字符串导致解码失败。
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -101,6 +101,21 @@
|
||||
android:layout_marginBottom="8dp"
|
||||
android:enabled="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_fps_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/fps_label"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_fps"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:enabled="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_current_resolution"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<string name="accessibility_dialog_cancel">取消</string>
|
||||
<string name="resolution_label">采集分辨率</string>
|
||||
<string name="resolution_changed_message">采集分辨率已切换为 %1$d×%2$d</string>
|
||||
<string name="fps_label">采集帧率</string>
|
||||
|
||||
<!-- 免密连接 -->
|
||||
<string name="connection_request_mode_none">连接方式:免密连接(被控端手动确认)</string>
|
||||
|
||||
Reference in New Issue
Block a user