fix: 修复自编码模式黑屏及并发问题
- 修复 MediaCodec 回调需在 configure 前设置导致的异常 - 修复解码器线程安全问题及包乱序/重复处理 - 增加关键帧前重发 SPS/PPS 以防首帧丢失导致黑屏 - 支持分辨率切换时重置解码器 - 修复 Flutter 控制端连接类型选项 UI 显示拥挤问题
This commit is contained in:
@@ -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)));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user