You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android端WebRTC连接中捕获帧返回黑Bitmap问题求助

解决Android WebRTC截图返回黑Bitmap的问题

我看你用参考实现尝试捕获WebRTC视频帧,但最终得到黑图,结合你的代码和WebRTC在Android上的常见坑点,整理了几个关键修复方向:

一、最核心问题:OpenGL上下文线程不匹配

你的代码在renderFrame里切换到UI线程执行纹理转Bitmap的操作,但所有OpenGL相关操作(包括纹理转换、EGL上下文切换)必须在同一个持有EGL上下文的线程中执行——WebRTC的视频渲染线程才是拥有正确上下文的线程,切换到UI线程会导致纹理无法被正确读取,最终生成黑图。

修复代码:

把纹理转换逻辑留在WebRTC线程执行,只在最后切换到UI线程更新界面:

@Override
public void renderFrame(final org.webrtc.VideoRenderer.I420Frame i420Frame) {
    Log.d(TAG, "entered renderFrame");
    if (gotFrame || i420Frame.width == 0 || i420Frame.height == 0) {
        Log.d(TAG, "Already got frame so taking honourable exit");
        org.webrtc.VideoRenderer.renderFrameDone(i420Frame);
        return;
    }
    gotFrame = true;
    // 直接在WebRTC线程处理帧转换
    int width = i420Frame.width;
    int height = i420Frame.height;
    initWithSize(width, height);
    Bitmap bitmap = toBitmap(i420Frame);
    org.webrtc.VideoRenderer.renderFrameDone(i420Frame);
    
    // 仅切换到UI线程更新ImageView
    activity.runOnUiThread(new Runnable() {
        public void run() {
            listener.gotBitmap(bitmap);
            destroy();
        }
    });
}

二、EglBase上下文有效性检查

你使用了StreamActivity.rootEglBase,需要确保这个上下文和WebRTC视频轨道使用的是同一个共享上下文,并且操作流程正确:

  • 移除firstTimeOnly的限制,每次截图时创建独立的临时EglBase上下文(避免上下文冲突):
// 替换toBitmap方法里的EglBase初始化逻辑
EglBase eglBase = EglBase.create();
try {
    eglBase.createDummyPbufferSurface();
    eglBase.makeCurrent();
    // 执行纹理转换操作
    TextureToRGB textureToRGB = new TextureToRGB();
    // ... 后续转换逻辑不变
} finally {
    eglBase.release();
}

三、像素字节顺序修正

WebRTC纹理转RGB后的字节顺序通常是BGRA而不是你代码里假设的RGBA,这会导致颜色异常或黑图。调整像素读取顺序:

// 在toBitmap的循环中修改字节读取逻辑
for(int i = 0, j = 0; j < numPixels; i+=bytesPerPixel, j++) {
    // BGRA顺序:取B、G、R分量
    dataARGB[j] = buildARGB(frameBytes[i+2] & 0xff, frameBytes[i+1] & 0xff, frameBytes[i] & 0xff);
}

四、完善destroy方法释放资源

你的destroy方法代码不完整,补充EGL资源释放逻辑,避免内存泄漏和上下文冲突:

public void destroy() {
    if (mEGL != null && mEGLDisplay != null) {
        if (!mEGL.eglDestroySurface(mEGLDisplay, mEGLSurface)) {
            Log.e(TAG, "eglDestroySurface failed");
        }
        if (!mEGL.eglMakeCurrent(mEGLDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT)) {
            Log.e(TAG, "eglMakeCurrent failed");
        }
        if (!mEGL.eglTerminate(mEGLDisplay)) {
            Log.e(TAG, "eglTerminate failed");
        }
    }
    mEGLDisplay = null;
    mEGL = null;
    mWidth = 0;
    mHeight = 0;
}

五、替代方案:使用WebRTC官方VideoSink接口

如果上述修复仍有问题,可以尝试用WebRTC更推荐的VideoSink接口捕获帧,避开OpenGL上下文的坑:

public class FrameCapturer implements VideoSink {
    private final BitmapListener listener;
    private boolean hasCapturedFrame = false;

    public FrameCapturer(BitmapListener listener) {
        this.listener = listener;
    }

    @Override
    public void onFrame(VideoFrame frame) {
        if (hasCapturedFrame) {
            frame.release();
            return;
        }
        hasCapturedFrame = true;
        // 转换VideoFrame到Bitmap
        VideoFrame.I420Buffer i420Buffer = frame.getBuffer().toI420();
        int width = i420Buffer.getWidth();
        int height = i420Buffer.getHeight();
        
        Bitmap bitmap = convertI420ToBitmap(i420Buffer, width, height);
        listener.gotBitmap(bitmap);
        
        // 释放资源
        i420Buffer.release();
        frame.release();
    }

    private Bitmap convertI420ToBitmap(VideoFrame.I420Buffer buffer, int width, int height) {
        byte[] yData = buffer.getDataY();
        byte[] uData = buffer.getDataU();
        byte[] vData = buffer.getDataV();
        int[] argbPixels = new int[width * height];
        
        int yStride = buffer.getStrideY();
        int uStride = buffer.getStrideU();
        int vStride = buffer.getStrideV();

        // YUV转ARGB(建议使用libyuv库提升效率,这里是简化版手动转换)
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int yVal = yData[y * yStride + x] & 0xFF;
                int uVal = uData[(y/2) * uStride + (x/2)] & 0xFF;
                int vVal = vData[(y/2) * vStride + (x/2)] & 0xFF;

                int r = (int) (yVal + 1.370705 * (vVal - 128));
                int g = (int) (yVal - 0.698001 * (vVal - 128) - 0.337633 * (uVal - 128));
                int b = (int) (yVal + 1.732446 * (uVal - 128));

                // 确保颜色值在0-255范围内
                r = Math.max(0, Math.min(255, r));
                g = Math.max(0, Math.min(255, g));
                b = Math.max(0, Math.min(255, b));

                argbPixels[y * width + x] = (0xFF << 24) | (r << 16) | (g << 8) | b;
            }
        }
        return Bitmap.createBitmap(argbPixels, width, height, Bitmap.Config.ARGB_8888);
    }
}

使用方式:

// 从MediaStream获取VideoTrack
VideoTrack videoTrack = stream.videoTracks.get(0);
FrameCapturer capturer = new FrameCapturer(gotFrameListener);
videoTrack.addSink(capturer);
// 捕获完成后记得移除Sink
// videoTrack.removeSink(capturer);

内容的提问来源于stack exchange,提问作者Bruno

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 06:41:53