fix: 修复自编码模式黑屏及并发问题

- 修复 MediaCodec 回调需在 configure 前设置导致的异常
- 修复解码器线程安全问题及包乱序/重复处理
- 增加关键帧前重发 SPS/PPS 以防首帧丢失导致黑屏
- 支持分辨率切换时重置解码器
- 修复 Flutter 控制端连接类型选项 UI 显示拥挤问题
This commit is contained in:
2026-07-24 09:31:58 +08:00
parent f8048e1331
commit 778c37b8db
9 changed files with 374 additions and 107 deletions

View File

@@ -301,6 +301,11 @@ public class ScreenCaptureService extends Service {
currentCaptureHeight = targetH;
currentCaptureFps = targetFps;
// 关键:分辨率切换后,如果是自编码模式,同样需要触发扰动确保产生首帧,避免静态画面黑屏。
if (currentStreamMode == SelfCodecEncoder.STREAM_MODE_SELF_CODEC) {
updateNotification("正在切换分辨率...");
}
Log.i(TAG, "Capture resolution changed -> " + targetW + "x" + targetH + " @ " + targetFps + "fps (remote=" + fromRemote + ")");
showToast(getString(R.string.resolution_changed_message, targetW, targetH));
@@ -354,7 +359,19 @@ public class ScreenCaptureService extends Service {
if (webRtcClient != null) {
webRtcClient.stopCapture();
}
startSelfCodecEncoder();
if (!startSelfCodecEncoder()) {
// 启动失败(如 Android 14+ 不允许复用投屏授权令牌):
// 回退 WebRTC 模式并如实上报,避免两端都黑屏。
Log.e(TAG, "switch to self codec failed, fallback to webrtc mode");
if (webRtcClient != null) {
webRtcClient.startCapture();
}
currentStreamMode = SelfCodecEncoder.STREAM_MODE_WEBRTC;
reportStreamMode(SelfCodecEncoder.STREAM_MODE_WEBRTC);
return;
}
// 关键:切换到自编码模式后,立即触发一次通知刷新(扰动),确保 MediaProjection 产生首帧并喂入编码器。
updateNotification("正在使用自编码模式串流...");
} else {
// 切回 WebRTC停止自编码恢复默认屏幕采集。
stopSelfCodecEncoder();
@@ -371,10 +388,11 @@ public class ScreenCaptureService extends Service {
return currentStreamMode;
}
private void startSelfCodecEncoder() {
/** 启动自编码通道。返回 false 表示启动失败(调用方应回退 WebRTC 模式)。 */
private boolean startSelfCodecEncoder() {
if (selfEncoder != null) {
Log.w(TAG, "self codec encoder already running");
return;
return true;
}
if (webRtcClient == null || !webRtcClient.isVideoDataChannelOpen()) {
Log.w(TAG, "startSelfCodecEncoder: video channel not ready yet");
@@ -385,7 +403,7 @@ public class ScreenCaptureService extends Service {
selfMediaProjection = mpm.getMediaProjection(resultCode, resultDataIntent);
} catch (Exception e) {
Log.e(TAG, "getMediaProjection for self codec failed", e);
return;
return false;
}
int dpi = getResources().getDisplayMetrics().densityDpi;
int[] res = getCurrentCaptureResolution();
@@ -406,6 +424,7 @@ public class ScreenCaptureService extends Service {
}
});
selfEncoder.start();
return true;
}
private void stopSelfCodecEncoder() {
@@ -537,6 +556,16 @@ public class ScreenCaptureService extends Service {
webRtcClient = new WebRtcClient(this, wsClient, deviceId);
webRtcClient.initialize(eglBase);
webRtcClient.setInputCallback(commandBytes -> inputHandler.handleCommand(commandBytes));
webRtcClient.setIceEventListener(() -> {
mainHandler.post(() -> {
if (currentStreamMode == SelfCodecEncoder.STREAM_MODE_SELF_CODEC && selfEncoder != null) {
selfEncoder.requestKeyFrame();
updateNotification("远程控制已连接 (自编码)");
} else {
updateNotification("远程控制已连接");
}
});
});
// 注册远程控制断开回调:控制端断线时提示其用户名。
webRtcClient.setRemoteDisconnectCallback(controllerId ->
mainHandler.post(() -> onControllerDisconnected(controllerId)));

View File

@@ -48,6 +48,9 @@ public class SelfCodecEncoder {
private boolean running = false;
private int frameSeq = 0;
private final Object sendLock = new Object();
// 缓存 SPS/PPS 等参数集video 通道不可靠(可能丢包),在每个关键帧前重发,
// 保证控制端即使错过首次 CONFIG 也能在下一个 IDR 前完成解码器配置。
private final java.util.ArrayList<byte[]> cachedConfigs = new java.util.ArrayList<>();
public interface Callback {
void onEncoderStarted(int w, int h, int fps);
@@ -79,9 +82,10 @@ public class SelfCodecEncoder {
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
// 异步回调需在 configure() 之前设置(部分系统版本在 configure 后设置会抛异常)
encoder.setCallback(encoderCallback);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
inputSurface = encoder.createInputSurface();
encoder.setCallback(encoderCallback);
encoder.start();
virtualDisplay = mediaProjection.createVirtualDisplay(
@@ -132,8 +136,14 @@ public class SelfCodecEncoder {
mc.releaseOutputBuffer(index, false);
if (isConfig) {
cacheConfig(data);
sendUnit(UNIT_TYPE_CONFIG, 0, false, data);
} else {
if (isKey) {
// 关键帧前重发参数集:解码端已配置时会忽略重复 CONFIG
// 未配置(首次 CONFIG 丢失/通道未就绪)时借此完成配置。
resendCachedConfigs();
}
sendUnit(UNIT_TYPE_FRAME, pts, isKey, data);
}
} catch (Exception e) {
@@ -152,8 +162,39 @@ public class SelfCodecEncoder {
if (format == null) return;
ByteBuffer sps = format.getByteBuffer("csd-0");
ByteBuffer pps = format.getByteBuffer("csd-1");
if (sps != null) sendUnit(UNIT_TYPE_CONFIG, 0, false, toBytes(sps));
if (pps != null) sendUnit(UNIT_TYPE_CONFIG, 0, false, toBytes(pps));
if (sps != null) {
byte[] d = toBytes(sps);
cacheConfig(d);
sendUnit(UNIT_TYPE_CONFIG, 0, false, d);
}
if (pps != null) {
byte[] d = toBytes(pps);
cacheConfig(d);
sendUnit(UNIT_TYPE_CONFIG, 0, false, d);
}
}
/** 缓存参数集(去重),供关键帧前重发。 */
private void cacheConfig(byte[] data) {
if (data == null || data.length == 0) return;
synchronized (cachedConfigs) {
for (byte[] c : cachedConfigs) {
if (java.util.Arrays.equals(c, data)) return;
}
cachedConfigs.add(data);
}
}
/** 在每个关键帧前重发缓存的参数集。 */
private void resendCachedConfigs() {
byte[][] snapshot;
synchronized (cachedConfigs) {
if (cachedConfigs.isEmpty()) return;
snapshot = cachedConfigs.toArray(new byte[0][]);
}
for (byte[] c : snapshot) {
sendUnit(UNIT_TYPE_CONFIG, 0, false, c);
}
}
private static byte[] toBytes(ByteBuffer b) {

View File

@@ -72,6 +72,12 @@ public class WebRtcClient {
private RemoteDisconnectCallback remoteDisconnectCallback;
private boolean remoteDisconnectNotified = false;
public interface IceEventListener {
void onIceConnected();
}
private IceEventListener iceEventListener;
// 关键修复:控制端(网页)会在被控端弹出"接受"对话框期间就把 ICE 候选发过来,
// 此时 peerConnection 尚未创建 / 远端描述(offer)尚未设置,直接 addIceCandidate 会被丢弃。
// 因此先把候选缓存起来,等 setRemoteDescription(offer) 完成后再统一 flush。
@@ -101,6 +107,10 @@ public class WebRtcClient {
this.remoteDisconnectCallback = callback;
}
public void setIceEventListener(IceEventListener listener) {
this.iceEventListener = listener;
}
private void notifyRemoteDisconnected() {
if (remoteDisconnectNotified) return;
remoteDisconnectNotified = true;
@@ -266,8 +276,10 @@ public class WebRtcClient {
public void onIceConnectionChange(PeerConnection.IceConnectionState newState) {
Log.d(TAG, "ICE connection state: " + newState);
if (newState == PeerConnection.IceConnectionState.CONNECTED) {
// 连接成功时,诱发编码器产生一个关键帧,而不是重启采集器
// 连接成功时,诱发编码器产生一个关键帧
// 外部 ScreenCaptureService 可通过 registerObserver 等方式监听,或者我们直接在此处回调。
triggerKeyFrame();
if (iceEventListener != null) iceEventListener.onIceConnected();
} else if (newState == PeerConnection.IceConnectionState.DISCONNECTED
|| newState == PeerConnection.IceConnectionState.FAILED
|| newState == PeerConnection.IceConnectionState.CLOSED) {
@@ -631,4 +643,12 @@ public class WebRtcClient {
}
}
}
/** 即时请求自编码器产生一个关键帧。 */
public void requestSelfCodecKeyFrame(SelfCodecEncoder encoder) {
if (encoder != null) {
Log.d(TAG, "Requesting key frame for self codec encoder");
encoder.requestKeyFrame();
}
}
}