一、为什么需要给无声视频加BGM?

说实话,我刚开始做音视频编辑时,遇到最多的需求就是「给视频加个背景音乐」。你想想看,用户拍的视频往往没有声音,或者原声太嘈杂。加一段合适的BGM,整个视频的质感立马就上来了。

MediaMuxer这个组件,说白了就是个「音视频打包器」。它能把视频轨道和音频轨道合并成一个文件。嗯,这里要注意——它只管混合,不管编解码。所以我们需要先准备好解码后的数据。

核心思路:用MediaExtractor读取视频,用MediaCodec解码视频帧,同时用另一个MediaExtractor读取音频文件,再用MediaMuxer把两路数据合成输出。

二、整体流程设计

我个人习惯把流程分成三步走:

  1. 准备阶段——打开视频文件、音频文件,配置MediaMuxer的输出格式
  2. 处理阶段——逐帧读取视频数据,逐段读取音频数据,写入同一个输出文件
  3. 收尾阶段——释放资源,检查输出文件是否完整

这里我画了一张流程图,帮你理清整体脉络:

MediaMuxer 视频+BGM 合成流程 输入视频文件(无声) 输入音频文件(BGM) MediaExtractor(视频) MediaExtractor(音频) MediaMuxer 混合视频轨道 + 音频轨道 输出 MP4(带BGM) 视频轨道数据 音频轨道数据

三、核心代码实现

好,我们直接上代码。我会把关键部分拆开讲,你跟着走一遍就能跑通。

3.1 初始化MediaMuxer

// 设置输出文件路径
String outputPath = Environment.getExternalStorageDirectory() + "/output_with_bgm.mp4";
MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

// 注意:先添加轨道,再start()
int videoTrackIndex = muxer.addTrack(videoMediaFormat);
int audioTrackIndex = muxer.addTrack(audioMediaFormat);
muxer.start();

我曾经踩过的坑:MediaMuxer的addTrack()必须在start()之前调用。如果你先调了start()再addTrack(),会直接抛IllegalStateException。嗯,这个错误信息我看了不下十次。

3.2 读取视频数据并写入

MediaExtractor videoExtractor = new MediaExtractor();
videoExtractor.setDataSource(videoPath);
int videoTrack = selectTrack(videoExtractor, "video/");
videoExtractor.selectTrack(videoTrack);

MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); // 1MB缓冲区

while (true) {
    int sampleSize = videoExtractor.readSampleData(buffer, 0);
    if (sampleSize < 0) break;
    
    info.offset = 0;
    info.size = sampleSize;
    info.flags = videoExtractor.getSampleFlags();
    info.presentationTimeUs = videoExtractor.getSampleTime();
    
    muxer.writeSampleData(videoTrackIndex, buffer, info);
    videoExtractor.advance();
}

这里有个细节——presentationTimeUs必须严格递增。我遇到过视频画面跳帧的情况,排查了半天,发现是时间戳没有对齐。你想想看,MediaMuxer内部是按时间戳排序的,乱序就会出问题。

3.3 读取音频数据并写入

MediaExtractor audioExtractor = new MediaExtractor();
audioExtractor.setDataSource(audioPath);
int audioTrack = selectTrack(audioExtractor, "audio/");
audioExtractor.selectTrack(audioTrack);

while (true) {
    int sampleSize = audioExtractor.readSampleData(buffer, 0);
    if (sampleSize < 0) break;
    
    info.offset = 0;
    info.size = sampleSize;
    info.flags = audioExtractor.getSampleFlags();
    info.presentationTimeUs = audioExtractor.getSampleTime();
    
    muxer.writeSampleData(audioTrackIndex, buffer, info);
    audioExtractor.advance();
}

小技巧:如果BGM比视频长,你可以在读取音频时加一个判断——当音频时间戳超过视频总时长时,直接break。这样BGM就不会「拖尾」了。我个人习惯在视频读取完后记录一下总时长,传给音频循环用。

3.4 收尾工作

// 先释放提取器
videoExtractor.release();
audioExtractor.release();

// 最后停止并释放muxer
muxer.stop();
muxer.release();

