refactor(self-codec): 复用 WebRTC 采集管线适配 Android 14

自编码模式不再单独申请 MediaProjection 和 VirtualDisplay,
改为复用 WebRTC 的屏幕采集帧直接喂入编码器,规避 Android 14
单次投屏授权令牌限制。同时实现 I420 到 NV12 的颜色格式转换。
This commit is contained in:
TongTongStudio
2026-07-24 22:27:08 +08:00
parent 3792b75244
commit 7fc02ec73d
3 changed files with 228 additions and 101 deletions

View File

@@ -105,11 +105,10 @@ public class ScreenCaptureService extends Service {
private boolean isShuttingDown = false;
// ---- 屏幕串流"自编码"模式 ----
private int resultCode; // 屏幕捕获授权结果码(供自编码模式重新获取 MediaProjection
private int resultCode; // 屏幕捕获授权结果码(供 WebRTC 采集使用
private Intent resultDataIntent; // 屏幕捕获授权 Intent同上
private int currentStreamMode = SelfCodecEncoder.STREAM_MODE_WEBRTC;
private MediaProjection selfMediaProjection; // 自编码模式独占的 MediaProjection
private SelfCodecEncoder selfEncoder; // 自编码 H264 编码器
private SelfCodecEncoder selfEncoder; // 自编码 H264 编码器(复用 WebRTC 采集帧,不单独申请虚拟显示)
@Override
public void onCreate() {
@@ -407,29 +406,21 @@ public class ScreenCaptureService extends Service {
return;
}
if (mode == SelfCodecEncoder.STREAM_MODE_SELF_CODEC) {
// 切到自编码:先停 WebRTC 默认屏幕采集(释放其 MediaProjection再启动自编码。
if (webRtcClient != null) {
webRtcClient.stopCapture();
}
// 切到自编码WebRTC 屏幕采集保持运行(共享同一条采集管线),
// 自编码编码器只是额外"旁路"这些帧,不再单独申请 VirtualDisplay
// 因此无需 stop/startCapture规避 Android 14 单次投屏授权令牌的限制。
if (!startSelfCodecEncoder()) {
// 启动失败(如 Android 14+ 不允许复用投屏授权令牌):
// 回退 WebRTC 模式并如实上报,避免两端都黑屏。
Log.e(TAG, "switch to self codec failed, fallback to webrtc mode");
if (webRtcClient != null) {
webRtcClient.startCapture();
}
// 启动失败:保持 WebRTC 模式并如实上报,避免黑屏。
Log.e(TAG, "switch to self codec failed, stay webrtc mode");
currentStreamMode = SelfCodecEncoder.STREAM_MODE_WEBRTC;
reportStreamMode(SelfCodecEncoder.STREAM_MODE_WEBRTC);
return;
}
// 关键:切换到自编码模式后,立即触发一次通知刷新(扰动),确保 MediaProjection 产生首帧并喂入编码器。
updateNotification("正在使用自编码模式串流...");
} else {
// 切回 WebRTC停止自编码,恢复默认屏幕采集
// 切回 WebRTC停止自编码旁路WebRTC 采集与媒体流本就一直在线
stopSelfCodecEncoder();
if (webRtcClient != null) {
webRtcClient.startCapture();
}
updateNotification("正在使用 WebRTC 模式串流...");
}
currentStreamMode = mode;
reportStreamMode(mode);
@@ -449,18 +440,12 @@ public class ScreenCaptureService extends Service {
if (webRtcClient == null || !webRtcClient.isVideoDataChannelOpen()) {
Log.w(TAG, "startSelfCodecEncoder: video channel not ready yet");
}
MediaProjectionManager mpm =
(MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
try {
selfMediaProjection = mpm.getMediaProjection(resultCode, resultDataIntent);
} catch (Exception e) {
Log.e(TAG, "getMediaProjection for self codec failed", e);
return false;
}
int dpi = getResources().getDisplayMetrics().densityDpi;
int[] res = getCurrentCaptureResolution();
// 注意:自编码编码器不再创建自己的 MediaProjection/VirtualDisplay
// 而是复用 WebRTC 的屏幕采集帧(通过 WebRtcClient.addSink 转发),
// 从根本上消除 Android 14 上"无法复用投屏令牌 / 无法创建第二个虚拟显示"的崩溃。
selfEncoder = new SelfCodecEncoder(
selfMediaProjection, res[0], res[1], res[2], dpi,
res[0], res[1], res[2],
webRtcClient != null ? webRtcClient.getVideoDataChannel() : null,
new SelfCodecEncoder.Callback() {
@Override
@@ -483,21 +468,23 @@ public class ScreenCaptureService extends Service {
stopSelfCodecEncoder();
return false;
}
// 立即请求一个关键帧IDR让控制端尽快拿到 SPS/PPS 并完成解码器配置。
selfEncoder.requestKeyFrame();
// 把 WebRTC 采集到的每一帧旁路给自编码编码器
if (webRtcClient != null) {
webRtcClient.setSelfCodecFrameListener(frame -> selfEncoder.feedFrame(frame));
}
return true;
}
private void stopSelfCodecEncoder() {
if (webRtcClient != null) {
webRtcClient.setSelfCodecFrameListener(null);
}
if (selfEncoder != null) {
selfEncoder.release();
selfEncoder = null;
}
if (selfMediaProjection != null) {
try {
selfMediaProjection.stop();
} catch (Exception ignored) {
}
selfMediaProjection = null;
}
}
/** 经 control DataChannel 向控制端回报当前生效的串流模式。 */
@@ -560,10 +547,6 @@ public class ScreenCaptureService extends Service {
selfEncoder.release();
selfEncoder = null;
}
if (selfMediaProjection != null) {
selfMediaProjection.stop();
selfMediaProjection = null;
}
if (webRtcClient != null) {
webRtcClient.close();
}

View File

@@ -1,25 +1,33 @@
package com.ttstd.controlled.webrtc;
import android.media.Image;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.media.projection.MediaProjection;
import android.hardware.display.VirtualDisplay;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
import android.util.Log;
import android.view.Surface;
import org.webrtc.DataChannel;
import org.webrtc.VideoFrame;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayDeque;
/**
* 自建屏幕编码通道:用 MediaProjection 的 VirtualDisplay 把屏幕投影到 MediaCodec
* 的输入 Surface编码为 H.264 裸流后,经 WebRTC 的 video DataChannel 分片透传给
* 控制端(控制端使用自建 MediaCodec 解码渲染)。绕开 WebRTC 默认媒体轨道,使编码/
* 解码参数完全可控(码率、关键帧、低延迟调优)。
* 自建屏幕编码通道:WebRTC 的屏幕采集管线ScreenCapturerAndroid 的 VideoFrame
* 把每一帧编码为 H.264 裸流后,经 WebRTC 的 video DataChannel 分片透传给控制端
* (控制端使用自建 MediaCodec 解码渲染)。
*
* 设计要点Android 14 兼容):
* 在 Android 14+ 上MediaProjection 授权令牌为一次性,且一个 MediaProjection 实例
* 只能 createVirtualDisplay 一次。因此自编码模式【不再】自己申请 MediaProjection /
* VirtualDisplay而是复用 WebRTC 已经建立的、唯一的屏幕采集管线,从 VideoFrame 直接编码。
* 这样全程只有一个虚拟显示、一个令牌,既避免了
* "Must register a callback before starting capture"
* 也避免了 "Don't re-use the resultData ... Don't take multiple captures ..." 两类崩溃,
* 同时运行时可在 WebRTC 与自编码模式间自由切换(共享同一条采集)。
*/
public class SelfCodecEncoder {
@@ -34,21 +42,19 @@ public class SelfCodecEncoder {
private static final String TAG = "SelfCodecEncoder";
private final MediaProjection mediaProjection;
private final int width;
private final int height;
private final int fps;
private final int dpi;
private final DataChannel videoChannel;
private final Callback callback;
private MediaCodec encoder;
private Surface inputSurface;
private VirtualDisplay virtualDisplay;
private MediaProjection.Callback projectionCallback;
private boolean running = false;
private int frameSeq = 0;
private final Object sendLock = new Object();
private final Object inputLock = new Object();
private final ArrayDeque<Integer> freeInputIndices = new ArrayDeque<>();
private byte[] nv12Buffer;
// 缓存 SPS/PPS 等参数集video 通道不可靠(可能丢包),在每个关键帧前重发,
// 保证控制端即使错过首次 CONFIG 也能在下一个 IDR 前完成解码器配置。
private final java.util.ArrayList<byte[]> cachedConfigs = new java.util.ArrayList<>();
@@ -59,13 +65,10 @@ public class SelfCodecEncoder {
void onEncoderError(String error);
}
public SelfCodecEncoder(MediaProjection mp, int width, int height, int fps, int dpi,
DataChannel videoChannel, Callback cb) {
this.mediaProjection = mp;
public SelfCodecEncoder(int width, int height, int fps, DataChannel videoChannel, Callback cb) {
this.width = width;
this.height = height;
this.fps = fps;
this.dpi = dpi;
this.videoChannel = videoChannel;
this.callback = cb;
}
@@ -78,7 +81,7 @@ public class SelfCodecEncoder {
format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
format.setInteger(MediaFormat.KEY_CAPTURE_RATE, fps);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, selectColorFormat());
// 关键帧间隔1 秒一个 IDR便于断流后快速恢复也可经 requestKeyFrame 即时强求
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
@@ -86,27 +89,9 @@ public class SelfCodecEncoder {
// 异步回调需在 configure() 之前设置(部分系统版本在 configure 后设置会抛异常)
encoder.setCallback(encoderCallback);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
inputSurface = encoder.createInputSurface();
encoder.start();
// Android 14+ (API 34) 强制要求:在 createVirtualDisplay 之前必须先注册
// MediaProjection.Callback否则会抛
// IllegalStateException("Must register a callback before starting capture...")
// 导致自编码模式无法启动、控制端黑屏。onStop 时释放资源。
projectionCallback = new MediaProjection.Callback() {
@Override
public void onStop() {
Log.e(TAG, "MediaProjection stopped, releasing encoder");
release();
if (callback != null) callback.onEncoderError("MediaProjection stopped");
}
};
mediaProjection.registerCallback(projectionCallback, null);
virtualDisplay = mediaProjection.createVirtualDisplay(
"SelfCodec", width, height, dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, inputSurface, null, null);
nv12Buffer = new byte[width * height * 3 / 2];
running = true;
if (callback != null) callback.onEncoderStarted(width, height, fps);
Log.i(TAG, "self codec encoder started: " + width + "x" + height + "@" + fps + ", bitrate=" + bitrate);
@@ -117,15 +102,166 @@ public class SelfCodecEncoder {
}
}
/** 选择合适的 YUV 输入颜色格式(优先 NV12 系;几乎全部 AVC 编码器均支持)。 */
private int selectColorFormat() {
try {
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
MediaCodecInfo info = null;
for (MediaCodecInfo c : list.getCodecInfos()) {
if (!c.isEncoder()) continue;
for (String t : c.getSupportedTypes()) {
if (t.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AVC)) {
info = c;
break;
}
}
if (info != null) break;
}
if (info == null) return MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;
MediaCodecInfo.CodecCapabilities caps = info.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC);
// 优先 NV12COLOR_FormatYUV420SemiPlanar其输入 Image 为 2 平面Y + UV 交错),
// 与下方 writeNv12 的写入逻辑严格一致。仅在设备不支持 NV12 时才退而求其次用 Flexible。
for (int f : caps.colorFormats) {
if (f == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar) {
return f;
}
}
for (int f : caps.colorFormats) {
if (f == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible) {
return f;
}
}
return MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;
} catch (Exception e) {
return MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;
}
}
private int computeBitrate(int w, int h, int fps) {
int b = (int) (w * h * fps * 0.12);
return Math.max(500_000, Math.min(8_000_000, b));
}
/**
* 由 WebRTC 采集管线喂入一帧(共享同一条屏幕采集,无需再申请 VirtualDisplay
* 当编码器输入缓冲不足时会丢弃该帧(编码器跟不上),保证不堆积、不阻塞采集线程。
*/
public void feedFrame(VideoFrame frame) {
if (!running || encoder == null) return;
Integer idx;
synchronized (inputLock) {
idx = freeInputIndices.poll();
}
if (idx == null) {
return; // 丢掉这一帧,等待下一个空闲输入缓冲
}
try {
encodeFrame(idx, frame);
} catch (Exception e) {
Log.e(TAG, "encodeFrame failed", e);
try {
encoder.queueInputBuffer(idx, 0, 0, System.nanoTime() / 1000, 0);
} catch (Exception ignored) {
}
}
}
private void encodeFrame(int index, VideoFrame frame) {
long ptsUs = System.nanoTime() / 1000;
VideoFrame.I420Buffer i420 = frame.getBuffer().toI420();
int w = i420.getWidth();
int h = i420.getHeight();
ensureNv12Buffer(w, h);
i420ToNv12(i420, nv12Buffer, w, h);
i420.release();
writeNv12(index, nv12Buffer, w, h, ptsUs);
}
private void ensureNv12Buffer(int w, int h) {
int need = w * h * 3 / 2;
if (nv12Buffer == null || nv12Buffer.length < need) {
nv12Buffer = new byte[need];
}
}
/** I420三平面→ NV12Y + UV 交错),写入 out。 */
private void i420ToNv12(VideoFrame.I420Buffer i420, byte[] out, int w, int h) {
ByteBuffer y = i420.getDataY();
ByteBuffer u = i420.getDataU();
ByteBuffer v = i420.getDataV();
int yStride = i420.getStrideY();
int uStride = i420.getStrideU();
int vStride = i420.getStrideV();
int off = 0;
for (int r = 0; r < h; r++) {
y.position(r * yStride);
y.get(out, off, w);
off += w;
}
int cw = w / 2;
int ch = h / 2;
for (int r = 0; r < ch; r++) {
for (int c = 0; c < cw; c++) {
out[off++] = u.get(r * uStride + c);
out[off++] = v.get(r * vStride + c);
}
}
}
/** 把 NV12 按编码器输入 Image 的 stride 写入输入缓冲并入队。 */
private void writeNv12(int index, byte[] nv12, int w, int h, long ptsUs) {
Image image = encoder.getInputImage(index);
if (image == null) {
encoder.queueInputBuffer(index, 0, 0, ptsUs, 0);
return;
}
try {
Image.Plane yPlane = image.getPlanes()[0];
Image.Plane uvPlane = image.getPlanes()[1];
ByteBuffer yBuf = yPlane.getBuffer();
ByteBuffer uvBuf = uvPlane.getBuffer();
int yRow = yPlane.getRowStride();
int uvRow = uvPlane.getRowStride();
int yPix = yPlane.getPixelStride();
int uvPix = uvPlane.getPixelStride();
int src = 0;
for (int r = 0; r < h; r++) {
int dst = r * yRow;
for (int c = 0; c < w; c++) {
yBuf.put(dst + c * yPix, nv12[src + c]);
}
src += w;
}
int ch = h / 2;
int cw = w / 2;
int uvSrc = w * h;
for (int r = 0; r < ch; r++) {
int sRow = uvSrc + r * cw * 2;
int dRow = r * uvRow;
for (int c = 0; c < cw; c++) {
int s = sRow + c * 2;
int d = dRow + c * uvPix;
uvBuf.put(d, nv12[s]);
uvBuf.put(d + 1, nv12[s + 1]);
}
}
encoder.queueInputBuffer(index, 0, w * h * 3 / 2, ptsUs, 0);
} catch (Exception e) {
Log.e(TAG, "writeNv12 failed", e);
try {
encoder.queueInputBuffer(index, 0, 0, ptsUs, 0);
} catch (Exception ignored) {
}
}
}
private final MediaCodec.Callback encoderCallback = new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec mc, int index) {
// 使用 Surface 输入,编码器自动从输入 Surface 取帧,无需手动喂数据
synchronized (inputLock) {
freeInputIndices.add(index);
}
}
@Override
@@ -261,11 +397,6 @@ public class SelfCodecEncoder {
}
}
/** 返回编码器是否正在运行(供上层判断启动结果)。 */
public boolean isRunning() {
return running;
}
/** 即时请求一个关键帧(强求 IDR。 */
public void requestKeyFrame() {
if (encoder == null) return;
@@ -278,22 +409,14 @@ public class SelfCodecEncoder {
}
}
/** 返回编码器是否正在运行(供上层判断启动结果)。 */
public boolean isRunning() {
return running;
}
public void release() {
running = false;
if (mediaProjection != null && projectionCallback != null) {
try {
mediaProjection.unregisterCallback(projectionCallback);
} catch (Exception ignored) {
}
projectionCallback = null;
}
if (virtualDisplay != null) {
try {
virtualDisplay.release();
} catch (Exception ignored) {
}
virtualDisplay = null;
}
freeInputIndices.clear();
if (encoder != null) {
try {
encoder.stop();
@@ -305,9 +428,6 @@ public class SelfCodecEncoder {
}
encoder = null;
}
if (inputSurface != null) {
inputSurface.release();
inputSurface = null;
}
nv12Buffer = null;
}
}

View File

@@ -69,6 +69,13 @@ public class WebRtcClient {
private InputCommandCallback inputCallback;
/** 自编码模式帧监听器WebRTC 采集到的每一帧都会转发给自编码编码器(共享同一条屏幕采集)。 */
public interface SelfCodecFrameListener {
void onFrame(org.webrtc.VideoFrame frame);
}
private SelfCodecFrameListener selfCodecFrameListener;
private RemoteDisconnectCallback remoteDisconnectCallback;
private boolean remoteDisconnectNotified = false;
@@ -163,6 +170,18 @@ public class WebRtcClient {
if (localSink != null) {
videoTrack.addSink(localSink);
}
// 把视频轨道的每一帧再转发给自编码帧监听器(如果已设置)。
// 只在此处添加一次videoTrack 仅创建一次),避免重复连接时重复挂 sink 导致重复编码。
// 自编码模式复用同一条屏幕采集管线,无需再申请第二个 VirtualDisplay
// 兼容 Android 14 单次投屏授权令牌的限制。
videoTrack.addSink(new VideoSink() {
@Override
public void onFrame(org.webrtc.VideoFrame frame) {
if (selfCodecFrameListener != null) {
selfCodecFrameListener.onFrame(frame);
}
}
});
}
/**
@@ -347,7 +366,7 @@ public class WebRtcClient {
@Override
public void onSetSuccess() {
Log.e(TAG, "onSetSuccess: modifiedSdp = " + modifiedSdpDescription);
Log.d(TAG, "onSetSuccess: modifiedSdp = " + modifiedSdpDescription);
sendAnswer(modifiedSdpDescription, controllerId);
}
@@ -466,6 +485,11 @@ public class WebRtcClient {
return videoDataChannel != null && videoDataChannel.state() == DataChannel.State.OPEN;
}
/** 设置自编码帧监听器:为 null 时不再转发采集帧WebRTC 模式)。 */
public void setSelfCodecFrameListener(SelfCodecFrameListener listener) {
this.selfCodecFrameListener = listener;
}
/** 暂停 WebRTC 默认屏幕采集(切到自编码模式时调用)。 */
public void stopCapture() {
if (videoCapturer != null) {