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的使用流程,其实就三步:配置、启动、停止。但这里有个坑——配置顺序不能乱。

⚠️ 注意:MediaRecorder的setAudioSource()、setVideoSource()、setOutputFormat()、setAudioEncoder()、setVideoEncoder()这几个方法,调用顺序必须严格遵循官方文档。我曾经因为调换了setOutputFormat和setVideoSource的顺序,折腾了一下午才发现问题。
// 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就会自动把每一帧画面送进去。

💡 小技巧:我建议把MediaRecorder的配置和Camera2的CaptureSession创建分开管理。先配好MediaRecorder拿到Surface,再创建CaptureSession。这样逻辑清晰,不容易出错。

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,然后把每一帧编码数据写进去。

🔑 核心要点:MediaCodec的输入Surface和MediaRecorder的Surface,本质上都是给Camera2用的。区别在于,MediaRecorder内部帮你处理了编码和封装,而MediaCodec把编码后的数据交给你自己处理。

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,录制时可能会出现帧率波动。

⚠️ 踩坑提醒:我曾经在Surface共享时,把预览Surface和录制Surface的尺寸设成了不同的分辨率。结果预览正常,但录出来的视频是黑的。后来才发现,Camera2要求所有输出Surface的分辨率必须一致,或者至少是同一个配置集里的。建议统一用1920x1080。

10.5 知识体系总览

下面这张图,是我自己总结的Camera2视频录制核心流程。你看一眼,基本就能明白整个链路是怎么回事。

Camera2 视频录制核心流程 Camera2 设备 Surface(数据管道) 预览(TextureView/SurfaceView) 录制(MediaRecorder/MediaCodec) 拍照(ImageReader) 实时画面显示 MP4/H.264 文件 JPEG/DNG 图片 Surface共享:一个Camera2输出,同时供给多个消费端

你看,Camera2把数据写到Surface,然后Surface可以同时分给预览、录制、拍照三个方向。这就是Surface共享的核心思想——一个生产者,多个消费者。

10.6 避坑指南

  • 分辨率匹配:所有输出Surface的分辨率,必须在Camera2支持的输出尺寸列表里。我建议用StreamConfigurationMap.getOutputSizes()查一下,别硬编码。
  • 帧率控制:录制时帧率不要设太高。很多设备30fps是稳的,60fps就悬了。我曾经在某个低端机上设60fps,结果录出来的视频一卡一卡的。
  • 音频同步:如果用MediaRecorder,音频和视频是自动同步的。但用MediaCodec + MediaMuxer时,你得自己保证时间戳对齐。我习惯用System.nanoTime()来生成PTS。
  • 生命周期管理:录制过程中Activity被销毁了怎么办?我建议在onPause()里停止录制,释放资源。别指望用户会乖乖点停止按钮。
💡 我的经验:如果你刚开始接触Camera2视频录制,我建议先从MediaRecorder方案入手。跑通了再换MediaCodec。一口吃不成胖子,编码器这东西,坑多着呢。

好了,这一章的内容就到这儿。Surface共享这个机制,你理解了之后,后面做多路输出、画中画、甚至直播推流,都会轻松很多。


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