refactor(self-codec): 复用 WebRTC 采集管线适配 Android 14
自编码模式不再单独申请 MediaProjection 和 VirtualDisplay, 改为复用 WebRTC 的屏幕采集帧直接喂入编码器,规避 Android 14 单次投屏授权令牌限制。同时实现 I420 到 NV12 的颜色格式转换。
This commit is contained in:
@@ -105,11 +105,10 @@ public class ScreenCaptureService extends Service {
|
|||||||
private boolean isShuttingDown = false;
|
private boolean isShuttingDown = false;
|
||||||
|
|
||||||
// ---- 屏幕串流"自编码"模式 ----
|
// ---- 屏幕串流"自编码"模式 ----
|
||||||
private int resultCode; // 屏幕捕获授权结果码(供自编码模式重新获取 MediaProjection)
|
private int resultCode; // 屏幕捕获授权结果码(供 WebRTC 采集使用)
|
||||||
private Intent resultDataIntent; // 屏幕捕获授权 Intent(同上)
|
private Intent resultDataIntent; // 屏幕捕获授权 Intent(同上)
|
||||||
private int currentStreamMode = SelfCodecEncoder.STREAM_MODE_WEBRTC;
|
private int currentStreamMode = SelfCodecEncoder.STREAM_MODE_WEBRTC;
|
||||||
private MediaProjection selfMediaProjection; // 自编码模式独占的 MediaProjection
|
private SelfCodecEncoder selfEncoder; // 自编码 H264 编码器(复用 WebRTC 采集帧,不单独申请虚拟显示)
|
||||||
private SelfCodecEncoder selfEncoder; // 自编码 H264 编码器
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
@@ -407,29 +406,21 @@ public class ScreenCaptureService extends Service {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mode == SelfCodecEncoder.STREAM_MODE_SELF_CODEC) {
|
if (mode == SelfCodecEncoder.STREAM_MODE_SELF_CODEC) {
|
||||||
// 切到自编码:先停 WebRTC 默认屏幕采集(释放其 MediaProjection),再启动自编码。
|
// 切到自编码:WebRTC 的屏幕采集保持运行(共享同一条采集管线),
|
||||||
if (webRtcClient != null) {
|
// 自编码编码器只是额外"旁路"这些帧,不再单独申请 VirtualDisplay,
|
||||||
webRtcClient.stopCapture();
|
// 因此无需 stop/startCapture,规避 Android 14 单次投屏授权令牌的限制。
|
||||||
}
|
|
||||||
if (!startSelfCodecEncoder()) {
|
if (!startSelfCodecEncoder()) {
|
||||||
// 启动失败(如 Android 14+ 不允许复用投屏授权令牌):
|
// 启动失败:保持 WebRTC 模式并如实上报,避免黑屏。
|
||||||
// 回退 WebRTC 模式并如实上报,避免两端都黑屏。
|
Log.e(TAG, "switch to self codec failed, stay webrtc mode");
|
||||||
Log.e(TAG, "switch to self codec failed, fallback to webrtc mode");
|
|
||||||
if (webRtcClient != null) {
|
|
||||||
webRtcClient.startCapture();
|
|
||||||
}
|
|
||||||
currentStreamMode = SelfCodecEncoder.STREAM_MODE_WEBRTC;
|
currentStreamMode = SelfCodecEncoder.STREAM_MODE_WEBRTC;
|
||||||
reportStreamMode(SelfCodecEncoder.STREAM_MODE_WEBRTC);
|
reportStreamMode(SelfCodecEncoder.STREAM_MODE_WEBRTC);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 关键:切换到自编码模式后,立即触发一次通知刷新(扰动),确保 MediaProjection 产生首帧并喂入编码器。
|
|
||||||
updateNotification("正在使用自编码模式串流...");
|
updateNotification("正在使用自编码模式串流...");
|
||||||
} else {
|
} else {
|
||||||
// 切回 WebRTC:停止自编码,恢复默认屏幕采集。
|
// 切回 WebRTC:停止自编码旁路,WebRTC 采集与媒体流本就一直在线。
|
||||||
stopSelfCodecEncoder();
|
stopSelfCodecEncoder();
|
||||||
if (webRtcClient != null) {
|
updateNotification("正在使用 WebRTC 模式串流...");
|
||||||
webRtcClient.startCapture();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
currentStreamMode = mode;
|
currentStreamMode = mode;
|
||||||
reportStreamMode(mode);
|
reportStreamMode(mode);
|
||||||
@@ -449,18 +440,12 @@ public class ScreenCaptureService extends Service {
|
|||||||
if (webRtcClient == null || !webRtcClient.isVideoDataChannelOpen()) {
|
if (webRtcClient == null || !webRtcClient.isVideoDataChannelOpen()) {
|
||||||
Log.w(TAG, "startSelfCodecEncoder: video channel not ready yet");
|
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();
|
int[] res = getCurrentCaptureResolution();
|
||||||
|
// 注意:自编码编码器不再创建自己的 MediaProjection/VirtualDisplay,
|
||||||
|
// 而是复用 WebRTC 的屏幕采集帧(通过 WebRtcClient.addSink 转发),
|
||||||
|
// 从根本上消除 Android 14 上"无法复用投屏令牌 / 无法创建第二个虚拟显示"的崩溃。
|
||||||
selfEncoder = new SelfCodecEncoder(
|
selfEncoder = new SelfCodecEncoder(
|
||||||
selfMediaProjection, res[0], res[1], res[2], dpi,
|
res[0], res[1], res[2],
|
||||||
webRtcClient != null ? webRtcClient.getVideoDataChannel() : null,
|
webRtcClient != null ? webRtcClient.getVideoDataChannel() : null,
|
||||||
new SelfCodecEncoder.Callback() {
|
new SelfCodecEncoder.Callback() {
|
||||||
@Override
|
@Override
|
||||||
@@ -483,21 +468,23 @@ public class ScreenCaptureService extends Service {
|
|||||||
stopSelfCodecEncoder();
|
stopSelfCodecEncoder();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// 立即请求一个关键帧(IDR),让控制端尽快拿到 SPS/PPS 并完成解码器配置。
|
||||||
|
selfEncoder.requestKeyFrame();
|
||||||
|
// 把 WebRTC 采集到的每一帧旁路给自编码编码器
|
||||||
|
if (webRtcClient != null) {
|
||||||
|
webRtcClient.setSelfCodecFrameListener(frame -> selfEncoder.feedFrame(frame));
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopSelfCodecEncoder() {
|
private void stopSelfCodecEncoder() {
|
||||||
|
if (webRtcClient != null) {
|
||||||
|
webRtcClient.setSelfCodecFrameListener(null);
|
||||||
|
}
|
||||||
if (selfEncoder != null) {
|
if (selfEncoder != null) {
|
||||||
selfEncoder.release();
|
selfEncoder.release();
|
||||||
selfEncoder = null;
|
selfEncoder = null;
|
||||||
}
|
}
|
||||||
if (selfMediaProjection != null) {
|
|
||||||
try {
|
|
||||||
selfMediaProjection.stop();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
selfMediaProjection = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 经 control DataChannel 向控制端回报当前生效的串流模式。 */
|
/** 经 control DataChannel 向控制端回报当前生效的串流模式。 */
|
||||||
@@ -560,10 +547,6 @@ public class ScreenCaptureService extends Service {
|
|||||||
selfEncoder.release();
|
selfEncoder.release();
|
||||||
selfEncoder = null;
|
selfEncoder = null;
|
||||||
}
|
}
|
||||||
if (selfMediaProjection != null) {
|
|
||||||
selfMediaProjection.stop();
|
|
||||||
selfMediaProjection = null;
|
|
||||||
}
|
|
||||||
if (webRtcClient != null) {
|
if (webRtcClient != null) {
|
||||||
webRtcClient.close();
|
webRtcClient.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,33 @@
|
|||||||
package com.ttstd.controlled.webrtc;
|
package com.ttstd.controlled.webrtc;
|
||||||
|
|
||||||
|
import android.media.Image;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
|
import android.media.MediaCodecList;
|
||||||
import android.media.MediaFormat;
|
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.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Surface;
|
|
||||||
|
|
||||||
import org.webrtc.DataChannel;
|
import org.webrtc.DataChannel;
|
||||||
|
import org.webrtc.VideoFrame;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自建屏幕编码通道:用 MediaProjection 的 VirtualDisplay 把屏幕投影到 MediaCodec
|
* 自建屏幕编码通道:复用 WebRTC 的屏幕采集管线(ScreenCapturerAndroid 的 VideoFrame),
|
||||||
* 的输入 Surface,编码为 H.264 裸流后,经 WebRTC 的 video DataChannel 分片透传给
|
* 把每一帧编码为 H.264 裸流后,经 WebRTC 的 video DataChannel 分片透传给控制端
|
||||||
* 控制端(控制端使用自建 MediaCodec 解码渲染)。绕开 WebRTC 默认媒体轨道,使编码/
|
* (控制端使用自建 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 {
|
public class SelfCodecEncoder {
|
||||||
|
|
||||||
@@ -34,21 +42,19 @@ public class SelfCodecEncoder {
|
|||||||
|
|
||||||
private static final String TAG = "SelfCodecEncoder";
|
private static final String TAG = "SelfCodecEncoder";
|
||||||
|
|
||||||
private final MediaProjection mediaProjection;
|
|
||||||
private final int width;
|
private final int width;
|
||||||
private final int height;
|
private final int height;
|
||||||
private final int fps;
|
private final int fps;
|
||||||
private final int dpi;
|
|
||||||
private final DataChannel videoChannel;
|
private final DataChannel videoChannel;
|
||||||
private final Callback callback;
|
private final Callback callback;
|
||||||
|
|
||||||
private MediaCodec encoder;
|
private MediaCodec encoder;
|
||||||
private Surface inputSurface;
|
|
||||||
private VirtualDisplay virtualDisplay;
|
|
||||||
private MediaProjection.Callback projectionCallback;
|
|
||||||
private boolean running = false;
|
private boolean running = false;
|
||||||
private int frameSeq = 0;
|
private int frameSeq = 0;
|
||||||
private final Object sendLock = new Object();
|
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 通道不可靠(可能丢包),在每个关键帧前重发,
|
// 缓存 SPS/PPS 等参数集:video 通道不可靠(可能丢包),在每个关键帧前重发,
|
||||||
// 保证控制端即使错过首次 CONFIG 也能在下一个 IDR 前完成解码器配置。
|
// 保证控制端即使错过首次 CONFIG 也能在下一个 IDR 前完成解码器配置。
|
||||||
private final java.util.ArrayList<byte[]> cachedConfigs = new java.util.ArrayList<>();
|
private final java.util.ArrayList<byte[]> cachedConfigs = new java.util.ArrayList<>();
|
||||||
@@ -59,13 +65,10 @@ public class SelfCodecEncoder {
|
|||||||
void onEncoderError(String error);
|
void onEncoderError(String error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SelfCodecEncoder(MediaProjection mp, int width, int height, int fps, int dpi,
|
public SelfCodecEncoder(int width, int height, int fps, DataChannel videoChannel, Callback cb) {
|
||||||
DataChannel videoChannel, Callback cb) {
|
|
||||||
this.mediaProjection = mp;
|
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.fps = fps;
|
this.fps = fps;
|
||||||
this.dpi = dpi;
|
|
||||||
this.videoChannel = videoChannel;
|
this.videoChannel = videoChannel;
|
||||||
this.callback = cb;
|
this.callback = cb;
|
||||||
}
|
}
|
||||||
@@ -78,7 +81,7 @@ public class SelfCodecEncoder {
|
|||||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
|
format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
|
||||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
|
||||||
format.setInteger(MediaFormat.KEY_CAPTURE_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 即时强求
|
// 关键帧间隔:1 秒一个 IDR,便于断流后快速恢复;也可经 requestKeyFrame 即时强求
|
||||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
|
||||||
|
|
||||||
@@ -86,27 +89,9 @@ public class SelfCodecEncoder {
|
|||||||
// 异步回调需在 configure() 之前设置(部分系统版本在 configure 后设置会抛异常)
|
// 异步回调需在 configure() 之前设置(部分系统版本在 configure 后设置会抛异常)
|
||||||
encoder.setCallback(encoderCallback);
|
encoder.setCallback(encoderCallback);
|
||||||
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||||
inputSurface = encoder.createInputSurface();
|
|
||||||
encoder.start();
|
encoder.start();
|
||||||
|
|
||||||
// Android 14+ (API 34) 强制要求:在 createVirtualDisplay 之前必须先注册
|
nv12Buffer = new byte[width * height * 3 / 2];
|
||||||
// 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);
|
|
||||||
|
|
||||||
running = true;
|
running = true;
|
||||||
if (callback != null) callback.onEncoderStarted(width, height, fps);
|
if (callback != null) callback.onEncoderStarted(width, height, fps);
|
||||||
Log.i(TAG, "self codec encoder started: " + width + "x" + height + "@" + fps + ", bitrate=" + bitrate);
|
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);
|
||||||
|
// 优先 NV12(COLOR_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) {
|
private int computeBitrate(int w, int h, int fps) {
|
||||||
int b = (int) (w * h * fps * 0.12);
|
int b = (int) (w * h * fps * 0.12);
|
||||||
return Math.max(500_000, Math.min(8_000_000, b));
|
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(三平面)→ NV12(Y + 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() {
|
private final MediaCodec.Callback encoderCallback = new MediaCodec.Callback() {
|
||||||
@Override
|
@Override
|
||||||
public void onInputBufferAvailable(MediaCodec mc, int index) {
|
public void onInputBufferAvailable(MediaCodec mc, int index) {
|
||||||
// 使用 Surface 输入,编码器自动从输入 Surface 取帧,无需手动喂数据
|
synchronized (inputLock) {
|
||||||
|
freeInputIndices.add(index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -261,11 +397,6 @@ public class SelfCodecEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 返回编码器是否正在运行(供上层判断启动结果)。 */
|
|
||||||
public boolean isRunning() {
|
|
||||||
return running;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 即时请求一个关键帧(强求 IDR)。 */
|
/** 即时请求一个关键帧(强求 IDR)。 */
|
||||||
public void requestKeyFrame() {
|
public void requestKeyFrame() {
|
||||||
if (encoder == null) return;
|
if (encoder == null) return;
|
||||||
@@ -278,22 +409,14 @@ public class SelfCodecEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 返回编码器是否正在运行(供上层判断启动结果)。 */
|
||||||
|
public boolean isRunning() {
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
public void release() {
|
public void release() {
|
||||||
running = false;
|
running = false;
|
||||||
if (mediaProjection != null && projectionCallback != null) {
|
freeInputIndices.clear();
|
||||||
try {
|
|
||||||
mediaProjection.unregisterCallback(projectionCallback);
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
projectionCallback = null;
|
|
||||||
}
|
|
||||||
if (virtualDisplay != null) {
|
|
||||||
try {
|
|
||||||
virtualDisplay.release();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
virtualDisplay = null;
|
|
||||||
}
|
|
||||||
if (encoder != null) {
|
if (encoder != null) {
|
||||||
try {
|
try {
|
||||||
encoder.stop();
|
encoder.stop();
|
||||||
@@ -305,9 +428,6 @@ public class SelfCodecEncoder {
|
|||||||
}
|
}
|
||||||
encoder = null;
|
encoder = null;
|
||||||
}
|
}
|
||||||
if (inputSurface != null) {
|
nv12Buffer = null;
|
||||||
inputSurface.release();
|
|
||||||
inputSurface = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,13 @@ public class WebRtcClient {
|
|||||||
|
|
||||||
private InputCommandCallback inputCallback;
|
private InputCommandCallback inputCallback;
|
||||||
|
|
||||||
|
/** 自编码模式帧监听器:WebRTC 采集到的每一帧都会转发给自编码编码器(共享同一条屏幕采集)。 */
|
||||||
|
public interface SelfCodecFrameListener {
|
||||||
|
void onFrame(org.webrtc.VideoFrame frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SelfCodecFrameListener selfCodecFrameListener;
|
||||||
|
|
||||||
private RemoteDisconnectCallback remoteDisconnectCallback;
|
private RemoteDisconnectCallback remoteDisconnectCallback;
|
||||||
private boolean remoteDisconnectNotified = false;
|
private boolean remoteDisconnectNotified = false;
|
||||||
|
|
||||||
@@ -163,6 +170,18 @@ public class WebRtcClient {
|
|||||||
if (localSink != null) {
|
if (localSink != null) {
|
||||||
videoTrack.addSink(localSink);
|
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
|
@Override
|
||||||
public void onSetSuccess() {
|
public void onSetSuccess() {
|
||||||
Log.e(TAG, "onSetSuccess: modifiedSdp = " + modifiedSdpDescription);
|
Log.d(TAG, "onSetSuccess: modifiedSdp = " + modifiedSdpDescription);
|
||||||
sendAnswer(modifiedSdpDescription, controllerId);
|
sendAnswer(modifiedSdpDescription, controllerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,6 +485,11 @@ public class WebRtcClient {
|
|||||||
return videoDataChannel != null && videoDataChannel.state() == DataChannel.State.OPEN;
|
return videoDataChannel != null && videoDataChannel.state() == DataChannel.State.OPEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 设置自编码帧监听器:为 null 时不再转发采集帧(WebRTC 模式)。 */
|
||||||
|
public void setSelfCodecFrameListener(SelfCodecFrameListener listener) {
|
||||||
|
this.selfCodecFrameListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
/** 暂停 WebRTC 默认屏幕采集(切到自编码模式时调用)。 */
|
/** 暂停 WebRTC 默认屏幕采集(切到自编码模式时调用)。 */
|
||||||
public void stopCapture() {
|
public void stopCapture() {
|
||||||
if (videoCapturer != null) {
|
if (videoCapturer != null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user