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

Android下如何高效渲染YUV/NV21视频帧至TextureView/SurfaceView

解决方案

一、优先采用MediaCodec硬件渲染方案(稳定高效)

利用系统MediaCodec直接将YUV数据喂入硬件解码器,输出到Surface(TextureView/SurfaceView的Surface)实现GPU加速渲染,完全规避CPU转码开销。

实现步骤

  1. 初始化MediaCodec
    根据mediaFormat的YUV格式配置解码器,绑定输出Surface:
MediaCodec decoder = MediaCodec.createDecoderByType("video/raw");
// 强制指定帧参数,确保与输入流匹配
mediaFormat.setInteger(MediaFormat.KEY_WIDTH, 1920);
mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, 1088);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
// 绑定TextureView/SurfaceView的Surface
decoder.configure(mediaFormat, surface, null, 0);
decoder.start();
  1. 在onYuvDataReceived中喂入数据
    注意线程同步,避免缓冲区冲突:
@Override
public void onYuvDataReceived(byte[] yuvData, MediaFormat mediaFormat) {
    int inputBufferIndex = decoder.dequeueInputBuffer(10000); // 超时10ms
    if (inputBufferIndex >= 0) {
        ByteBuffer inputBuffer = decoder.getInputBuffer(inputBufferIndex);
        inputBuffer.clear();
        inputBuffer.put(yuvData);
        // 生成递增PTS,保证帧时序(30fps单帧间隔约33333微秒)
        long pts = System.nanoTime() / 1000;
        decoder.queueInputBuffer(inputBufferIndex, 0, yuvData.length, pts, 0);
    }
    // 自动处理输出渲染
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    int outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000);
    while (outputBufferIndex >= 0) {
        decoder.releaseOutputBuffer(outputBufferIndex, true);
        outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 0);
    }
}
  1. 生命周期管理
    在页面暂停/销毁时释放解码器资源:
@Override
protected void onPause() {
    super.onPause();
    if (decoder != null) decoder.stop();
}

@Override
protected void onDestroy() {
    super.onDestroy();
    if (decoder != null) {
        decoder.release();
        decoder = null;
    }
}

关键注意点

  • 确保YUV数据格式与mediaFormat完全匹配(SemiPlanar对应NV12,Planar对应I420),禁止混喂。
  • PTS必须严格递增,否则会出现帧卡顿、跳帧。
  • 喂数据操作需线程安全,可通过Handler.post切换到主线程或MediaCodec专用线程执行。

二、修复WebRTC方案的帧渲染异常

后续帧buffer不可访问问题,核心是缓冲区未正确释放或线程同步错误:

  1. 规范VideoFrame缓冲区管理
    使用WebRTC原生Buffer类创建帧,渲染后强制释放:
@Override
public void onYuvDataReceived(byte[] yuvData, MediaFormat mediaFormat) {
    int format = mediaFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT);
    VideoFrame.Buffer buffer;
    if (format == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar) {
        buffer = JavaI420Buffer.wrap(1920, 1088, yuvData, 1920, 960, 960);
    } else if (format == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar) {
        buffer = JavaNV12Buffer.wrap(1920, 1088, yuvData, 1920);
    } else {
        return;
    }
    VideoFrame frame = new VideoFrame(buffer, 0, System.nanoTime() / 1000);
    // 切换到主线程渲染
    handler.post(() -> {
        surfaceViewRenderer.onFrame(frame);
        frame.release(); // 必须释放,避免内存泄漏与buffer锁死
    });
}
  1. 强制线程同步
    WebRTC渲染操作必须在主线程或其专用线程执行,禁止在自定义接收线程直接调用onFrame。

三、修复OpenGL ES方案的兼容性问题

1. Android 10色彩失真、拉伸问题

  • 色彩失真修复:根据YUV格式使用对应着色器转换矩阵:
    • I420(Planar)片段着色器:
      precision mediump float;
      varying vec2 vTexCoord;
      uniform sampler2D yTexture;
      uniform sampler2D uTexture;
      uniform sampler2D vTexture;
      const mat3 YUV2RGB = mat3(
          1.0, 1.0, 1.0,
          0.0, -0.39465, 2.03211,
          1.13983, -0.58060, 0.0
      );
      void main() {
          vec3 yuv;
          yuv.x = texture2D(yTexture, vTexCoord).r;
          yuv.y = texture2D(uTexture, vTexCoord).r - 0.5;
          yuv.z = texture2D(vTexture, vTexCoord).r - 0.5;
          vec3 rgb = YUV2RGB * yuv;
          gl_FragColor = vec4(rgb, 1.0);
      }
      
    • NV12(SemiPlanar)片段着色器:
      precision mediump float;
      varying vec2 vTexCoord;
      uniform sampler2D yTexture;
      uniform sampler2D uvTexture;
      const mat3 YUV2RGB = mat3(
          1.0, 1.0, 1.0,
          0.0, -0.39465, 2.03211,
          1.13983, -0.58060, 0.0
      );
      void main() {
          float y = texture2D(yTexture, vTexCoord).r;
          vec2 uv = texture2D(uvTexture, vTexCoord).rg - vec2(0.5, 0.5);
          vec3 yuv = vec3(y, uv.x, uv.y);
          vec3 rgb = YUV2RGB * yuv;
          gl_FragColor = vec4(rgb, 1.0);
      }
      
  • 拉伸修复:计算帧与Surface的宽高比,调整顶点坐标适配:
    float frameRatio = 1920f / 1088f;
    float surfaceRatio = (float) surfaceWidth / surfaceHeight;
    float scaleX = frameRatio > surfaceRatio ? surfaceRatio / frameRatio : 1f;
    float scaleY = frameRatio < surfaceRatio ? frameRatio / surfaceRatio : 1f;
    float[] vertices = {
        -scaleX, scaleY,
        -scaleX, -scaleY,
        scaleX, scaleY,
        scaleX, -scaleY
    };
    

2. Android 13(Pixel)SIGSEGV崩溃问题

  • 核心原因:非GL线程调用OpenGL API,或重复初始化GL资源导致指针异常。
  • 修复措施:
    • 仅在onSurfaceCreated中初始化一次GL资源:
      private int[] textures;
      private int program;
      
      @Override
      public void onSurfaceCreated(GL10 gl, EGLConfig config) {
          if (program != 0) return;
          program = createProgram(vertexShader, fragmentShader);
          textures = new int[format == COLOR_FormatYUV420Planar ? 3 : 2];
          GLES20.glGenTextures(textures.length, textures, 0);
          for (int texture : textures) {
              GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
              GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
              GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
          }
      }
      
    • 所有GL操作必须在Renderer线程执行,自定义接收线程仅缓存YUV数据,在onDrawFrame中更新纹理:
      private byte[] latestYuvData;
      
      @Override
      public void onDrawFrame(GL10 gl) {
          if (latestYuvData != null) {
              updateTextures(latestYuvData, format);
              drawFrame();
              latestYuvData = null;
          }
      }
      

四、性能优化提示

  • 采用ByteBuffer.allocateDirect创建YUV缓冲区,减少CPU到GPU的数据拷贝。
  • 关闭OpenGL调试日志、禁用不必要的性能分析工具,降低额外开销。
  • 对于MediaCodec方案,可通过MediaCodec.Callback替代手动轮询缓冲区,进一步优化线程效率。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 18:27:55