15、VideoCapture流程:VideoStream配置、MediaCodec集成、编码参数传递、录制与预览并发、慢动作实现

各位同学,今天我们来聊聊VideoCapture流程中比较核心的几个环节。说实话,这部分内容在Android多媒体开发里,算是真正考验功底的地方。我当年刚接触时也踩过不少坑,今天就把这些经验分享给你们。

VideoStream配置:从Camera到编码器的桥梁

VideoStream配置,说白了就是告诉系统:我要从摄像头拿什么格式的数据,然后怎么送到编码器里去。这里有个关键点——Surface

为什么是Surface?因为Camera2 API输出的数据是YUV格式的,而MediaCodec编码器接收的也是YUV。但如果你直接走内存拷贝,那性能就太差了。所以Android设计了一个共享内存机制——通过Surface把Camera的输出直接连到编码器的输入。

核心思路:Camera的CaptureRequest里设置一个Target Surface,这个Surface来自MediaCodec的createInputSurface()。这样数据流就是零拷贝的。

我在项目中遇到过一个问题:配置了Surface但预览黑屏。后来发现是忘了给预览也配一个Surface。嗯,这里要注意——预览和录制是两个独立的Surface,不能共用。

// 伪代码示例
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, frameRate);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface inputSurface = encoder.createInputSurface();

// 配置Camera的CaptureRequest
CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
builder.addTarget(inputSurface);  // 编码器Surface
builder.addTarget(previewSurface); // 预览Surface

MediaCodec集成:编码器的正确打开方式

MediaCodec的集成,我建议你把它想象成一个流水线。输入是原始帧,输出是编码后的数据包。但这里有个坑——编码器的状态机

你想想看,MediaCodec有四种状态:Stopped、Configured、Executing、OutputFormatChanged。很多人一上来就调start(),结果发现拿不到数据。为什么?因为编码器需要先收到输入数据,才会产生输出。

我的习惯:先调用start(),然后通过dequeueInputBuffer()往里面塞数据。但更推荐的做法是直接用Surface输入,这样Camera的数据会自动推给编码器。

我曾经犯过一个低级错误——在编码器还没配置完就开始往Surface上画数据。结果编码器直接报错。后来我加了个回调监听:

encoder.setCallback(new MediaCodec.Callback() {
    @Override
    public void onInputBufferAvailable(MediaCodec codec, int index) {
        // Surface模式不需要手动塞数据
    }

    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
        ByteBuffer outputBuffer = codec.getOutputBuffer(index);
        // 处理编码后的数据
        codec.releaseOutputBuffer(index, false);
    }

    @Override
    public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
        // 编码器输出格式变化,比如H.264的SPS/PPS
    }

    @Override
    public void onError(MediaCodec codec, MediaCodec.CodecException e) {
        // 错误处理
    }
});

编码参数传递:比特率、帧率、关键帧间隔

编码参数传递,说白了就是告诉编码器:你要压成什么样。这里有几个关键参数:

参数 说明 我的建议
KEY_BIT_RATE 比特率,单位bps 1080p建议8-12Mbps,720p建议4-6Mbps
KEY_FRAME_RATE 帧率,单位fps 一般30fps,慢动作需要120fps或更高
KEY_I_FRAME_INTERVAL 关键帧间隔,单位秒 1秒一个关键帧,方便seek
KEY_COLOR_FORMAT 颜色格式 Surface模式用COLOR_FormatSurface

注意:比特率不是越高越好。我见过有人给1080p设了50Mbps,结果手机发热严重,编码器直接降频。要根据设备能力来。

还有一个容易被忽略的参数——KEY_PROFILEKEY_LEVEL。比如H.264的Baseline Profile和High Profile,压缩效率差很多。我建议用High Profile,兼容性也够。

录制与预览并发:两个Surface的协同

录制与预览并发,这个场景很常见。你打开相机,屏幕上看到预览画面,同时后台在录视频。这里的关键是——两个Surface要同步

为什么会有同步问题?因为Camera的输出是流式的,你配了两个Surface,系统会复制数据分别发给它们。但如果预览Surface的尺寸和编码Surface的尺寸不一样,系统会做缩放,这就可能引入延迟。

我个人习惯的做法是:预览和录制用相同的分辨率。比如都设为1920x1080。这样Camera输出一份数据,两个Surface各拿一份,不需要额外缩放。

// 创建两个Surface
Surface previewSurface = new Surface(previewTextureView.getSurfaceTexture());
Surface recordSurface = encoder.createInputSurface();

// 配置CaptureRequest
CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
builder.addTarget(previewSurface);
builder.addTarget(recordSurface);

但如果你非要不同分辨率,比如预览用1280x720,录制用1920x1080,那就要用ImageReader做中转。不过我不推荐,性能开销太大。

慢动作实现:高帧率录制与回放

慢动作,说白了就是录的时候用高帧率,播的时候用正常帧率。比如你录了120fps的视频,播放时用30fps,那就相当于4倍慢动作。

实现慢动作有几个关键点:

  • 摄像头要支持高帧率。不是所有摄像头都支持120fps,需要查CameraCharacteristics.SCALER_AVAILABLE_STREAM_CONFIGURATIONS。
  • 编码器要支持高帧率。MediaCodec的KEY_FRAME_RATE要设成120。
  • 比特率要翻倍。帧率高了,数据量大了,比特率不翻倍画面会糊。

避坑指南:我曾经在某个手机上做慢动作,录出来的视频播放时一卡一卡的。后来发现是编码器的KEY_FRAME_RATE设成了30,但摄像头输出的是120fps。编码器以为只有30fps,结果关键帧间隔不对。嗯,这个坑我踩过。

慢动作的回放有两种方式:

  • 实时慢动作:录制时就决定哪些片段要慢放,在编码时插入SEI信息。
  • 后期慢动作:录完整段高帧率视频,播放时通过MediaCodec或ExoPlayer控制播放速度。

我个人更推荐后期慢动作,灵活性更高。用户可以在编辑时选择慢放区间。

// 配置高帧率编码
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE_AVC, width, height);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 120);  // 高帧率
format.setInteger(MediaFormat.KEY_BIT_RATE, 20_000_000); // 比特率翻倍
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

知识体系总览

下面这张图是我整理的VideoCapture核心流程,你可以对照着理解:

VideoCapture 核心流程 Camera2 设备 CaptureRequest(配置Target Surface) 预览 Surface 编码 Surface MediaCodec 编码器 编码后的视频数据 预览显示 零拷贝输入 编码参数 比特率: 8-12Mbps 帧率: 30/120fps 关键帧间隔: 1s

从这张图你可以看到,整个流程就是一条流水线:Camera输出 -> CaptureRequest分发 -> 预览Surface和编码Surface并行处理 -> MediaCodec编码 -> 输出视频数据。每个环节都有它的坑,但只要理解了核心逻辑,调试起来就不难。

好了,这一章的内容就到这里。记住,VideoCapture的核心就是Surface的配置和编码参数的传递。你把这些搞明白了,后面遇到什么奇葩问题都能找到根因。

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