feat: 增加分辨率修改
This commit is contained in:
@@ -12,8 +12,15 @@ import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
@@ -56,6 +63,7 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
// 注册状态监听,使界面能感知信令断开/控制端断开并同步显示。
|
||||
screenCaptureService.setStateListener(MainActivity.this);
|
||||
setupLocalPreview();
|
||||
setupResolutionSpinner();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -271,6 +279,82 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
||||
binding.btnStop.setEnabled(running);
|
||||
binding.etServerUrl.setEnabled(!running);
|
||||
binding.etDeviceId.setEnabled(!running);
|
||||
binding.spinnerResolution.setEnabled(running && isBound);
|
||||
binding.tvStatus.setText(running ? "状态: 运行中" : "状态: 已停止");
|
||||
}
|
||||
|
||||
/** 分辨率预设(按长边像素描述,0/负数表示使用设备原生分辨率)。 */
|
||||
private static class ResolutionPreset {
|
||||
final String label;
|
||||
final int longEdge;
|
||||
|
||||
ResolutionPreset(String label, int longEdge) {
|
||||
this.label = label;
|
||||
this.longEdge = longEdge;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
/** 构建分辨率下拉框并在本地/远端切换时同步显示当前采集分辨率。 */
|
||||
private void setupResolutionSpinner() {
|
||||
if (screenCaptureService == null) return;
|
||||
|
||||
int nativeLong = screenCaptureService.getNativeLongEdge();
|
||||
List<ResolutionPreset> presets = new ArrayList<>();
|
||||
presets.add(new ResolutionPreset("原始 (" + nativeLong + ")", nativeLong));
|
||||
presets.add(new ResolutionPreset("1080P", 1920));
|
||||
presets.add(new ResolutionPreset("720P", 1280));
|
||||
presets.add(new ResolutionPreset("480P", 854));
|
||||
|
||||
ArrayAdapter<ResolutionPreset> adapter = new ArrayAdapter<>(
|
||||
this, android.R.layout.simple_spinner_item, presets);
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
binding.spinnerResolution.setAdapter(adapter);
|
||||
|
||||
// 选中当前采集分辨率对应的预设
|
||||
int[] cur = screenCaptureService.getCurrentCaptureResolution();
|
||||
int curLong = Math.max(cur[0], cur[1]);
|
||||
int selectIdx = 0;
|
||||
int bestDiff = Integer.MAX_VALUE;
|
||||
for (int i = 0; i < presets.size(); i++) {
|
||||
int diff = Math.abs(presets.get(i).longEdge - curLong);
|
||||
if (diff < bestDiff) {
|
||||
bestDiff = diff;
|
||||
selectIdx = i;
|
||||
}
|
||||
}
|
||||
binding.spinnerResolution.setSelection(selectIdx);
|
||||
updateCurrentResolutionText(cur[0], cur[1], cur[2]);
|
||||
|
||||
// 注册监听:本地或远端切换分辨率后刷新当前分辨率文本
|
||||
screenCaptureService.setResolutionListener((w, h, fps, fromRemote) ->
|
||||
runOnUiThread(() -> updateCurrentResolutionText(w, h, fps)));
|
||||
|
||||
// 注意:先 setSelection 再设置监听,避免初始化时触发一次多余的切换。
|
||||
binding.spinnerResolution.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (screenCaptureService == null) return;
|
||||
ResolutionPreset preset = (ResolutionPreset) parent.getItemAtPosition(position);
|
||||
int[] now = screenCaptureService.getCurrentCaptureResolution();
|
||||
int nowLong = Math.max(now[0], now[1]);
|
||||
// 与当前一致则不重复切换,避免无谓的提示与编码重启
|
||||
if (preset.longEdge == nowLong) return;
|
||||
screenCaptureService.changeResolutionByLongEdge(preset.longEdge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateCurrentResolutionText(int w, int h, int fps) {
|
||||
binding.tvCurrentResolution.setText(
|
||||
String.format("当前: %dx%d @ %dfps", w, h, fps));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,13 @@ public class InputCommandHandler {
|
||||
private final int screenHeight;
|
||||
private final InputExecutor inputExecutor;
|
||||
|
||||
/** 控制端请求切换屏幕采集分辨率时的回调(由 ScreenCaptureService 实现)。 */
|
||||
public interface ResolutionRequestListener {
|
||||
void onResolutionRequested(int width, int height, int fps);
|
||||
}
|
||||
|
||||
private ResolutionRequestListener resolutionListener;
|
||||
|
||||
public InputCommandHandler(int screenWidth, int screenHeight) {
|
||||
this(screenWidth, screenHeight, new SystemInputUtils());
|
||||
}
|
||||
@@ -35,6 +42,10 @@ public class InputCommandHandler {
|
||||
this.inputExecutor = inputExecutor;
|
||||
}
|
||||
|
||||
public void setResolutionRequestListener(ResolutionRequestListener listener) {
|
||||
this.resolutionListener = listener;
|
||||
}
|
||||
|
||||
public void handleCommand(byte[] data) {
|
||||
try {
|
||||
ControlMessage command = ControlMessage.parseFrom(data);
|
||||
@@ -56,6 +67,15 @@ public class InputCommandHandler {
|
||||
case MOTION_EVENT:
|
||||
handleMotionEvent(command);
|
||||
break;
|
||||
case SET_RESOLUTION:
|
||||
// 分辨率切换不依赖输入执行器,转发给屏幕采集服务处理。
|
||||
if (resolutionListener != null) {
|
||||
resolutionListener.onResolutionRequested(
|
||||
command.getWidth(), command.getHeight(), command.getFps());
|
||||
} else {
|
||||
Log.w(TAG, "SET_RESOLUTION received but no listener registered");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Unknown action: " + action);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,13 @@ public class ScreenCaptureService extends Service {
|
||||
private String pendingOfferSdp;
|
||||
/** 当前正在控制本设备的控制端名称(用户名或设备 ID),用于断开提示。 */
|
||||
private String controllingName;
|
||||
/** 设备真实屏幕分辨率,用于分辨率切换时按宽高比计算采集尺寸。 */
|
||||
private int realScreenWidth;
|
||||
private int realScreenHeight;
|
||||
/** 当前实际采集分辨率与帧率(运行时可被本地/远端请求切换)。 */
|
||||
private int currentCaptureWidth;
|
||||
private int currentCaptureHeight;
|
||||
private int currentCaptureFps;
|
||||
/** 主线程 Handler,用于从 WebRTC 回调线程切回主线程执行 UI 提示。 */
|
||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
/** 标记服务是否正在关闭,避免重复停止/重复弹窗。 */
|
||||
@@ -137,6 +144,8 @@ public class ScreenCaptureService extends Service {
|
||||
}
|
||||
int realWidth = dm.widthPixels;
|
||||
int realHeight = dm.heightPixels;
|
||||
this.realScreenWidth = realWidth;
|
||||
this.realScreenHeight = realHeight;
|
||||
|
||||
// 分辨率缩放逻辑:如果设备分辨率大于 1080p,则等比例缩小到 1080p 以内
|
||||
int captureWidth = realWidth;
|
||||
@@ -167,8 +176,15 @@ public class ScreenCaptureService extends Service {
|
||||
Log.i(TAG, "Real resolution: " + realWidth + "x" + realHeight);
|
||||
Log.i(TAG, "Capture resolution: " + captureWidth + "x" + captureHeight + " @ " + fps + "fps");
|
||||
|
||||
// 记录初始采集分辨率,供 UI 与运行时切换使用
|
||||
this.currentCaptureWidth = captureWidth;
|
||||
this.currentCaptureHeight = captureHeight;
|
||||
this.currentCaptureFps = fps;
|
||||
|
||||
// 输入处理器必须使用真实分辨率进行坐标映射
|
||||
inputHandler = new InputCommandHandler(realWidth, realHeight, createInputExecutor());
|
||||
// 控制端发来的 SET_RESOLUTION 指令转交本服务处理(坐标映射与输入执行器无关)。
|
||||
inputHandler.setResolutionRequestListener(this::onRemoteResolutionRequested);
|
||||
|
||||
// 开启屏幕捕获,使用计算出的 capture 分辨率
|
||||
startScreenCapture(resultCode, resultData, serverUrl, deviceId, captureWidth, captureHeight, fps);
|
||||
@@ -216,6 +232,90 @@ public class ScreenCaptureService extends Service {
|
||||
return webRtcClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换屏幕采集分辨率(本地 UI 调用)。
|
||||
* @param longEdge 目标长边像素;<=0 表示使用设备原生分辨率(按屏幕宽高比缩放)。
|
||||
*/
|
||||
public void changeResolutionByLongEdge(int longEdge) {
|
||||
requestResolutionChange(longEdge, 0, 0, false);
|
||||
}
|
||||
|
||||
/** 返回当前采集分辨率(宽/高/帧率)。 */
|
||||
public int[] getCurrentCaptureResolution() {
|
||||
return new int[]{ currentCaptureWidth, currentCaptureHeight, currentCaptureFps };
|
||||
}
|
||||
|
||||
/** 返回设备原生分辨率的长边(像素),供 UI 生成“原始画质”选项。 */
|
||||
public int getNativeLongEdge() {
|
||||
return Math.max(realScreenWidth, realScreenHeight);
|
||||
}
|
||||
|
||||
/** 控制端经 DataChannel 发来的 SET_RESOLUTION 指令入口。 */
|
||||
private void onRemoteResolutionRequested(int width, int height, int fps) {
|
||||
requestResolutionChange(width, height, fps, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析并应用目标采集分辨率。
|
||||
* 语义(与控制端约定一致):
|
||||
* - width <= 0:使用设备原生分辨率。
|
||||
* - height <= 0:仅按 width 指定长边,按屏幕宽高比计算另一维度。
|
||||
* - 否则使用精确 width x height。
|
||||
* 最终宽高会偶数对齐(部分 H264 编码器要求),并触发一次关键帧刷新远端画面。
|
||||
*
|
||||
* @param width 目标宽度/长边(像素)
|
||||
* @param height 目标高度(像素,<=0 时按宽高比计算)
|
||||
* @param fps 目标帧率(<=0 时沿用当前帧率)
|
||||
* @param fromRemote 是否由远端控制端触发(仅用于 UI 提示区分)
|
||||
*/
|
||||
public void requestResolutionChange(int width, int height, int fps, boolean fromRemote) {
|
||||
int[] size = resolveCaptureSize(width, height);
|
||||
int targetW = size[0];
|
||||
int targetH = size[1];
|
||||
int targetFps = fps > 0 ? fps : currentCaptureFps;
|
||||
if (targetFps <= 0) targetFps = 30;
|
||||
|
||||
if (webRtcClient == null) {
|
||||
Log.w(TAG, "requestResolutionChange ignored: WebRTC not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
webRtcClient.changeCaptureFormat(targetW, targetH, targetFps);
|
||||
currentCaptureWidth = targetW;
|
||||
currentCaptureHeight = targetH;
|
||||
currentCaptureFps = targetFps;
|
||||
|
||||
Log.i(TAG, "Capture resolution changed -> " + targetW + "x" + targetH + " @ " + targetFps + "fps (remote=" + fromRemote + ")");
|
||||
showToast(getString(R.string.resolution_changed_message, targetW, targetH));
|
||||
|
||||
if (resolutionListener != null) {
|
||||
resolutionListener.onResolutionChanged(targetW, targetH, targetFps, fromRemote);
|
||||
}
|
||||
}
|
||||
|
||||
/** 根据指令参数解析出最终采集尺寸(保留屏幕宽高比、偶数对齐)。 */
|
||||
private int[] resolveCaptureSize(int width, int height) {
|
||||
int w, h;
|
||||
if (width <= 0) {
|
||||
w = realScreenWidth;
|
||||
h = realScreenHeight;
|
||||
} else if (height <= 0) {
|
||||
int longEdge = Math.max(realScreenWidth, realScreenHeight);
|
||||
float scale = (float) width / longEdge;
|
||||
w = Math.round(realScreenWidth * scale);
|
||||
h = Math.round(realScreenHeight * scale);
|
||||
} else {
|
||||
w = width;
|
||||
h = height;
|
||||
}
|
||||
// 偶数对齐(部分 H264 编码器要求宽高为偶数)
|
||||
if (w % 2 != 0) w--;
|
||||
if (h % 2 != 0) h--;
|
||||
if (w <= 0) w = 2;
|
||||
if (h <= 0) h = 2;
|
||||
return new int[]{ w, h };
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态回调。由已 bind 的 MainActivity 注册,用于把连接状态同步到界面。
|
||||
*/
|
||||
@@ -232,6 +332,19 @@ public class ScreenCaptureService extends Service {
|
||||
this.stateListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分辨率变化回调。由已 bind 的 MainActivity 注册,用于在界面上同步当前采集分辨率。
|
||||
*/
|
||||
public interface ResolutionChangeListener {
|
||||
void onResolutionChanged(int width, int height, int fps, boolean fromRemote);
|
||||
}
|
||||
|
||||
private ResolutionChangeListener resolutionListener;
|
||||
|
||||
public void setResolutionListener(ResolutionChangeListener listener) {
|
||||
this.resolutionListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.util.Log;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.ttstd.control.Action;
|
||||
import com.ttstd.control.ControlMessage;
|
||||
import com.ttstd.controlled.signaling.SignalMessage;
|
||||
import com.ttstd.controlled.signaling.WebSocketClient;
|
||||
|
||||
@@ -148,6 +150,69 @@ public class WebRtcClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时切换屏幕采集分辨率(无需重建采集器/连接)。
|
||||
* 通过 {@link org.webrtc.VideoCapturer#changeCaptureFormat} 平滑改变编码尺寸,
|
||||
* 并触发一次关键帧,避免远端解码器出现花屏/卡顿。
|
||||
*
|
||||
* @param width 目标宽度(像素,需为偶数)
|
||||
* @param height 目标高度(像素,需为偶数)
|
||||
* @param fps 目标帧率,<=0 时沿用当前帧率
|
||||
*/
|
||||
public void changeCaptureFormat(int width, int height, int fps) {
|
||||
if (videoCapturer == null) {
|
||||
Log.w(TAG, "changeCaptureFormat ignored: capturer not ready");
|
||||
return;
|
||||
}
|
||||
int targetFps = fps > 0 ? fps : this.videoFps;
|
||||
if (targetFps <= 0) targetFps = 30;
|
||||
Log.i(TAG, "Changing capture format -> " + width + "x" + height + " @ " + targetFps + "fps");
|
||||
try {
|
||||
videoCapturer.changeCaptureFormat(width, height, targetFps);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "changeCaptureFormat failed", e);
|
||||
return;
|
||||
}
|
||||
this.videoWidth = width;
|
||||
this.videoHeight = height;
|
||||
this.videoFps = targetFps;
|
||||
// 编码尺寸变化后诱发关键帧,使远端立即刷新画面。
|
||||
triggerKeyFrame();
|
||||
// 主动把最新采集分辨率上报给控制端,使其 UI 与实际保持一致。
|
||||
sendResolutionReport(width, height, targetFps);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 DataChannel 把当前实际采集分辨率上报给控制端(action=REPORT_RESOLUTION)。
|
||||
* 控制端(网页/Android/Flutter)据此同步展示当前分辨率,避免 UI 与实际不一致。
|
||||
*/
|
||||
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()
|
||||
.setAction(Action.REPORT_RESOLUTION)
|
||||
.setWidth(width)
|
||||
.setHeight(height)
|
||||
.setFps(fps)
|
||||
.build();
|
||||
ByteBuffer buffer = ByteBuffer.wrap(report.toByteArray());
|
||||
// binary=true:必须作为二进制发送,控制端按 protobuf 二进制解析;
|
||||
// 若传 false 会被当作文本,Web 端收到的将是字符串导致解码失败。
|
||||
dataChannel.send(new DataChannel.Buffer(buffer, true));
|
||||
Log.i(TAG, "Reported capture resolution -> " + width + "x" + height + " @ " + fps + "fps");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "sendResolutionReport failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回当前采集分辨率(像素),用于 UI 展示。 */
|
||||
public int[] getCurrentResolution() {
|
||||
return new int[]{ videoWidth, videoHeight, videoFps };
|
||||
}
|
||||
|
||||
private VideoSink localSink;
|
||||
|
||||
public void setLocalSink(VideoSink sink) {
|
||||
@@ -372,7 +437,10 @@ public class WebRtcClient {
|
||||
@Override
|
||||
public void onStateChange() {
|
||||
Log.d(TAG, "DataChannel state: " + dc.state());
|
||||
if (dc.state() == DataChannel.State.CLOSED) {
|
||||
if (dc.state() == DataChannel.State.OPEN) {
|
||||
// 通道建立后,立即把当前实际采集分辨率上报给控制端(初始值或最近一次切换值)。
|
||||
sendResolutionReport(videoWidth, videoHeight, videoFps);
|
||||
} else if (dc.state() == DataChannel.State.CLOSED) {
|
||||
notifyRemoteDisconnected();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ enum Action {
|
||||
KEY = 3;
|
||||
LONG_PRESS = 4;
|
||||
MOTION_EVENT = 5;
|
||||
SET_RESOLUTION = 6; // 控制端请求被控端切换屏幕采集分辨率
|
||||
REPORT_RESOLUTION = 7; // 被控端上报当前实际采集分辨率(含连接建立后的初始值)
|
||||
}
|
||||
|
||||
// 通过 RTCDataChannel 传输的控制指令(protobuf 二进制)。
|
||||
@@ -38,4 +40,12 @@ message ControlMessage {
|
||||
|
||||
// 原始动作(MOTION_EVENT):对应 Android MotionEvent ACTION_*(0=DOWN 1=UP 2=MOVE)
|
||||
int32 motion_action = 11;
|
||||
|
||||
// 分辨率切换(SET_RESOLUTION):
|
||||
// width 为目标长边/宽度;<=0 表示使用被控端原始(native)分辨率。
|
||||
// height 为目标高度;<=0 时按被控端屏幕宽高比基于 width 计算(保留原始比例)。
|
||||
// fps 为目标帧率;<=0 表示沿用当前帧率。
|
||||
int32 width = 12;
|
||||
int32 height = 13;
|
||||
int32 fps = 14;
|
||||
}
|
||||
|
||||
@@ -76,6 +76,30 @@
|
||||
android:enabled="false"
|
||||
android:text="停止屏幕共享" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_resolution_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/resolution_label"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_resolution"
|
||||
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"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="当前: -"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/local_view"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -13,4 +13,6 @@
|
||||
<string name="accessibility_dialog_message">当前应用未使用系统签名,也未获取 Root 权限。如需在远程控制时模拟键盘与触摸输入,请前往系统设置开启本应用的无障碍服务。</string>
|
||||
<string name="accessibility_dialog_go">去开启</string>
|
||||
<string name="accessibility_dialog_cancel">取消</string>
|
||||
<string name="resolution_label">采集分辨率</string>
|
||||
<string name="resolution_changed_message">采集分辨率已切换为 %1$d×%2$d</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user