17. 相机与编码器集成:CameraSource、编码器配置、实时视频处理

好,我们进入第十七章。说实话,这一章是我个人觉得Stagefright框架里最“刺激”的部分——相机和编码器怎么握手,怎么把一帧帧的原始图像数据,变成压缩后的视频流。你想想看,摄像头每秒产出30帧数据,编码器要实时吞下去,中间但凡有一点延迟或者配置不对,画面就卡住或者直接黑屏。

我在项目中遇到过好几次这种问题。有一次是客户反馈,录像前几秒总是花屏。查了半天,发现是编码器配置里的关键帧间隔设得太短,导致I帧还没准备好,P帧就已经冲进去了。嗯,这种坑,踩过一次就记住了。

17.1 CameraSource:相机数据的“搬运工”

CameraSource是什么?说白了,它就是一个生产者。它从Camera HAL层拿到预览帧或者拍照帧,然后把这些数据包装成MediaCodec能吃的格式,塞进编码器里。

它的核心工作流程其实不复杂:

  • 打开相机:通过Camera HAL打开指定摄像头ID
  • 配置参数:设置分辨率、帧率、格式(一般是NV21或YV12)
  • 启动预览:开始接收帧数据
  • 数据回调:每一帧到达时,CameraSource会收到通知
  • 封装成Buffer:把原始数据包装成MediaCodec的InputBuffer

我习惯把CameraSource看作一个“适配器”。它把Camera HAL的异步回调模型,转成了编码器需要的同步Buffer模型。这个转换过程,其实挺考验设计功力的。

关键点:CameraSource内部维护了一个Buffer队列。相机回调来的帧不会直接送给编码器,而是先入队。编码器从队列里取数据。这样做的目的是解耦——相机和编码器的工作节奏可能不一样,队列能起到缓冲作用。

17.2 编码器配置:不是随便填几个参数就行

编码器配置,我见过太多人栽跟头了。你以为设置个分辨率、码率、帧率就完事了?太天真了。

我们来看看一个典型的MediaCodec配置流程:

MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");
MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();

这段代码看起来简单,但有几个坑:

  • COLOR_FormatSurface:这是最推荐的输入方式。编码器直接消费Surface上的数据,省去了内存拷贝。但如果你用CameraSource,它给的是ByteBuffer,那就得用COLOR_FormatYUV420Flexible。
  • KEY_I_FRAME_INTERVAL:单位是秒。设成1表示每秒一个关键帧。设成0表示只有第一个是关键帧。设成-1表示完全由编码器自己决定。我建议设成1,否则seek的时候会很痛苦。
  • KEY_BIT_RATE:这个值不是越大越好。码率太高,文件体积爆炸;码率太低,画面全是马赛克。我一般根据分辨率来估算:720p用2-4Mbps,1080p用8-12Mbps。

注意:不是所有编码器都支持所有ColorFormat。你最好在运行时查询一下CodecCapabilities,否则configure阶段就会抛异常。我曾经因为没查这个,在某个低端设备上直接崩溃了。

17.3 实时视频处理:在编码前“动手脚”

实时视频处理,说白了就是在相机拿到原始帧之后、编码器吃掉之前,对图像做点“手脚”。比如加滤镜、做人脸检测、叠加水印。

这个环节,性能是关键。你想想看,每一帧只有33毫秒(30fps)的处理时间。如果你处理一帧花了50毫秒,那下一帧来了怎么办?要么丢帧,要么卡顿。

我个人的经验是:

  • 尽量用GPU:OpenGL ES或者RenderScript来处理图像,比CPU快一个数量级。
  • 避免内存分配:每一帧都new一个Bitmap?那是找死。复用Buffer,用对象池。
  • 降低分辨率:如果只是做人脸检测,没必要用1080p的帧。先缩放到320x240,检测完再映射回去。

举个例子,如果你要在视频上叠加时间戳:

// 伪代码,展示思路
void onFrameAvailable(byte[] frameData, int width, int height) {
    // 1. 把NV21转成RGBA(用RenderScript或者直接NEON优化)
    // 2. 在RGBA上绘制文字(用Canvas或者OpenGL)
    // 3. 转回NV21(如果编码器需要)
    // 4. 送给编码器
}

这里有个细节:NV21转RGBA再转回NV21,这个来回转换的开销其实不小。我建议如果编码器支持直接输入RGBA,那就别转回去了。但大多数编码器只认NV21或者YV12,所以这一步往往逃不掉。

小技巧:如果你用Surface作为编码器输入,那处理流程可以简化。你只需要在Surface上绘制内容,编码器自动消费。这样你连Buffer都不用碰。但缺点是,你没法直接操作原始像素数据了。

17.4 整体架构图

下面这张图,是我自己整理的CameraSource到编码器的数据流。你看一眼,应该就能明白整个链路是怎么走的。

CameraSource → 编码器 数据流架构 Camera HAL 原始帧 (NV21/YV12) CameraSource Buffer队列 + 格式转换 实时处理模块 滤镜/检测/叠加 MediaCodec H.264/H.265编码 旁路(无处理) MediaMuxer 封装成MP4 系统组件 框架组件 可选处理 旁路路径

从图上你能看到,数据流有两条路径:一条是经过实时处理模块的“加工路径”,另一条是直接旁路到编码器的“直通路径”。选择哪条,取决于你的业务需求。如果只是简单录像,直通路径就够了。如果要加美颜或者AR效果,那就得走加工路径。

17.5 避坑指南

最后,我分享几个实战中遇到的坑:

  • 编码器启动顺序:必须先配置好编码器,再启动CameraSource。否则相机数据来了,编码器还没准备好,数据就丢了。
  • 帧率匹配:相机输出30fps,编码器也设30fps,但实际编码器可能跑不到30fps。这时候你会看到丢帧。我建议编码器帧率设成相机帧率的1.5倍,给编码器留点余量。
  • 关键帧间隔:别设成0。否则seek的时候,你得从第一个I帧开始解码,用户体验极差。
  • 内存泄漏:CameraSource的回调里,如果持有了Activity的引用,很容易泄漏。用WeakReference或者ApplicationContext。

嗯,这一章的内容就到这里。相机和编码器的集成,说难不难,说简单也不简单。核心就是理解数据流的走向,以及每个环节的配置细节。你把这些搞清楚了,剩下的就是调参和踩坑了。


公众号:蓝海资料掘金营,微信deep3321