一、视频转码到底是什么?

视频转码,说白了就是「把视频从一种格式变成另一种格式」。比如你手里有个 H.264 编码的 1080p 视频,想转成 H.265 编码的 720p 视频,这就是转码。

我刚开始做音视频开发时,总觉得转码不就是解码再编码吗?后来踩了坑才发现,这里面门道不少。你想想看,解码和编码之间怎么衔接?数据怎么传递?时间戳怎么处理?这些都是问题。

转码的核心流程其实很简单:

  1. 解封装:从 MP4 文件里读出音视频数据包
  2. 解码:把压缩的数据包解成原始帧(比如 YUV 或 PCM)
  3. 滤镜处理:缩放分辨率、改变帧率、旋转等
  4. 编码:把原始帧重新压缩成目标格式
  5. 封装:把编码后的数据包写进新的 MP4 文件

嗯,这里要注意,MediaMuxer 只负责最后一步——封装。前面的解码和滤镜,得靠 MediaCodec 和 MediaExtractor 来完成。

核心要点:MediaMuxer 本身不做转码,它只是把编码后的数据「打包」成 MP4 文件。真正的转码工作,是 MediaCodec 解码 + MediaCodec 编码的组合拳。

二、转码的整体架构

我习惯把转码流程画成一张图,这样思路更清晰。下面是我自己整理的架构图:

视频转码核心流程 输入 MP4 文件 H.264 / AAC MediaExtractor 解封装 视频解码 MediaCodec → YUV 音频解码 MediaCodec → PCM 滤镜处理 缩放/旋转 帧率调整 视频编码 YUV → H.265 音频编码 PCM → AAC MediaMuxer 封装 MP4 输出 输出文件 H.265 / AAC 解封装/封装 编解码 滤镜处理

这张图我画了好几次才满意。你看,左边是输入,右边是输出,中间是编解码和滤镜。MediaMuxer 在右上角,负责把编码后的数据打包成 MP4。

三、MediaMuxer 在转码中的角色

MediaMuxer 在转码流程里,其实只做一件事:把编码后的音视频数据包,按时间顺序写入 MP4 文件。它不关心数据是怎么来的,也不关心数据是什么格式。

我记得第一次做转码时,以为 MediaMuxer 能自动处理一切。结果发现,它连时间戳都要我自己传。嗯,这里有个坑:

时间戳陷阱:MediaMuxer 要求传入的时间戳必须是微秒(microseconds)单位,而且必须是单调递增的。如果你传的时间戳乱跳,生成的 MP4 文件播放时会卡顿或者音画不同步。

MediaMuxer 的核心 API 其实就几个:

方法 作用 注意事项
addTrack() 添加音视频轨道 必须在 start() 之前调用
start() 开始封装 调用后不能再 addTrack()
writeSampleData() 写入一帧数据 时间戳必须递增
stop() 停止封装 调用后释放资源
release() 释放资源 必须调用,否则内存泄漏

四、实战:从 H.264 转码到 H.265

下面我写一个完整的转码示例。这个例子把 H.264 编码的 MP4 文件,转成 H.265 编码,同时把分辨率从 1080p 降到 720p。

public class VideoTranscoder {
    private static final String TAG = "VideoTranscoder";
    private static final int TIMEOUT_USEC = 10000;

    private MediaExtractor extractor;
    private MediaMuxer muxer;
    private MediaCodec decoder;
    private MediaCodec encoder;

    public void transcode(String inputPath, String outputPath) {
        try {
            // 1. 初始化解封装器
            extractor = new MediaExtractor();
            extractor.setDataSource(inputPath);

            // 2. 选择视频轨道
            int videoTrackIndex = selectVideoTrack();
            if (videoTrackIndex < 0) {
                Log.e(TAG, "没有找到视频轨道");
                return;
            }
            extractor.selectTrack(videoTrackIndex);
            MediaFormat inputFormat = extractor.getTrackFormat(videoTrackIndex);

            // 3. 初始化解码器
            String mime = inputFormat.getString(MediaFormat.KEY_MIME);
            decoder = MediaCodec.createDecoderByType(mime);
            decoder.configure(inputFormat, null, null, 0);
            decoder.start();

            // 4. 初始化编码器(H.265,720p)
            MediaFormat outputFormat = MediaFormat.createVideoFormat(
                MediaFormat.MIMETYPE_VIDEO_HEVC, 1280, 720);
            outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, 2_000_000);
            outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
            outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
            outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);

            encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_HEVC);
            encoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            encoder.start();

            // 5. 初始化封装器
            muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            int muxerTrackIndex = muxer.addTrack(outputFormat);
            muxer.start();

            // 6. 开始转码循环
            boolean inputDone = false;
            boolean outputDone = false;
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

            while (!outputDone) {
                // 6.1 喂数据给解码器
                if (!inputDone) {
                    int inputIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
                    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();
                        }
                    }
                }

                // 6.2 从解码器取数据,喂给编码器
                int outputIndex = decoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
                if (outputIndex >= 0) {
                    // 这里可以做滤镜处理(缩放、旋转等)
                    // 为了简化,直接传给编码器
                    ByteBuffer outputBuffer = decoder.getOutputBuffer(outputIndex);
                    // 实际项目中,这里需要做格式转换和缩放
                    // 这里省略了 Surface 渲染的细节
                    decoder.releaseOutputBuffer(outputIndex, false);
                }

                // 6.3 从编码器取数据,写入 Muxer
                int encoderOutputIndex = encoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
                if (encoderOutputIndex >= 0) {
                    ByteBuffer encodedData = encoder.getOutputBuffer(encoderOutputIndex);
                    if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
                        muxer.writeSampleData(muxerTrackIndex, encodedData, bufferInfo);
                    }
                    encoder.releaseOutputBuffer(encoderOutputIndex, false);
                    if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                        outputDone = true;
                    }
                }
            }

            Log.i(TAG, "转码完成");

        } catch (Exception e) {
            Log.e(TAG, "转码失败", e);
        } finally {
            releaseResources();
        }
    }

    private int selectVideoTrack() {
        for (int i = 0; i < extractor.getTrackCount(); i++) {
            MediaFormat format = extractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (mime != null && mime.startsWith("video/")) {
                return i;
            }
        }
        return -1;
    }

    private void releaseResources() {
        if (decoder != null) {
            decoder.stop();
            decoder.release();
            decoder = null;
        }
        if (encoder != null) {
            encoder.stop();
            encoder.release();
            encoder = null;
        }
        if (muxer != null) {
            muxer.stop();
            muxer.release();
            muxer = null;
        }
        if (extractor != null) {
            extractor.release();
            extractor = null;
        }
    }
}

个人经验:上面的代码为了清晰,省略了 Surface 渲染的部分。实际项目中,我建议用 Surface 来做解码输出和编码输入,这样效率更高。具体做法是:解码器配置 Surface,编码器也配置 Surface,然后用 OpenGL 做中间处理。

五、转码中的关键问题

5.1 分辨率缩放

改变分辨率是转码最常见的需求。我建议用 Surface + OpenGL 来做缩放,而不是在 CPU 上做像素操作。为什么呢?因为 GPU 做缩放比 CPU 快得多,而且不占 CPU 资源。

我曾经试过用 Bitmap 做缩放,结果 1080p 的视频转码速度只有 0.5x,卡得不行。换成 Surface 后,速度直接飙到 3x。

5.2 时间戳处理

转码时,时间戳是最容易出问题的地方。我总结了几条经验:

  • 保持原始时间戳:解码器输出的时间戳,直接传给编码器,不要自己算
  • 注意单位:MediaCodec 的时间戳是微秒,MediaMuxer 也是微秒,保持一致
  • 避免跳跃:如果编码器输出的时间戳不是单调递增的,MediaMuxer 会报错

我曾经踩过的坑:有一次转码后视频播放卡顿,查了半天发现是编码器输出的时间戳有重复。原因是编码器内部缓冲导致输出顺序和输入顺序不一致。解决办法是用一个队列来重新排序时间戳。

5.3 编码参数选择

编码参数直接影响输出质量和文件大小。我常用的参数组合:

场景 编码格式 码率 帧率 关键帧间隔
高清存储 H.265 4-8 Mbps 30 1 秒
网络传输 H.264 1-2 Mbps 24 2 秒
缩略图 H.264 500 Kbps 15 5 秒

六、性能优化建议

转码是个吃性能的活,我分享几个优化技巧:

  • 用 Surface 模式:解码器输出到 Surface,编码器从 Surface 读取,避免内存拷贝
  • 控制缓冲区大小:dequeueInputBuffer 的超时时间设 10ms 左右,别太长
  • 异步处理:解码和编码用不同的线程,用队列传递数据
  • 硬件加速:优先使用硬件编解码器,速度比软件快 5-10 倍

我的习惯:在转码开始前,先检查设备支持的编解码器列表。有些设备不支持 H.265 硬件编码,这时候就要降级到 H.264。用 MediaCodecList 可以查到所有可用的编解码器。

好了,这一章的内容就到这里。转码的核心思路就是「解码 → 处理 → 编码 → 封装」,MediaMuxer 只负责最后一步。下一章我们会深入 MediaMuxer 的 addTrack 和 writeSampleData 的细节,看看这两个方法到底做了什么。


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