feat: add root shell
This commit is contained in:
@@ -98,7 +98,7 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
|||||||
boolean systemSigned = SignatureUtils.isSystemSignature(this)
|
boolean systemSigned = SignatureUtils.isSystemSignature(this)
|
||||||
|| SignatureUtils.isSharedSystemUid(this)
|
|| SignatureUtils.isSharedSystemUid(this)
|
||||||
|| SignatureUtils.isSystemUid();
|
|| SignatureUtils.isSystemUid();
|
||||||
boolean rooted = SignatureUtils.isDeviceRooted() && SignatureUtils.isAppHasRootPermission();
|
boolean rooted = SignatureUtils.isDeviceRooted() || SignatureUtils.isAppHasRootPermission();
|
||||||
isPrivileged = systemSigned || rooted;
|
isPrivileged = systemSigned || rooted;
|
||||||
Log.e(TAG, "initData: isPrivileged = " + isPrivileged);
|
Log.e(TAG, "initData: isPrivileged = " + isPrivileged);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,15 @@ public class InputCommandHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放持有的资源。
|
||||||
|
*/
|
||||||
|
public void release() {
|
||||||
|
if (inputExecutor != null) {
|
||||||
|
inputExecutor.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleMotionEvent(ControlMessage command) {
|
private void handleMotionEvent(ControlMessage command) {
|
||||||
int action = command.getMotionAction();
|
int action = command.getMotionAction();
|
||||||
float relX = (float) command.getX();
|
float relX = (float) command.getX();
|
||||||
|
|||||||
@@ -9,4 +9,9 @@ public interface InputExecutor {
|
|||||||
void injectSwipe(int x1, int y1, int x2, int y2, long duration);
|
void injectSwipe(int x1, int y1, int x2, int y2, long duration);
|
||||||
void injectKeyEvent(int keyCode);
|
void injectKeyEvent(int keyCode);
|
||||||
void injectMotionEvent(int action, int x, int y);
|
void injectMotionEvent(int action, int x, int y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放执行器持有的资源(如持久的 Shell 进程)。
|
||||||
|
*/
|
||||||
|
default void release() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package com.ttstd.controlled.input;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 Root Shell (su) 执行注入操作的实现类。
|
||||||
|
* 解决普通 Shell 无法获取足够权限执行 input 命令的问题。
|
||||||
|
*/
|
||||||
|
public class RootShellInputUtils implements InputExecutor {
|
||||||
|
private static final String TAG = "RootShellInputUtils";
|
||||||
|
|
||||||
|
private Process mSuProcess;
|
||||||
|
private DataOutputStream mSuStream;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void injectTap(int x, int y) {
|
||||||
|
executeRootCommand(String.format(Locale.US, "input tap %d %d", x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void injectLongPress(int x, int y) {
|
||||||
|
injectSwipe(x, y, x, y, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void injectSwipe(int x1, int y1, int x2, int y2, long duration) {
|
||||||
|
executeRootCommand(String.format(Locale.US, "input swipe %d %d %d %d %d", x1, y1, x2, y2, duration));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void injectKeyEvent(int keyCode) {
|
||||||
|
executeRootCommand(String.format(Locale.US, "input keyevent %d", keyCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void injectMotionEvent(int action, int x, int y) {
|
||||||
|
// Root Shell 下执行 input 命令开销较大
|
||||||
|
if (action == 0) { // ACTION_DOWN
|
||||||
|
injectTap(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行需要 root 权限的命令。
|
||||||
|
* 使用持久的 su 进程以减少重复创建进程的开销。
|
||||||
|
*/
|
||||||
|
private synchronized void executeRootCommand(String command) {
|
||||||
|
try {
|
||||||
|
if (mSuProcess == null || mSuStream == null) {
|
||||||
|
mSuProcess = Runtime.getRuntime().exec("su");
|
||||||
|
mSuStream = new DataOutputStream(mSuProcess.getOutputStream());
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Executing root command: " + command);
|
||||||
|
mSuStream.writeBytes(command + "\n");
|
||||||
|
mSuStream.flush();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error executing root command: " + command, e);
|
||||||
|
closeSuProcess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void release() {
|
||||||
|
closeSuProcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void closeSuProcess() {
|
||||||
|
try {
|
||||||
|
if (mSuStream != null) {
|
||||||
|
mSuStream.writeBytes("exit\n");
|
||||||
|
mSuStream.flush();
|
||||||
|
mSuStream.close();
|
||||||
|
}
|
||||||
|
if (mSuProcess != null) {
|
||||||
|
mSuProcess.destroy();
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
} finally {
|
||||||
|
mSuStream = null;
|
||||||
|
mSuProcess = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void finalize() throws Throwable {
|
||||||
|
closeSuProcess();
|
||||||
|
super.finalize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -40,7 +40,9 @@ public class ShellInputUtils implements InputExecutor {
|
|||||||
private void executeShellCommand(String command) {
|
private void executeShellCommand(String command) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "Executing shell command: " + command);
|
Log.d(TAG, "Executing shell command: " + command);
|
||||||
Runtime.getRuntime().exec(command);
|
Process process = Runtime.getRuntime().exec(command);
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
Log.i(TAG, "Shell command executed with exit code " + exitCode);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Error executing shell command: " + command, e);
|
Log.e(TAG, "Error executing shell command: " + command, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import com.ttstd.controlled.accessibility.KeyboardAccessibilityService;
|
|||||||
import com.ttstd.controlled.input.AccessibilityInputUtils;
|
import com.ttstd.controlled.input.AccessibilityInputUtils;
|
||||||
import com.ttstd.controlled.input.InputCommandHandler;
|
import com.ttstd.controlled.input.InputCommandHandler;
|
||||||
import com.ttstd.controlled.input.InputExecutor;
|
import com.ttstd.controlled.input.InputExecutor;
|
||||||
|
import com.ttstd.controlled.input.RootShellInputUtils;
|
||||||
import com.ttstd.controlled.input.ShellInputUtils;
|
import com.ttstd.controlled.input.ShellInputUtils;
|
||||||
import com.ttstd.controlled.input.SystemInputUtils;
|
import com.ttstd.controlled.input.SystemInputUtils;
|
||||||
import com.ttstd.controlled.utils.SignatureUtils;
|
import com.ttstd.controlled.utils.SignatureUtils;
|
||||||
@@ -57,7 +58,6 @@ public class ScreenCaptureService extends Service {
|
|||||||
public static final String EXTRA_SERVER_URL = "server_url";
|
public static final String EXTRA_SERVER_URL = "server_url";
|
||||||
public static final String EXTRA_DEVICE_ID = "device_id";
|
public static final String EXTRA_DEVICE_ID = "device_id";
|
||||||
|
|
||||||
private MediaProjection mediaProjection;
|
|
||||||
private ScreenCapturerAndroid screenCapturer;
|
private ScreenCapturerAndroid screenCapturer;
|
||||||
private WebSocketClient wsClient;
|
private WebSocketClient wsClient;
|
||||||
private WebRtcClient webRtcClient;
|
private WebRtcClient webRtcClient;
|
||||||
@@ -65,6 +65,9 @@ public class ScreenCaptureService extends Service {
|
|||||||
private EglBase eglBase;
|
private EglBase eglBase;
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
/** 标记服务是否已经初始化,避免 onStartCommand 多次调用导致重复创建 */
|
||||||
|
private boolean isInitialized = false;
|
||||||
|
|
||||||
/** 静态实例引用,供弹窗 Activity 在无需绑定的情况下回调本服务。 */
|
/** 静态实例引用,供弹窗 Activity 在无需绑定的情况下回调本服务。 */
|
||||||
private static ScreenCaptureService instance;
|
private static ScreenCaptureService instance;
|
||||||
|
|
||||||
@@ -118,6 +121,12 @@ public class ScreenCaptureService extends Service {
|
|||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isInitialized) {
|
||||||
|
Log.i(TAG, "Service already initialized, ignoring parameters");
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
isInitialized = true;
|
||||||
|
|
||||||
// 获取屏幕真实尺寸和刷新率
|
// 获取屏幕真实尺寸和刷新率
|
||||||
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
|
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
|
||||||
DisplayMetrics dm = new DisplayMetrics();
|
DisplayMetrics dm = new DisplayMetrics();
|
||||||
@@ -177,6 +186,10 @@ public class ScreenCaptureService extends Service {
|
|||||||
Log.i(TAG, "Input executor: SystemInputUtils (system signature)");
|
Log.i(TAG, "Input executor: SystemInputUtils (system signature)");
|
||||||
return new SystemInputUtils();
|
return new SystemInputUtils();
|
||||||
}
|
}
|
||||||
|
if (SignatureUtils.isDeviceRooted() || SignatureUtils.isAppHasRootPermission()) {
|
||||||
|
Log.i(TAG, "Input executor: RootShellInputUtils (rooted)");
|
||||||
|
return new RootShellInputUtils();
|
||||||
|
}
|
||||||
if (KeyboardAccessibilityService.getInstance() != null) {
|
if (KeyboardAccessibilityService.getInstance() != null) {
|
||||||
Log.i(TAG, "Input executor: AccessibilityInputUtils (accessibility enabled)");
|
Log.i(TAG, "Input executor: AccessibilityInputUtils (accessibility enabled)");
|
||||||
return new AccessibilityInputUtils();
|
return new AccessibilityInputUtils();
|
||||||
@@ -238,18 +251,15 @@ public class ScreenCaptureService extends Service {
|
|||||||
}
|
}
|
||||||
if (eglBase != null) {
|
if (eglBase != null) {
|
||||||
eglBase.release();
|
eglBase.release();
|
||||||
|
eglBase = null;
|
||||||
}
|
}
|
||||||
if (mediaProjection != null) {
|
if (inputHandler != null) {
|
||||||
mediaProjection.stop();
|
inputHandler.release();
|
||||||
|
inputHandler = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startScreenCapture(int resultCode, Intent resultData, String serverUrl, String deviceId, int width, int height, int fps) {
|
private void startScreenCapture(int resultCode, Intent resultData, String serverUrl, String deviceId, int width, int height, int fps) {
|
||||||
// 初始化 MediaProjection
|
|
||||||
MediaProjectionManager projectionManager =
|
|
||||||
(MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
|
|
||||||
mediaProjection = projectionManager.getMediaProjection(resultCode, resultData);
|
|
||||||
|
|
||||||
// 初始化 EGL
|
// 初始化 EGL
|
||||||
eglBase = EglBase.create();
|
eglBase = EglBase.create();
|
||||||
|
|
||||||
@@ -355,10 +365,21 @@ public class ScreenCaptureService extends Service {
|
|||||||
String controllerId = pendingControllerId;
|
String controllerId = pendingControllerId;
|
||||||
controllingName = pendingControllerName;
|
controllingName = pendingControllerName;
|
||||||
clearPendingRequest();
|
clearPendingRequest();
|
||||||
|
|
||||||
|
// 1. 通过更新通知文字触发屏幕刷新(扰动方案)
|
||||||
|
// 状态栏的变化会强制 MediaProjection 产生至少一帧,解决静态画面黑屏问题。
|
||||||
|
updateNotification("正在接受 " + controllingName + " 的控制...");
|
||||||
|
|
||||||
|
// 2. 建立连接
|
||||||
webRtcClient.createPeerConnectionAndAnswer(offerSdp, controllerId);
|
webRtcClient.createPeerConnectionAndAnswer(offerSdp, controllerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateNotification(String contentText) {
|
||||||
|
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
manager.notify(NOTIFICATION_ID, createNotification(contentText));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户在弹窗中点击“拒绝”:向主控端回送拒绝通知,不建立连接。
|
* 用户在弹窗中点击“拒绝”:向主控端回送拒绝通知,不建立连接。
|
||||||
*/
|
*/
|
||||||
@@ -401,6 +422,10 @@ public class ScreenCaptureService extends Service {
|
|||||||
? controllingName : controllerId;
|
? controllingName : controllerId;
|
||||||
Log.i(TAG, "Remote controller disconnected: " + name);
|
Log.i(TAG, "Remote controller disconnected: " + name);
|
||||||
showToast(getString(R.string.controller_disconnected_message, name));
|
showToast(getString(R.string.controller_disconnected_message, name));
|
||||||
|
|
||||||
|
// 断开后恢复原始通知状态
|
||||||
|
updateNotification("远程控制服务正在运行");
|
||||||
|
|
||||||
if (stateListener != null) {
|
if (stateListener != null) {
|
||||||
stateListener.onControllerDisconnected(name);
|
stateListener.onControllerDisconnected(name);
|
||||||
}
|
}
|
||||||
@@ -446,15 +471,21 @@ public class ScreenCaptureService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Notification createNotification() {
|
private Notification createNotification() {
|
||||||
|
return createNotification("远程控制服务正在运行");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Notification createNotification(String contentText) {
|
||||||
Intent notificationIntent = new Intent(this, MainActivity.class);
|
Intent notificationIntent = new Intent(this, MainActivity.class);
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent,
|
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||||
|
|
||||||
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
.setContentTitle("屏幕共享中")
|
.setContentTitle("屏幕共享中")
|
||||||
.setContentText("远程控制服务正在运行")
|
.setContentText(contentText)
|
||||||
.setSmallIcon(android.R.drawable.ic_menu_camera)
|
.setSmallIcon(android.R.drawable.ic_menu_camera)
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.setOngoing(true)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,10 @@ public class WebRtcClient {
|
|||||||
private static final String STREAM_ID = "screen_stream";
|
private static final String STREAM_ID = "screen_stream";
|
||||||
private static final String DATA_CHANNEL_LABEL = "control_channel";
|
private static final String DATA_CHANNEL_LABEL = "control_channel";
|
||||||
|
|
||||||
// 码率优化参数(单位 kbps)。屏幕内容(文字/边缘多)对码率敏感,
|
// 码率优化参数(单位 kbps)。
|
||||||
// 过低的下限会产生色块。提高下限可显著改善画质;
|
private static final int MIN_BITRATE_KBPS = 1000; // 1 Mbps
|
||||||
// 若被控端上行带宽较弱(如蜂窝网络),请适当下调 MIN_BITRATE_KBPS。
|
private static final int START_BITRATE_KBPS = 2000; // 2 Mbps
|
||||||
private static final int MIN_BITRATE_KBPS = 5000; // 带宽估计下限,不低于此值
|
private static final int MAX_BITRATE_KBPS = 8000; // 8 Mbps
|
||||||
private static final int START_BITRATE_KBPS = 12000; // 连接初始码率猜测
|
|
||||||
private static final int MAX_BITRATE_KBPS = 40000; // 带宽估计上限
|
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final WebSocketClient wsClient;
|
private final WebSocketClient wsClient;
|
||||||
@@ -54,7 +52,9 @@ public class WebRtcClient {
|
|||||||
private PeerConnectionFactory peerConnectionFactory;
|
private PeerConnectionFactory peerConnectionFactory;
|
||||||
private PeerConnection peerConnection;
|
private PeerConnection peerConnection;
|
||||||
private DataChannel dataChannel;
|
private DataChannel dataChannel;
|
||||||
|
private VideoSource videoSource;
|
||||||
private VideoTrack videoTrack;
|
private VideoTrack videoTrack;
|
||||||
|
private org.webrtc.RtpSender videoSender;
|
||||||
private EglBase eglBase;
|
private EglBase eglBase;
|
||||||
private String currentControllerId;
|
private String currentControllerId;
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ public class WebRtcClient {
|
|||||||
|
|
||||||
SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBase.getEglBaseContext());
|
SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBase.getEglBaseContext());
|
||||||
// 对于屏幕共享,isScreencast 设为 true,这会优化细节保留
|
// 对于屏幕共享,isScreencast 设为 true,这会优化细节保留
|
||||||
VideoSource videoSource = peerConnectionFactory.createVideoSource(true);
|
videoSource = peerConnectionFactory.createVideoSource(true);
|
||||||
capturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
|
capturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
|
||||||
capturer.startCapture(width, height, fps);
|
capturer.startCapture(width, height, fps);
|
||||||
|
|
||||||
@@ -191,7 +191,8 @@ public class WebRtcClient {
|
|||||||
public void onIceConnectionChange(PeerConnection.IceConnectionState newState) {
|
public void onIceConnectionChange(PeerConnection.IceConnectionState newState) {
|
||||||
Log.d(TAG, "ICE connection state: " + newState);
|
Log.d(TAG, "ICE connection state: " + newState);
|
||||||
if (newState == PeerConnection.IceConnectionState.CONNECTED) {
|
if (newState == PeerConnection.IceConnectionState.CONNECTED) {
|
||||||
refreshVideoSource();
|
// 连接成功时,诱发编码器产生一个关键帧,而不是重启采集器
|
||||||
|
triggerKeyFrame();
|
||||||
} else if (newState == PeerConnection.IceConnectionState.DISCONNECTED
|
} else if (newState == PeerConnection.IceConnectionState.DISCONNECTED
|
||||||
|| newState == PeerConnection.IceConnectionState.FAILED
|
|| newState == PeerConnection.IceConnectionState.FAILED
|
||||||
|| newState == PeerConnection.IceConnectionState.CLOSED) {
|
|| newState == PeerConnection.IceConnectionState.CLOSED) {
|
||||||
@@ -218,7 +219,7 @@ public class WebRtcClient {
|
|||||||
peerConnection = peerConnectionFactory.createPeerConnection(config, peerObserver);
|
peerConnection = peerConnectionFactory.createPeerConnection(config, peerObserver);
|
||||||
|
|
||||||
if (videoTrack != null) {
|
if (videoTrack != null) {
|
||||||
peerConnection.addTrack(videoTrack, Collections.singletonList(STREAM_ID));
|
videoSender = peerConnection.addTrack(videoTrack, Collections.singletonList(STREAM_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注意:DataChannel 由控制端(Offer 方)创建并协商,
|
// 注意:DataChannel 由控制端(Offer 方)创建并协商,
|
||||||
@@ -363,21 +364,20 @@ public class WebRtcClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String optimizeSdp(String sdp) {
|
private String optimizeSdp(String sdp) {
|
||||||
// 使用 \n 分割以提高兼容性,并处理可能的 \r
|
|
||||||
String[] lines = sdp.split("\n");
|
String[] lines = sdp.split("\n");
|
||||||
StringBuilder newSdp = new StringBuilder();
|
StringBuilder newSdp = new StringBuilder();
|
||||||
|
|
||||||
List<String> h264Payloads = new ArrayList<>();
|
List<String> videoPayloads = new ArrayList<>();
|
||||||
// 首先找到所有的 H264 payload type (可能包含多个 profile)
|
// 识别所有视频负载类型
|
||||||
for (String line : lines) {
|
for (String line : lines) {
|
||||||
String trimmedLine = line.trim();
|
String trimmedLine = line.trim();
|
||||||
if (trimmedLine.startsWith("a=rtpmap:") && trimmedLine.contains("H264/90000")) {
|
if (trimmedLine.startsWith("a=rtpmap:") && (trimmedLine.contains("H264/90000") || trimmedLine.contains("VP8/90000") || trimmedLine.contains("VP9/90000"))) {
|
||||||
String payload = trimmedLine.split(":")[1].split(" ")[0];
|
String payload = trimmedLine.split(":")[1].split(" ")[0];
|
||||||
h264Payloads.add(payload);
|
videoPayloads.add(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (h264Payloads.isEmpty()) {
|
if (videoPayloads.isEmpty()) {
|
||||||
return sdp;
|
return sdp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,17 +386,15 @@ public class WebRtcClient {
|
|||||||
if (trimmedLine.isEmpty()) continue;
|
if (trimmedLine.isEmpty()) continue;
|
||||||
|
|
||||||
if (trimmedLine.startsWith("m=video")) {
|
if (trimmedLine.startsWith("m=video")) {
|
||||||
// 将所有 H264 payload 移到最前面以优先使用
|
|
||||||
String[] parts = trimmedLine.split(" ");
|
String[] parts = trimmedLine.split(" ");
|
||||||
if (parts.length > 3) {
|
if (parts.length > 3) {
|
||||||
StringBuilder mLine = new StringBuilder(parts[0] + " " + parts[1] + " " + parts[2]);
|
StringBuilder mLine = new StringBuilder(parts[0] + " " + parts[1] + " " + parts[2]);
|
||||||
// 先添加所有的 H264 payload
|
// 将 H264 放在最前面(如果存在)
|
||||||
for (String h264 : h264Payloads) {
|
for (String payload : videoPayloads) {
|
||||||
mLine.append(" ").append(h264);
|
mLine.append(" ").append(payload);
|
||||||
}
|
}
|
||||||
// 再添加其余的 payload
|
|
||||||
for (int i = 3; i < parts.length; i++) {
|
for (int i = 3; i < parts.length; i++) {
|
||||||
if (!h264Payloads.contains(parts[i])) {
|
if (!videoPayloads.contains(parts[i])) {
|
||||||
mLine.append(" ").append(parts[i]);
|
mLine.append(" ").append(parts[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,25 +403,18 @@ public class WebRtcClient {
|
|||||||
newSdp.append(trimmedLine).append("\r\n");
|
newSdp.append(trimmedLine).append("\r\n");
|
||||||
}
|
}
|
||||||
} else if (trimmedLine.startsWith("a=fmtp:")) {
|
} else if (trimmedLine.startsWith("a=fmtp:")) {
|
||||||
boolean isH264 = false;
|
boolean isVideoPayload = false;
|
||||||
for (String h264 : h264Payloads) {
|
for (String payload : videoPayloads) {
|
||||||
if (trimmedLine.startsWith("a=fmtp:" + h264)) {
|
if (trimmedLine.startsWith("a=fmtp:" + payload)) {
|
||||||
isH264 = true;
|
isVideoPayload = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isH264) {
|
if (isVideoPayload) {
|
||||||
// 为 H264 注入码率优化参数。
|
|
||||||
// 屏幕内容(文字/边缘)对码率敏感,过低的下限会产生色块;
|
|
||||||
// 提高下限可显著改善画质。若被控端上行带宽较弱,请适当下调 MIN。
|
|
||||||
String bonus = ";x-google-start-bitrate=" + START_BITRATE_KBPS
|
String bonus = ";x-google-start-bitrate=" + START_BITRATE_KBPS
|
||||||
+ ";x-google-max-bitrate=" + MAX_BITRATE_KBPS
|
+ ";x-google-max-bitrate=" + MAX_BITRATE_KBPS
|
||||||
+ ";x-google-min-bitrate=" + MIN_BITRATE_KBPS;
|
+ ";x-google-min-bitrate=" + MIN_BITRATE_KBPS;
|
||||||
if (!trimmedLine.contains("x-google-start-bitrate")) {
|
newSdp.append(trimmedLine).append(bonus).append("\r\n");
|
||||||
newSdp.append(trimmedLine).append(bonus).append("\r\n");
|
|
||||||
} else {
|
|
||||||
newSdp.append(trimmedLine).append("\r\n");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
newSdp.append(trimmedLine).append("\r\n");
|
newSdp.append(trimmedLine).append("\r\n");
|
||||||
}
|
}
|
||||||
@@ -464,14 +455,17 @@ public class WebRtcClient {
|
|||||||
wsClient.sendMessage(msg);
|
wsClient.sendMessage(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshVideoSource() {
|
/**
|
||||||
if (videoCapturer != null) {
|
* 通过重新应用 RtpParameters 诱发编码器生成关键帧(I-Frame)。
|
||||||
try {
|
* 相比重启采集器,这种方法更平滑,不会导致 MediaProjection 管道断开。
|
||||||
Log.d(TAG, "Refreshing video capture to force a frame update for new connection");
|
*/
|
||||||
videoCapturer.stopCapture();
|
public void triggerKeyFrame() {
|
||||||
videoCapturer.startCapture(videoWidth, videoHeight, videoFps);
|
if (videoSender != null) {
|
||||||
} catch (Exception e) {
|
Log.d(TAG, "Triggering key frame via RtpSender parameters update");
|
||||||
Log.e(TAG, "Error refreshing video capture: " + e.getMessage());
|
org.webrtc.RtpParameters parameters = videoSender.getParameters();
|
||||||
|
if (parameters != null && !parameters.encodings.isEmpty()) {
|
||||||
|
// 即使不修改任何参数,重新设置一次 setParameters 也会诱发许多编码器产生 I-Frame
|
||||||
|
videoSender.setParameters(parameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user