第23章:Camera与视频编码——MediaCodec与Camera的Buffer共享、视频编码参数配置、HDR视频录制、视频防抖与Camera协同、实战:实现4K HDR视频录制

各位好,欢迎来到第23章。这一章我们聊点硬核的——把Camera拍到的画面,变成真正能存下来的视频文件。

说实话,很多做Camera驱动的人,做到YUV数据出来就觉得完事了。但实际产品中,用户要的是视频,不是裸数据。我见过太多项目,Camera预览跑得飞起,一到录像就卡成PPT。为什么?因为Camera和编码器之间没配合好。

这一章,我们就来彻底搞定这件事。

23.1 MediaCodec与Camera的Buffer共享

先问一个问题:Camera HAL层输出的buffer,怎么送到MediaCodec去编码?

最笨的办法:把数据从GPU/ISP拷贝到CPU内存,再交给编码器。但4K 60fps的数据量有多大?一帧大约12MB,一秒720MB。你拷贝一次试试?CPU直接跑满,手机发烫,掉帧是必然的。

所以,正确的做法是——Buffer共享

23.1.1 共享机制的核心:Surface与ANativeWindow

Android的MediaCodec支持从Surface直接消费数据。什么意思?就是Camera HAL层输出的buffer,可以直接“喂”给编码器的Surface,中间零拷贝。

流程是这样的:

  1. MediaCodec配置编码器时,设置一个Surface作为输出目标。
  2. Camera HAL层通过ANativeWindow接口,把buffer queue注册到编码器的Surface上。
  3. HAL层填充完一帧数据后,通过dequeue/queue机制交给编码器。
  4. 编码器直接从GraphicBuffer中读取数据,编码输出。

嗯,这里要注意:Camera HAL层必须支持GRALLOC_USAGE_HW_VIDEO_ENCODER这个usage flag。否则分配出来的buffer编码器用不了。

关键代码片段: 在HAL层配置buffer时,记得加上这个flag

// Camera HAL层,配置Stream时
stream->usage = GRALLOC_USAGE_SW_READ_OFTEN |
                GRALLOC_USAGE_HW_TEXTURE |
                GRALLOC_USAGE_HW_VIDEO_ENCODER;  // 这个不能少

我曾经在一个项目里,忘了加这个flag,结果编码器拿到的buffer全是花屏。查了两天才发现是usage没配对。这种坑,踩过一次就记住了。

23.1.2 共享的Buffer格式要求

不是所有格式编码器都吃。常见的编码器只认NV12或NV21。如果你HAL层输出的是YV12或者YUV420P,编码器会直接报错。

HAL输出格式 编码器支持情况 备注
NV12 ✅ 广泛支持 首选,性能最好
NV21 ✅ 支持 部分平台兼容
YV12 ❌ 不支持 需要格式转换
YUV420P ❌ 不支持 需要格式转换

所以,你在HAL层配置Camera输出格式时,直接选NV12就对了。别折腾别的格式,省得给自己找麻烦。

23.2 视频编码参数配置

参数配置是门学问。配好了,画质好、文件小;配不好,要么模糊要么文件巨大。

23.2.1 码率控制

码率决定了视频的清晰度。码率太低,画面全是马赛克;码率太高,文件太大,存储和传输都扛不住。

我个人习惯这样配:

  • 4K 30fps:码率设40-60 Mbps
  • 1080p 30fps:码率设15-25 Mbps
  • 720p 30fps:码率设8-12 Mbps

当然,这取决于场景。如果是静态场景,码率可以低一些;如果是运动场景(比如拍赛车),码率要往上加。

小技巧: 使用BITRATE_MODE_VBR(可变码率)比CBR(固定码率)画质更好。VBR会在复杂场景自动提高码率,简单场景降低码率。我一般都用VBR。

23.2.2 帧率配置

帧率要和Camera的帧率匹配。Camera输出30fps,编码器就设30fps。如果Camera输出60fps,编码器设30fps,那就会丢一半的帧,视频看起来一卡一卡的。

