10. Camera2视频录制:用Camera2 + MediaCodec/MediaRecorder实现视频录制,Surface共享机制
视频录制,说白了就是把摄像头一帧帧的画面,编码压缩成视频文件存下来。Android上这条路有好几种走法,但Camera2时代,最主流的就两条:MediaRecorder和MediaCodec。我个人习惯把前者叫“傻瓜方案”,后者叫“硬核方案”。
嗯,咱们先搞清楚一个核心概念——Surface。Camera2的输出,本质上就是往Surface上扔数据。不管是预览、拍照还是录像,最终都是通过Surface来传递图像数据的。你想想看,这个Surface就像个“数据水龙头”,接上不同的管子,就能干不同的事。
10.1 两条路:MediaRecorder vs MediaCodec
| 对比项 | MediaRecorder | MediaCodec |
|---|---|---|
| 易用性 | 高,几行代码搞定 | 低,需要自己管理编码器状态 |
| 灵活性 | 低,无法干预编码过程 | 高,可以自定义编码参数、处理帧数据 |
| 适用场景 | 普通视频录制、短视频App | 需要实时处理帧、添加滤镜、自定义编码 |
| 性能开销 | 较低,系统做了优化 | 较高,需要开发者自己把控 |
我在项目中遇到过不少选型纠结的情况。如果只是录个普通视频,别折腾,直接用MediaRecorder。但如果你要做美颜、加滤镜、或者需要把视频帧实时传给网络,那MediaCodec才是你的菜。
10.2 MediaRecorder方案:最省心的录制方式
MediaRecorder的使用流程,其实就三步:配置、启动、停止。但这里有个坑——配置顺序不能乱。
// 1. 创建MediaRecorder实例
MediaRecorder mediaRecorder = new MediaRecorder();
// 2. 配置(顺序不能乱!)
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mediaRecorder.setOutputFile(outputFile.getAbsolutePath());
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mediaRecorder.setVideoSize(1920, 1080);
mediaRecorder.setVideoFrameRate(30);
mediaRecorder.setVideoBitRate(10_000_000);
// 3. 准备
mediaRecorder.prepare();
// 4. 获取Surface,交给Camera2
Surface recorderSurface = mediaRecorder.getSurface();
// 然后把这个Surface添加到CaptureSession中
// 具体代码见下文
// 5. 开始录制
mediaRecorder.start();
// 6. 停止录制
mediaRecorder.stop();
mediaRecorder.reset();
mediaRecorder.release();
这里有个关键点:mediaRecorder.getSurface()返回的Surface,就是Camera2要往上面写数据的那个“管子”。你只需要把这个Surface加到CaptureSession的输出列表里,Camera2就会自动把每一帧画面送进去。
10.3 MediaCodec方案:自己掌控编码流程
MediaCodec方案,说白了就是你自己当“包工头”,管着编码器的输入输出。流程稍微复杂点,但自由度极高。
// 1. 创建MediaCodec编码器
MediaCodec mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIME_TYPE_AVC);
MediaFormat mediaFormat = MediaFormat.createVideoFormat(
MediaFormat.MIME_TYPE_AVC, 1920, 1080);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 10_000_000);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
// 2. 获取输入Surface
Surface codecSurface = mediaCodec.createInputSurface();
// 3. 启动编码器
mediaCodec.start();
// 4. 在循环中取出编码后的数据
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!isRecordingStopped) {
int outputIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000);
if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
// 暂时没有数据,继续等
} else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 输出格式变了,可以在这里初始化muxer
} else {
ByteBuffer encodedData = outputBuffers[outputIndex];
// 把encodedData写入文件或网络
mediaCodec.releaseOutputBuffer(outputIndex, false);
}
}
// 5. 停止
mediaCodec.stop();
mediaCodec.release();
嗯,这里要注意,MediaCodec编码出来的数据是H.264裸流,不能直接播放。你需要用MediaMuxer把它封装成MP4文件。我习惯的做法是:在收到INFO_OUTPUT_FORMAT_CHANGED时初始化MediaMuxer,然后把每一帧编码数据写进去。
10.4 Surface共享机制:一个画面,多个用途
Surface共享,说白了就是让Camera2的一帧数据,同时送给多个Surface。比如你既要预览,又要录制,甚至还要拍照。这时候就需要把多个Surface打包成一个列表,传给CaptureSession。
// 创建多个Surface
Surface previewSurface = surfaceView.getHolder().getSurface();
Surface recorderSurface = mediaRecorder.getSurface();
// 或者 Surface codecSurface = mediaCodec.createInputSurface();
// 创建CaptureRequest
CaptureRequest.Builder captureRequestBuilder =
cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
captureRequestBuilder.addTarget(previewSurface);
captureRequestBuilder.addTarget(recorderSurface);
// 创建CaptureSession
cameraDevice.createCaptureSession(
Arrays.asList(previewSurface, recorderSurface),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
// 开始连续预览
session.setRepeatingRequest(captureRequestBuilder.build(), null, null);
}
}, null);
这里有个细节:TEMPLATE_RECORD是专门为录制场景优化的模板。它会优先保证帧率稳定,而不是画质。如果你用TEMPLATE_PREVIEW,录制时可能会出现帧率波动。
10.5 知识体系总览
下面这张图,是我自己总结的Camera2视频录制核心流程。你看一眼,基本就能明白整个链路是怎么回事。
你看,Camera2把数据写到Surface,然后Surface可以同时分给预览、录制、拍照三个方向。这就是Surface共享的核心思想——一个生产者,多个消费者。
10.6 避坑指南
- 分辨率匹配:所有输出Surface的分辨率,必须在Camera2支持的输出尺寸列表里。我建议用
StreamConfigurationMap.getOutputSizes()查一下,别硬编码。 - 帧率控制:录制时帧率不要设太高。很多设备30fps是稳的,60fps就悬了。我曾经在某个低端机上设60fps,结果录出来的视频一卡一卡的。
- 音频同步:如果用MediaRecorder,音频和视频是自动同步的。但用MediaCodec + MediaMuxer时,你得自己保证时间戳对齐。我习惯用
System.nanoTime()来生成PTS。 - 生命周期管理:录制过程中Activity被销毁了怎么办?我建议在
onPause()里停止录制,释放资源。别指望用户会乖乖点停止按钮。
好了,这一章的内容就到这儿。Surface共享这个机制,你理解了之后,后面做多路输出、画中画、甚至直播推流,都会轻松很多。
公众号:蓝海资料掘金营,微信deep3321