24、MediaCodec与Camera集成:相机预览硬编码、实时滤镜、编码-解码环回、延迟分析
相机和MediaCodec的集成,是Android视频开发里绕不开的硬骨头。说实话,我早年刚接触这块时也被折腾得够呛——预览画面卡顿、编码延迟忽高忽低、滤镜效果不对……各种问题轮番上阵。今天咱们就把这块彻底捋清楚。
24.1 相机预览与硬编码的衔接
相机预览的数据怎么喂给MediaCodec?这是第一个要解决的问题。Android相机有两套API:Camera1和Camera2。我个人强烈建议直接用Camera2,虽然它复杂一些,但可控性高太多了。
核心思路是这样的:
- 从Camera2的ImageReader拿到预览帧
- 把Image转成SurfaceTexture或者直接传给MediaCodec的输入Surface
- MediaCodec从Surface上拉取数据进行编码
嗯,这里有个关键点——不要走CPU拷贝。你想想看,如果每帧都从GPU读回内存,再塞给编码器,延迟至少多出10ms以上。正确的做法是让Camera直接输出到MediaCodec的输入Surface。
核心原则:Camera → Surface → MediaCodec,全程走GPU管线,避免CPU介入。
代码上大概是这样:
// 创建编码器
MediaCodec encoder = MediaCodec.createEncoderByType(MediaFormat.MIME_TYPE_AVC);
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE_AVC, width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface inputSurface = encoder.createInputSurface();
encoder.start();
// 配置Camera2,把预览输出到inputSurface
CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
builder.addTarget(inputSurface);
cameraCaptureSession.setRepeatingRequest(builder.build(), null, null);
我在项目中遇到过一个问题:某些低端机型上,直接传Surface会导致预览画面颜色偏绿。后来发现是ColorFormat不匹配。解决办法是手动设置COLOR_FormatSurface,让编码器自己协商格式。
24.2 实时滤镜的实现方案
实时滤镜说白了就是在编码之前对画面做一次像素级处理。方案有两种:
- GPU方案(推荐):用OpenGL ES写Fragment Shader,在渲染管线里做颜色变换
- CPU方案:从ImageReader拿到YUV数据,用RenderScript或Neon指令处理
我个人习惯用GPU方案。为什么呢?因为CPU方案每帧都要做YUV→RGB转换,再处理,再转回YUV,这个开销太大了。GPU方案直接在纹理上操作,几乎零拷贝。
具体做法是插入一个中间Surface:
Camera → SurfaceTexture → GPU滤镜处理 → Surface(MediaCodec输入)
这里需要一个EGL环境来管理OpenGL上下文。我常用的套路是:
// 创建EGL环境
EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
EGL14.eglInitialize(eglDisplay, null, 0, null, 0);
// 创建离屏Surface,绑定到编码器的输入Surface
EGLSurface eglSurface = egl14.eglCreateWindowSurface(eglDisplay, eglConfig, encoderInputSurface, null, 0);
// 在渲染循环中:
// 1. 从Camera的SurfaceTexture更新纹理
// 2. 用滤镜Shader渲染到离屏Surface
// 3. eglSwapBuffers触发编码器消费
小技巧:滤镜Shader里尽量用高精度mediump,别用lowp。我吃过亏——lowp在某些GPU上颜色量化太严重,画面出现色带。
常见的滤镜效果其实就几个数学变换:
| 滤镜类型 | 核心算法 | 性能开销 |
|---|---|---|
| 黑白 | 灰度加权平均 | 极低 |
| 怀旧 | 颜色矩阵偏移 | 低 |
| 美颜 | 双边滤波+肤色检测 | 中高 |
| LUT映射 | 3D查找表插值 | 中 |
24.3 编码-解码环回(Loopback)
环回测试是验证编码解码链路是否正常的重要手段。说白了就是:编码器出来的数据,不存文件,直接喂给解码器,然后显示出来。
为什么要做这个?我在调试一个直播项目时遇到过——编码出来的画面在远端播放器上花屏,但本地录文件是正常的。后来用环回测试才发现,是编码器的SPS/PPS参数在特定分辨率下有问题。
环回的核心流程:
Camera → 编码器 → 内存缓冲区 → 解码器 → SurfaceView
实现时要注意几个坑:
- 时序同步:编码器和解码器跑在不同的线程上,需要用
MediaCodec.BufferInfo里的presentationTimeUs来对齐 - 关键帧处理:解码器必须等到第一个IDR帧才能开始输出,所以编码器要确保第一帧是关键帧
- 格式协商:编码器输出的CSD(Codec Specific Data)要正确传递给解码器
注意:环回测试时,编码器和解码器不要共用一个MediaCodec实例。我曾经犯过这个错——以为能省资源,结果两个实例互相干扰,直接崩溃。
代码结构大致如下:
// 编码线程
while (!isStop) {
int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
if (outputIndex >= 0) {
ByteBuffer outputData = encoder.getOutputBuffer(outputIndex);
// 把数据拷贝到共享缓冲区,通知解码线程
onEncodedFrame(outputData, bufferInfo);
encoder.releaseOutputBuffer(outputIndex, false);
}
}
// 解码线程
void onEncodedFrame(ByteBuffer data, MediaCodec.BufferInfo info) {
int inputIndex = decoder.dequeueInputBuffer(TIMEOUT_US);
if (inputIndex >= 0) {
ByteBuffer inputBuffer = decoder.getInputBuffer(inputIndex);
inputBuffer.clear();
inputBuffer.put(data);
decoder.queueInputBuffer(inputIndex, 0, info.size, info.presentationTimeUs, info.flags);
}
// 取出解码后的帧,渲染到Surface
int outputIndex = decoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
if (outputIndex >= 0) {
decoder.releaseOutputBuffer(outputIndex, true); // true表示渲染
}
}
24.4 延迟分析与优化
延迟是实时视频系统的命门。我见过太多项目因为延迟问题被用户骂。咱们来拆解一下延迟都出在哪里。
一条完整的链路延迟包括:
- 相机采集延迟:Camera2的TEMPLATE_PREVIEW模式下,一般1-2帧的缓冲
- 滤镜处理延迟:GPU渲染一帧的时间,通常0.5-2ms
- 编码延迟:MediaCodec内部缓冲,取决于配置和码率
- 解码延迟:解码器从输入到输出,通常1-3帧
- 渲染延迟:SurfaceFlinger合成和显示
实测数据(某骁龙8系机型,1080p@30fps):
采集延迟:~16ms(1帧)
滤镜处理:~1ms
编码延迟:~33ms(2帧)
解码延迟:~16ms(1帧)
渲染延迟:~8ms
总延迟:约74ms
优化方向其实很明确:
- 减少缓冲帧数:Camera2里设置
setRepeatingRequest时,把AE_TARGET_FPS_RANGE设高一些 - 编码器配置:设置
KEY_PRIORITY为0(实时模式),关闭B帧(KEY_MAX_B_FRAMES=0) - 解码器配置:用
setVideoScalingMode跳过缩放处理 - 渲染优化:用
SurfaceView代替TextureView,减少合成层数
我曾经在一个项目里把延迟从120ms压到了60ms以下。最关键的改动就是关掉了编码器的B帧——B帧虽然压缩率高,但会引入至少2帧的额外延迟。实时场景下,压缩率没那么重要,延迟才是命根子。
调试工具:用Choreographer打点,配合Trace工具,可以精确测量每一段的耗时。我习惯在关键节点用System.nanoTime()打时间戳,输出到log里分析。
最后说一个容易被忽略的点——CPU频率缩放。编码器工作时,如果CPU governor没有及时响应,会导致编码速度跟不上。我建议在编码开始时,用PowerManager申请一个PARTIAL_WAKE_LOCK,同时把编码线程绑到大核上。
这张图把整个链路画得很清楚了。你可以看到,环回路径绕过了文件存储,直接在内存里流转。这也是为什么环回测试能真实反映编解码延迟的原因——它排除了I/O的干扰。
好了,这一章的内容就到这里。Camera与MediaCodec的集成,说白了就是管好Surface和Buffer的流转。滤镜是锦上添花,环回是调试利器,延迟分析是优化基础。把这些吃透了,实时视频这块你基本就稳了。