初探 Android MediaCodec:用硬编码压缩视频

5 阅读4分钟

初探 Android MediaCodec:用硬编码压缩视频

前言

最近对音视频开发产生了兴趣,想了解下 Android 上是怎么处理视频编解码的。花了几天时间研究了 MediaCodec API,写了个简单的视频压缩 Demo,记录下学习过程。

为什么要学 MediaCodec

之前做 Launcher 定制时,遇到过一个需求:用户录制的视频太大,需要压缩后再上传。当时用的第三方库 FFmpeg,但打包后 APK 体积增加了 20MB+。

后来发现 Android 原生就有 MediaCodec API,可以直接调用硬件编解码器,性能好、体积小。所以决定研究下。

MediaCodec 是什么

MediaCodec 是 Android 提供的底层编解码 API(API 16+),可以访问设备的硬件编解码器(如高通的 Adreno、联发科的 MDP)。

优势:

  • 硬件加速,速度快
  • 功耗低
  • 无需引入第三方库

劣势:

  • API 比较底层,使用复杂
  • 不同设备兼容性问题

视频编解码基础

在开始前,先理解几个概念:

1. 编码格式

  • H.264 (AVC):最常用,兼容性好
  • H.265 (HEVC):压缩率更高,但部分设备不支持
  • VP8/VP9:Google 推的开源格式

2. 关键帧和 P 帧

  • I 帧(关键帧):完整的图像,可以独立解码
  • P 帧:只存储与前一帧的差异,体积小

视频压缩的本质就是减少 I 帧数量,增加 P 帧。

3. 码率(Bitrate)

码率决定视频质量和文件大小。常见设置:

  • 1080p:8-10 Mbps
  • 720p:4-6 Mbps
  • 480p:1-2 Mbps

MediaCodec 工作流程

1. 创建编码器:MediaCodec.createEncoderByType("video/avc")
2. 配置参数:configure(format, null, null, CONFIGURE_FLAG_ENCODE)
3. 启动编码器:start()
4. 循环处理:
   - 获取输入缓冲区:dequeueInputBuffer()
   - 填充原始数据:getInputBuffer()
   - 提交数据:queueInputBuffer()
   - 获取输出缓冲区:dequeueOutputBuffer()
   - 读取编码数据:getOutputBuffer()
   - 释放缓冲区:releaseOutputBuffer()
5. 停止编码器:stop() / release()

实战:压缩视频

1. 创建和配置编码器

MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");
MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000); // 2Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 1秒一个关键帧
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();

2. 解码原视频并重新编码

MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(inputPath);

// 找到视频轨道
int videoTrack = -1;
for (int i = 0; i < extractor.getTrackCount(); i++) {
    MediaFormat format = extractor.getTrackFormat(i);
    String mime = format.getString(MediaFormat.KEY_MIME);
    if (mime.startsWith("video/")) {
        videoTrack = i;
        break;
    }
}
extractor.selectTrack(videoTrack);

// 创建解码器
MediaCodec decoder = MediaCodec.createDecoderByType(mime);
decoder.configure(format, null, null, 0);
decoder.start();

3. 处理循环(简化版)

boolean inputDone = false;
boolean outputDone = false;

while (!outputDone) {
    // 喂数据给解码器
    if (!inputDone) {
        int inputIndex = decoder.dequeueInputBuffer(10000);
        if (inputIndex >= 0) {
            ByteBuffer inputBuffer = decoder.getInputBuffer(inputIndex);
            int sampleSize = extractor.readSampleData(inputBuffer, 0);
            if (sampleSize < 0) {
                decoder.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                inputDone = true;
            } else {
                decoder.queueInputBuffer(inputIndex, 0, sampleSize, extractor.getSampleTime(), 0);
                extractor.advance();
            }
        }
    }

    // 获取解码后的数据,喂给编码器
    MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
    int outputIndex = decoder.dequeueOutputBuffer(info, 10000);
    if (outputIndex >= 0) {
        // 这里需要将解码后的 YUV 数据传给编码器
        // 实际项目中会用 Surface 来传递,避免数据拷贝
        decoder.releaseOutputBuffer(outputIndex, true);
    }
}

踩过的坑

坑1:颜色格式不匹配

一开始解码出来的是 YUV420 格式,但编码器期望的是 Surface 输入。导致画面颜色错乱。

解决: 使用 Surface 模式,让解码器直接输出到 Surface,编码器从 Surface 读取。

Surface surface = MediaCodec.createPersistentInputSurface();
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.setInputSurface(surface);
decoder.configure(format, surface, null, 0);

坑2:时间戳不连续

编码后的视频播放卡顿,检查发现是时间戳(PTS)没处理好。

解决: 必须保证时间戳单调递增,且间隔均匀。

long presentationTimeUs = extractor.getSampleTime();
decoder.queueInputBuffer(inputIndex, 0, sampleSize, presentationTimeUs, 0);

坑3:部分设备不支持硬编码

在某些低端设备上,createEncoderByType() 直接崩溃。

解决: 先检查设备是否支持硬编码。

MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
MediaCodecInfo[] codecInfos = codecList.getCodecInfos();
for (MediaCodecInfo info : codecInfos) {
    if (info.isEncoder() && info.getName().contains("avc")) {
        // 支持 H.264 硬编码
    }
}

性能测试

测试环境:小米 10(骁龙 865),压缩 1080p 30fps 视频

方案耗时CPU 占用文件大小
FFmpeg 软编码45秒80%15MB
MediaCodec 硬编码8秒20%12MB

硬编码速度快 5 倍,CPU 占用低很多。

学习总结

通过这次实践,对音视频编解码有了初步认识:

  1. MediaCodec 是底层 API,使用复杂但性能好
  2. 时间戳很重要,直接影响播放流畅度
  3. 硬件兼容性是个大问题,需要做好降级方案

后续计划继续学习:

  • MediaMuxer 封装音视频
  • OpenGL 处理视频滤镜
  • 实时音视频通信(WebRTC)

参考资料


音视频是个大坑,慢慢填。有问题欢迎交流!