顺序很重要。一定要先release提取器,再stop muxer。我曾经反过来写过,结果输出文件损坏了,播放器直接报「无法播放此视频」。

四、避坑指南

做这个功能,有几个坑我不得不提:

常见问题 原因 解决方案
输出文件无法播放 muxer.stop()前没有写入所有数据 确保两个while循环都完整执行
音视频不同步 时间戳没有对齐 检查presentationTimeUs是否连续递增
BGM音量太大盖过原声 没有做音量混音处理 用AudioMixer或MediaCodec调整音量
内存溢出 ByteBuffer分配过大 建议1MB以内,根据实际帧大小动态调整

五、完整代码示例

我把上面所有片段拼成一个完整的方法,你直接复制就能用:

public void mergeVideoAndAudio(String videoPath, String audioPath, String outputPath) {
    MediaMuxer muxer = null;
    MediaExtractor videoExtractor = null;
    MediaExtractor audioExtractor = null;
    
    try {
        // 1. 初始化视频提取器
        videoExtractor = new MediaExtractor();
        videoExtractor.setDataSource(videoPath);
        int videoTrackIndex = selectTrack(videoExtractor, "video/");
        videoExtractor.selectTrack(videoTrackIndex);
        MediaFormat videoFormat = videoExtractor.getTrackFormat(videoTrackIndex);
        
        // 2. 初始化音频提取器
        audioExtractor = new MediaExtractor();
        audioExtractor.setDataSource(audioPath);
        int audioTrackIndex = selectTrack(audioExtractor, "audio/");
        audioExtractor.selectTrack(audioTrackIndex);
        MediaFormat audioFormat = audioExtractor.getTrackFormat(audioTrackIndex);
        
        // 3. 初始化Muxer
        muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
        int videoMuxerIndex = muxer.addTrack(videoFormat);
        int audioMuxerIndex = muxer.addTrack(audioFormat);
        muxer.start();
        
        // 4. 写入视频数据
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
        
        while (true) {
            int sampleSize = videoExtractor.readSampleData(buffer, 0);
            if (sampleSize < 0) break;
            info.offset = 0;
            info.size = sampleSize;
            info.flags = videoExtractor.getSampleFlags();
            info.presentationTimeUs = videoExtractor.getSampleTime();
            muxer.writeSampleData(videoMuxerIndex, buffer, info);
            videoExtractor.advance();
        }
        
        // 5. 写入音频数据
        while (true) {
            int sampleSize = audioExtractor.readSampleData(buffer, 0);
            if (sampleSize < 0) break;
            info.offset = 0;
            info.size = sampleSize;
            info.flags = audioExtractor.getSampleFlags();
            info.presentationTimeUs = audioExtractor.getSampleTime();
            muxer.writeSampleData(audioMuxerIndex, buffer, info);
            audioExtractor.advance();
        }
        
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (videoExtractor != null) videoExtractor.release();
        if (audioExtractor != null) audioExtractor.release();
        if (muxer != null) {
            muxer.stop();
            muxer.release();
        }
    }
}

private int selectTrack(MediaExtractor extractor, String mimeType) {
    for (int i = 0; i < extractor.getTrackCount(); i++) {
        MediaFormat format = extractor.getTrackFormat(i);
        String mime = format.getString(MediaFormat.KEY_MIME);
        if (mime.startsWith(mimeType)) {
            return i;
        }
    }
    return -1;
}

六、总结

MediaMuxer做视频加BGM,说白了就是「拆开-混合-打包」三个动作。你只要记住:

  • 视频和音频分别用两个MediaExtractor读取
  • 时间戳必须严格递增
  • 释放顺序不能乱

做到这三点,基本不会出大问题。我刚开始做这个功能时,也踩过不少坑,但跑通一次之后,后面就顺了。嗯,代码量不大,但细节确实多。

最后提醒一句:如果你要处理的是带原声的视频,那就不能直接用MediaMuxer了——它只能混合,不能混音。那时候你需要用AudioMixer或者MediaCodec做PCM级别的混音。这个我们后面章节会讲到。

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