9. MediaCodec编码实战:H.264/H.265硬编码、编码参数配置、实时编码

各位同学,今天我们来聊聊MediaCodec编码实战。说实话,编码这块儿在Android音视频开发里,算是比较硬核的部分了。我当年刚接触时也被各种参数搞得头晕,但一旦摸透了,你会发现它其实很有规律。

9.1 硬编码 vs 软编码:你该怎么选?

先问个问题:为什么我们要用硬编码?说白了,硬编码就是让GPU或专门的硬件模块去干活,软编码则是CPU硬扛。我做过一个项目,用软编码在低端机上跑1080p视频,手机直接烫得能煎鸡蛋。换成硬编码后,温度降了十几度,帧率还稳了。

对比项 硬编码 软编码
CPU占用 低(5%-15%) 高(30%-80%)
功耗
编码质量 中等(取决于芯片) 高(可精细控制)
灵活性 低(参数受限) 高(可自定义算法)
适用场景 实时推流、录制 离线转码、专业制作

我个人习惯是:实时场景一律硬编码,离线场景才考虑软编码。你想想看,用户正在直播,你让CPU满载,画面卡顿不说,电量也撑不住。

9.2 MediaCodec编码器初始化

初始化编码器,其实就三步:创建编码器、配置参数、启动。但每一步都有坑,我一个个说。

9.2.1 创建编码器

Android提供了两种方式创建编码器。我建议用第二种,更灵活。

// 方式一:直接指定MIME类型
MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");

// 方式二:通过MediaCodecList查找(推荐)
MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
String codecName = codecList.findEncoderForFormat(mediaFormat);
MediaCodec encoder = MediaCodec.createByCodecName(codecName);

为什么要用第二种?因为有些设备上,你指定的编码器可能不支持你想要的参数。通过MediaCodecList查找,能确保找到可用的编码器。我曾经在某个国产平板上踩过这个坑,直接指定"video/avc"结果编码出来的视频全是花屏。

9.2.2 配置编码参数

配置参数是编码的核心。MediaFormat就像一张配置表,你往里面填参数,编码器按这个干活。

MediaFormat format = MediaFormat.createVideoFormat("video/avc", 1920, 1080);
format.setInteger(MediaFormat.KEY_BIT_RATE, 4_000_000);      // 4Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);           // 30fps
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);      // 关键帧间隔1秒
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, 
    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);   // 输入格式
format.setInteger(MediaFormat.KEY_PROFILE, 
    MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);        // H.264 High Profile
format.setInteger(MediaFormat.KEY_LEVEL, 
    MediaCodecInfo.CodecProfileLevel.AVCLevel41);            // Level 4.1

关键参数说明:

  • BIT_RATE:码率,直接影响画质和文件大小。1080p一般2-8Mbps
  • FRAME_RATE:帧率,30fps是主流,60fps对硬件要求高
  • I_FRAME_INTERVAL:关键帧间隔,直播场景建议1秒,点播可以2-3秒
  • COLOR_FORMAT:颜色格式,Surface输入效率最高

9.2.3 启动编码器

encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();

嗯,这里要注意:configure的第三个参数是MediaCrypto,编码时传null就行。第四个参数必须传CONFIGURE_FLAG_ENCODE,否则编码器不会启动。

9.3 H.264 vs H.265:选哪个?

这个问题我经常被问到。直接说结论:

  • H.264:兼容性最好,几乎所有设备都支持。码率控制成熟,画质稳定。
  • H.265:压缩率更高,同等画质下码率能省30%-50%。但编码更耗电,老设备可能不支持。

我个人的经验是:如果是做直播或短视频,优先用H.264。因为用户设备千奇百怪,H.264最保险。如果是做本地录制或点播,可以考虑H.265,能省不少存储空间。

// H.265编码器创建
MediaFormat format = MediaFormat.createVideoFormat("video/hevc", 1920, 1080);
// 其他参数配置与H.264类似
format.setInteger(MediaFormat.KEY_PROFILE, 
    MediaCodecInfo.CodecProfileLevel.HEVCProfileMain);

9.4 实时编码流程

实时编码的核心是循环:不断从输入队列取数据,编码后放入输出队列。我画了一张流程图,帮你理解这个过程。

MediaCodec实时编码流程 原始视频帧(YUV/Surface) dequeueInputBuffer() 获取输入缓冲区 queueInputBuffer() 送入编码器 dequeueOutputBuffer() 获取编码数据 H.264/H.265 码流 循环

看到这个循环了吗?这就是编码器的核心工作模式。我写个完整的代码示例:

// 实时编码循环
while (!isStopRequested) {
    // 1. 获取输入缓冲区
    int inputIndex = encoder.dequeueInputBuffer(10000); // 10秒超时
    if (inputIndex >= 0) {
        ByteBuffer inputBuffer = encoder.getInputBuffer(inputIndex);
        // 填充YUV数据到inputBuffer
        // ...
        encoder.queueInputBuffer(inputIndex, 0, dataSize, presentationTimeUs, 0);
    }
    
    // 2. 获取输出缓冲区
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000);
    
    if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
        // 没有数据,稍后重试
        continue;
    } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        // 输出格式变化,通常在编码开始时触发
        MediaFormat newFormat = encoder.getOutputFormat();
        // 处理格式变化(如初始化muxer)
    } else if (outputIndex >= 0) {
        ByteBuffer outputBuffer = encoder.getOutputBuffer(outputIndex);
        // 处理编码后的数据(写入文件或发送网络)
        // ...
        encoder.releaseOutputBuffer(outputIndex, false);
    }
}

小技巧:dequeueInputBuffer的超时时间不要设太长,10毫秒就够了。设太长会导致编码延迟增加,实时场景下很致命。

9.5 编码参数调优实战

参数配置不是一成不变的,得根据场景调。我分享几个实战经验:

9.5.1 码率控制模式

MediaCodec支持三种码率控制模式:

// CBR:恒定码率,适合直播
format.setInteger(MediaFormat.KEY_BITRATE_MODE, 
    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);

// VBR:可变码率,适合本地录制
format.setInteger(MediaFormat.KEY_BITRATE_MODE, 
    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);

// CQ:恒定质量,适合专业场景
format.setInteger(MediaFormat.KEY_BITRATE_MODE, 
    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ);

我个人习惯:直播用CBR,保证码率稳定不卡顿;本地录制用VBR,画质更好;CQ模式慎用,有些设备不支持。

9.5.2 编码质量与性能平衡

有些设备上,你设了高码率但编码器跑不动,就会丢帧。我建议先做一次性能探测:

// 获取编码器能力
MediaCodecInfo codecInfo = new MediaCodecList(MediaCodecList.REGULAR_CODECS)
    .findCodecByName(codecName);
MediaCodecInfo.CodecCapabilities capabilities = 
    codecInfo.getCapabilitiesForType("video/avc");

// 检查支持的码率范围
int maxBitrate = capabilities.getVideoCapabilities().getBitrateRange().getUpper();
int maxFrameRate = capabilities.getVideoCapabilities().getSupportedFrameRates().getUpper();

// 根据能力设置参数
int targetBitrate = Math.min(4_000_000, maxBitrate); // 不超过设备上限

注意:不要盲目设高参数。我曾经在某个低端机上设了4K@60fps编码,结果编码器直接崩溃。一定要先检查设备能力,再设参数。

9.6 常见问题与避坑指南

做编码开发,总会遇到各种问题。我整理了几个高频问题:

  • 编码出来的视频花屏:通常是输入数据格式不对。检查YUV数据排列方式,Android上默认是NV12或NV21。
  • 编码延迟越来越高:可能是输出缓冲区没及时释放。记得每次dequeueOutputBuffer后都要releaseOutputBuffer。
  • 关键帧不生效:有些设备上I_FRAME_INTERVAL设了1但实际不生效。可以手动请求关键帧:encoder.setParameters(new Bundle().putInt("request-sync", 0))
  • 编码器崩溃:多半是参数超出设备能力。用MediaCodecList查一下支持的Profile和Level。

我曾经遇到过一个奇葩问题:某款手机上编码出来的视频时间戳全乱套。排查了半天,发现是presentationTimeUs传的是系统时间,而不是相对时间。记住,时间戳要从0开始递增,单位是微秒。

9.7 性能优化建议

最后,分享几个性能优化的经验:

  1. 使用Surface输入:比ByteBuffer输入效率高很多,尤其是Camera预览直接编码的场景。
  2. 控制编码帧率:不要超过Camera的实际帧率,否则编码器会积压。
  3. 合理设置缓冲区数量:默认的缓冲区数量可能不够,可以尝试增加:format.setInteger("max-input-size", width * height * 3 / 2)
  4. 异步模式:Android 5.0+支持异步编码,用Callback代替循环,代码更简洁,性能也更好。
// 异步编码示例
encoder.setCallback(new MediaCodec.Callback() {
    @Override
    public void onInputBufferAvailable(MediaCodec codec, int index) {
        // 填充数据
    }
    
    @Override
    public void onOutputBufferAvailable(MediaCodec codec, int index, BufferInfo info) {
        // 处理编码数据
    }
    
    @Override
    public void onError(MediaCodec codec, MediaCodec.CodecException e) {
        // 错误处理
    }
    
    @Override
    public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
        // 格式变化
    }
});

嗯,编码这块儿内容不少,但核心就是理解编码器的工作流程,然后根据场景调参数。多动手写代码,遇到问题别慌,先查设备能力,再查数据格式,基本能解决90%的问题。

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