反过来,Camera输出30fps,编码器设60fps,编码器会重复插入帧,浪费码率。

所以,帧率一定要保持一致。这个在HAL层和MediaCodec配置时都要确认。

23.2.3 GOP(关键帧间隔)

GOP就是两个I帧之间的间隔。I帧是关键帧,可以独立解码;P帧和B帧依赖前面的帧。

GOP设得越小,视频越容易seek(快进快退),但文件越大。GOP设得越大,文件越小,但seek时会有延迟。

我一般这样配:

  • 普通录制:GOP = 30(每秒一个I帧)
  • 直播/实时场景:GOP = 15(每0.5秒一个I帧,降低延迟)
  • 监控/存储:GOP = 60(每2秒一个I帧,节省空间)

注意: 有些编码器不支持任意GOP值。比如某些硬件编码器只支持GOP为2的幂次方(16、32、64)。配置前先查一下编码器的能力。

23.3 HDR视频录制(HLG/PQ)

HDR是现在的主流。iPhone、三星、华为都在推。Android从10开始原生支持HDR录制。

HDR有两种主流标准:

  • HLG(Hybrid Log-Gamma):兼容SDR显示设备,适合广播电视和直播
  • PQ(Perceptual Quantization):也叫ST 2084,适合电影级制作,需要HDR显示器

在Camera HAL层,HDR录制需要做两件事:

  1. Sensor输出HDR数据:比如10-bit RAW或者多帧合成的HDR YUV
  2. 传递HDR元数据:包括色彩空间、传输函数、色域等

23.3.1 HAL层配置HDR

在配置Camera Stream时,需要设置android.request.availableCapabilities包含DYNAMIC_RANGE_PROFILE,然后指定HDR profile。

// HAL层配置HDR
camera_metadata_entry_t entry;
entry.tag = ANDROID_REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES;
entry.data.i32[0] = ANDROID_DYNAMIC_RANGE_PROFILE_HLG;  // 或者PQ
entry.count = 1;

同时,编码器也要支持HDR。MediaCodec需要配置COLOR_RANGE_LIMITED和对应的色彩空间。

MediaCodec HDR配置示例:

MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, 60000000);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
// HDR相关
format.setInteger(MediaFormat.KEY_COLOR_STANDARD, MediaFormat.COLOR_STANDARD_BT2020);
format.setInteger(MediaFormat.KEY_COLOR_TRANSFER, MediaFormat.COLOR_TRANSFER_HLG);
format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED);

这里要注意:不是所有编码器都支持HDR。高通平台从SM8250(骁龙865)开始支持,联发科从天玑1000开始。老平台就别想了。

23.4 视频防抖(EIS/OIS)与Camera协同

防抖分两种:

  • OIS(光学防抖):硬件层面,通过镜头移动补偿抖动。HAL层只需要使能即可。
  • EIS(电子防抖):软件层面,通过裁剪画面+陀螺仪数据补偿。这个需要HAL层和编码器配合。

23.4.1 EIS与编码器的协同

EIS的原理是:Sensor输出比最终视频更大的画面(比如输出16:10,最终裁剪成16:9),然后根据陀螺仪数据,每帧动态调整裁剪区域。

这就带来一个问题:编码器收到的画面,是裁剪后的还是裁剪前的?

答案是:裁剪后的。HAL层或者ISP在做EIS时,已经把画面裁剪好了,编码器只负责编码最终画面。

但这里有个坑:EIS需要额外的buffer margin。比如你要输出4K视频,Sensor可能需要输出5K的画面,多出来的部分用于裁剪补偿。这会导致带宽和功耗上升。

我的经验: 在低端平台上,EIS和4K HDR同时开,很容易带宽不够。我建议先做性能摸底,看看ISP和编码器能不能扛住。扛不住的话,要么降分辨率,要么关EIS。

23.4.2 OIS的配置

OIS相对简单。在HAL层使能OIS即可:

// 使能OIS
camera_metadata_entry_t entry;
entry.tag = ANDROID_CONTROL_OIS_MODE;
entry.data.i32[0] = ANDROID_CONTROL_OIS_MODE_ON;
entry.count = 1;

