安卓MediaCodec实现视频文件的转码压缩

8 阅读4分钟

以下为Android平台使用MediaCodec实现视频文件解码→转码→压缩的全流程技术原理与核心代码解析,结合硬件加速与参数优化策略,涵盖关键状态机管理与性能陷阱规避方案。


🔧 ​一、全流程架构与核心组件

  1. 组件分工

    • MediaExtractor​:分离视频文件的音视频轨道,提取编码数据(如H.264)
    • MediaCodec​:硬件加速编解码器,负责解码原始数据→处理→重新编码
    • MediaMuxer​:封装编码后的数据为MP4等容器格式
  2. 流程概览

    graph LR
    A[输入视频] --> B(MediaExtractor提取轨道)
    B --> C{选择视频轨道}
    C --> D[MediaCodec解码]
    D --> E[帧处理/压缩]
    E --> F[MediaCodec编码]
    F --> G[MediaMuxer封装]
    G --> H[输出视频]
    

⚙️ ​二、解码原理与代码实现

1. ​初始化解码器

MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(inputPath); // 输入文件路径
int videoTrackIndex = -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/")) {
        videoTrackIndex = i;
        extractor.selectTrack(i); // 选中轨道
        break;
    }
}
// 配置解码器(以H.264为例)
MediaCodec decoder = MediaCodec.createDecoderByType("video/avc");
decoder.configure(extractor.getTrackFormat(videoTrackIndex), null, null, 0); // Surface设为null
decoder.start();

2. ​解码循环(关键步骤)​

ByteBuffer[] inputBuffers = decoder.getInputBuffers();
ByteBuffer[] outputBuffers = decoder.getOutputBuffers();
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
boolean eos = false;

while (!eos) {
    // 提交压缩数据到解码器
    int inputBufferIndex = decoder.dequeueInputBuffer(10000);
    if (inputBufferIndex >= 0) {
        ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
        int sampleSize = extractor.readSampleData(inputBuffer, 0);
        if (sampleSize < 0) {
            decoder.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            eos = true;
        } else {
            decoder.queueInputBuffer(inputBufferIndex, 0, sampleSize, extractor.getSampleTime(), 0);
            extractor.advance(); // 移动到下一帧
        }
    }
    
    // 获取解码后的原始帧(YUV格式)
    int outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000);
    if (outputBufferIndex >= 0) {
        ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
        // 此处可插入帧处理(如缩放、滤镜等)
        decoder.releaseOutputBuffer(outputBufferIndex, false); // 不渲染到Surface
    }
}

技术要点​:

  • 若需处理解码后的YUV数据,需通过outputBuffer获取原始帧
  • 结束标志BUFFER_FLAG_END_OF_STREAM必须发送,否则解码器会阻塞

🎚️ ​三、转码压缩原理与参数配置

1. ​编码器初始化(关键参数)​

MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");
MediaFormat format = MediaFormat.createVideoFormat("video/avc", targetWidth, targetHeight);
// 压缩核心参数
format.setInteger(MediaFormat.KEY_BIT_RATE, 1_000_000); // 1Mbps码率
format.setInteger(MediaFormat.KEY_FRAME_RATE, 25);      // 帧率
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2);  // 关键帧间隔(秒)
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();

2. ​压缩参数影响分析

参数典型值范围压缩效果
分辨率1080p→720p像素量减少50%,文件体积显著下降
码率 (KEY_BIT_RATE)​5Mbps→1Mbps码率降低80%,可能引入画面模糊
帧率30fps→24fps动态细节减少,体积下降约20%
关键帧间隔1秒→5秒GOP增大,压缩率提升,但快进/seek可能卡顿

注意​:颜色格式需通过MediaCodecInfo查询设备支持,优先选COLOR_FormatYUV420Flexible


🔄 ​四、转码全流程桥接(解码→编码→封装)​

1. ​MediaMuxer初始化

MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
int videoTrack = -1; // 等待编码器输出格式确定后再添加轨道

2. ​数据流转核心循环

while (true) {
    // 从解码器获取原始帧(YUV)
    int decoderOutputIndex = decoder.dequeueOutputBuffer(bufferInfo, TIMEOUT);
    if (decoderOutputIndex >= 0) {
        ByteBuffer rawFrame = decoder.getOutputBuffer(decoderOutputIndex);
        
        // 提交原始帧到编码器
        int encoderInputIndex = encoder.dequeueInputBuffer(TIMEOUT);
        if (encoderInputIndex >= 0) {
            ByteBuffer encoderInputBuffer = encoder.getInputBuffer(encoderInputIndex);
            encoderInputBuffer.put(rawFrame);
            encoder.queueInputBuffer(encoderInputIndex, 0, bufferInfo.size, bufferInfo.presentationTimeUs, 0);
        }
        decoder.releaseOutputBuffer(decoderOutputIndex, false);
    }
    
    // 从编码器获取压缩数据
    int encoderOutputIndex = encoder.dequeueOutputBuffer(bufferInfo, TIMEOUT);
    if (encoderOutputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        videoTrack = muxer.addTrack(encoder.getOutputFormat()); // 添加轨道
        muxer.start();
    } else if (encoderOutputIndex >= 0) {
        ByteBuffer encodedData = encoder.getOutputBuffer(encoderOutputIndex);
        muxer.writeSampleData(videoTrack, encodedData, bufferInfo); // 写入文件
        encoder.releaseOutputBuffer(encoderOutputIndex, false);
    }
}

关键逻辑​:

  • 编码器输出格式变化(INFO_OUTPUT_FORMAT_CHANGED)时必须通知MediaMuxer
  • 时间戳presentationTimeUs需保持连续性,否则播放异常

🚀 ​五、性能优化与进阶技巧

  1. 异步模式(API 21+)​
    避免轮询阻塞,使用Callback提升效率:

    encoder.setCallback(new MediaCodec.Callback() {
        @Override
        public void onInputBufferAvailable(MediaCodec mc, int index) {
            // 填充数据到编码器
        }
        @Override
        public void onOutputBufferAvailable(MediaCodec mc, int index, BufferInfo info) {
            // 处理压缩数据并提交给MediaMuxer
        }
    });
    
  2. Surface输入(零拷贝)​
    直接绑定Surface到编码器,跳过CPU处理:

    Surface inputSurface = encoder.createInputSurface();
    // 将解码器输出渲染到inputSurface(需OpenGL ES上下文)
    
  3. 动态码率调整
    根据网络或存储需求实时调整码率:

    Bundle params = new Bundle();
    params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, newBitrate);
    encoder.setParameters(params);
    

⚠️ ​六、常见问题解决

  1. 绿屏/花屏

    • 原因:颜色格式不匹配或帧数据未对齐
    • 方案:确认设备支持的COLOR_FORMAT,YUV数据需满足分辨率对齐要求(通常是2的倍数)
  2. 帧率骤降

    • 原因:编码器处理超时
    • 优化:降低分辨率/码率,或使用Surface输入跳过YUV转换
  3. 封装失败

    • 原因:未添加轨道或时间戳错误
    • 检查:确保在INFO_OUTPUT_FORMAT_CHANGED后才启动MediaMuxer

💎 ​总结与最佳实践

  1. 核心流程​:Extractor→Decoder→Frame Processing→Encoder→Muxer

  2. 参数黄金法则​:分辨率 > 码率 > 帧率 > GOP,按需求牺牲画质换体积

  3. 必选优化​:

    • 异步模式处理I/O避免阻塞
    • Surface输入减少CPU-YUV拷贝
    • 动态码率适应场景需求

完整代码参考:Android MediaCodec官方示例
深入性能调优:分析dumpsys media.codec获取编解码器实时状态