Android下如何高效渲染YUV/NV21视频帧至TextureView/SurfaceView
解决方案
一、优先采用MediaCodec硬件渲染方案(稳定高效)
利用系统MediaCodec直接将YUV数据喂入硬件解码器,输出到Surface(TextureView/SurfaceView的Surface)实现GPU加速渲染,完全规避CPU转码开销。
实现步骤
- 初始化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();
- 在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); } }
- 生命周期管理
在页面暂停/销毁时释放解码器资源:
@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不可访问问题,核心是缓冲区未正确释放或线程同步错误:
- 规范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锁死 }); }
- 强制线程同步
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); }
- I420(Planar)片段着色器:
- 拉伸修复:计算帧与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
相关产品推荐
相关产品推荐