OIS对编码器没有直接影响。但要注意:OIS工作时,Sensor的FOV(视场角)会略微变化,因为镜头在移动。这个变化很小,一般不用管。

23.5 实战:实现4K HDR视频录制

好了,理论讲完了。我们来写一个完整的流程。

23.5.1 整体流程

先画个图,让大家看清楚整个链路:

4K HDR视频录制数据流 Camera Sensor 10-bit RAW / HDR ISP / HAL NV12 + HDR元数据 MediaCodec H.265 / H.264 MP4 Muxer 封装输出 陀螺仪 (Gyro) EIS算法 HDR元数据注入 Buffer共享:零拷贝,通过Surface/ANativeWindow传递 HDR元数据:色彩空间BT.2020,传输函数HLG/PQ EIS:陀螺仪数据驱动裁剪,HAL层完成 最终输出:4K HDR MP4文件(含HDR元数据)

23.5.2 关键代码实现

下面是一个简化的实现流程。注意,这里只展示核心逻辑,实际产品中还要处理错误、状态机等。

// 1. 配置Camera HAL层
void configureCameraHDR() {
    // 设置输出格式为NV12
    stream.format = HAL_PIXEL_FORMAT_YCbCr_420_SP;
    stream.usage = GRALLOC_USAGE_HW_VIDEO_ENCODER;
    stream.width = 3840;
    stream.height = 2160;

    // 设置HDR profile
    metadata.add(ANDROID_REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES,
                 ANDROID_DYNAMIC_RANGE_PROFILE_HLG);

    // 使能EIS
    metadata.add(ANDROID_CONTROL_VIDEO_STABILIZATION_MODE,
                 ANDROID_CONTROL_VIDEO_STABILIZATION_MODE_ON);
}

// 2. 配置MediaCodec
void configureMediaCodec(Surface surface) {
    MediaFormat format = MediaFormat.createVideoFormat(
        "video/hevc", 3840, 2160);  // H.265编码
    format.setInteger(MediaFormat.KEY_BIT_RATE, 60000000);  // 60Mbps
    format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
    format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);  // GOP=30
    format.setInteger(MediaFormat.KEY_BITRATE_MODE,
                      MediaFormat.BITRATE_MODE_VBR);

    // HDR元数据
    format.setInteger(MediaFormat.KEY_COLOR_STANDARD,
                      MediaFormat.COLOR_STANDARD_BT2020);
    format.setInteger(MediaFormat.KEY_COLOR_TRANSFER,
                      MediaFormat.COLOR_TRANSFER_HLG);
    format.setInteger(MediaFormat.KEY_COLOR_RANGE,
                      MediaFormat.COLOR_RANGE_LIMITED);

    // 配置编码器
    MediaCodec codec = MediaCodec.createEncoderByType("video/hevc");
    codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    codec.setInputSurface(surface);  // 关键:设置输入Surface
    codec.start();
}

// 3. 启动Camera预览到Surface
void startCameraPreview(Surface surface) {
    // CameraManager.openCamera(...)
    // CameraDevice.createCaptureSession(
    //     Arrays.asList(surface),  // 直接传编码器的Surface
    //     ...);
}

23.5.3 验证与调试

录制完成后,怎么验证HDR是否生效?

  • 用MediaInfo工具查看视频文件的色彩信息,确认是BT.2020 + HLG
  • 在支持HDR的显示器上播放,看亮度范围是否正常
  • 检查编码器输出的SEI NALU,看是否包含了HDR元数据

常见问题: 录出来的视频在普通显示器上颜色发灰?这是正常的。因为SDR显示器无法正确映射HDR的亮度范围。你需要用HDR显示器或者支持HDR转SDR的播放器来看。

好了,这一章的内容就到这里。4K HDR录制涉及的东西不少,但核心就是三点:Buffer共享、参数配置、元数据传递。把这三点搞明白,其他都是细节。

希望各位在实际项目中,能少踩坑,一次点亮。


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