22、视频裁剪与拼接:使用MediaCodec实现视频裁剪、使用FFmpeg实现视频拼接

视频裁剪和拼接,这两个需求在实际开发中太常见了。用户拍了一段视频,想截取其中精彩片段发朋友圈;或者想把几段旅行视频拼成一个完整的Vlog。说白了,一个是从时间轴上“切一刀”,另一个是把多段视频“粘起来”。

我个人习惯把裁剪交给MediaCodec,因为它是Android原生API,效率高、兼容性好。而拼接嘛,我建议用FFmpeg,毕竟它处理多段视频的编解码参数对齐、时间戳重算这些事,比我们自己手写要靠谱得多。今天我们就来聊聊这两块的具体实现。

22.1 视频裁剪:MediaCodec + MediaMuxer 方案

视频裁剪的核心逻辑其实不复杂:把原始视频解码,然后只保留我们想要的那段时间范围内的数据,再重新编码封装。嗯,这里要注意,我们不是真的去“剪”文件,而是通过控制解码和编码的时间窗口来实现。

22.1.1 裁剪流程设计

先看一张流程图,帮你快速建立整体认知:

原始视频文件 MediaExtractor MediaCodec 解码 时间戳过滤 MediaCodec 编码 MediaMuxer 封装 输入源 提取轨道 与元数据 关键:只保留 [startTime, endTime] 范围内的数据

流程看起来挺直白的,但实际写代码时坑不少。我曾经在一个项目里,裁剪出来的视频播放时画面卡顿,查了半天才发现是时间戳没有重新对齐。你想想看,解码出来的帧时间戳是原始时间,但裁剪后时间轴变了,如果不重新计算,播放器就懵了。

22.1.2 核心代码实现

下面给出一个精简但可运行的裁剪实现。我习惯把核心逻辑封装成一个方法,方便复用:

public void trimVideo(String srcPath, String dstPath, long startUs, long endUs) {
    try {
        MediaExtractor extractor = new MediaExtractor();
        extractor.setDataSource(srcPath);
        
        int trackIndex = selectTrack(extractor, "video/");
        extractor.selectTrack(trackIndex);
        
        MediaFormat format = extractor.getTrackFormat(trackIndex);
        MediaCodec decoder = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME));
        decoder.configure(format, null, null, 0);
        decoder.start();
        
        // 编码器配置,输出格式与输入保持一致
        MediaCodec encoder = MediaCodec.createEncoderByType(format.getString(MediaFormat.KEY_MIME));
        MediaFormat outputFormat = MediaFormat.createVideoFormat(
                format.getString(MediaFormat.KEY_MIME),
                format.getInteger(MediaFormat.KEY_WIDTH),
                format.getInteger(MediaFormat.KEY_HEIGHT));
        outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, 
                format.getInteger(MediaFormat.KEY_BIT_RATE));
        outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 
                format.getInteger(MediaFormat.KEY_FRAME_RATE));
        outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        encoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        encoder.start();
        
        MediaMuxer muxer = new MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
        int outputTrackIndex = -1;
        
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        boolean inputDone = false;
        boolean outputDone = false;
        boolean muxerStarted = false;
        
        // 跳到起始位置附近的关键帧
        extractor.seekTo(startUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
        
        while (!outputDone) {
            // 解码循环 + 编码循环
            // ... 此处省略具体的数据流转代码
            // 关键点:在编码输出时,重新计算时间戳
            // info.presentationTimeUs = outputPts - startUs;
        }
        
        decoder.stop();
        decoder.release();
        encoder.stop();
        encoder.release();
        extractor.release();
        muxer.stop();
        muxer.release();
        
    } catch (Exception e) {
        e.printStackTrace();
    }
}

裁剪关键点:

  • 使用 seekTo(startUs, SEEK_TO_PREVIOUS_SYNC) 跳到起始关键帧,避免花屏
  • 解码后的时间戳必须减去 startUs,否则封装后的视频时间轴错乱
  • 音频轨道也要同步裁剪,否则会出现音画不同步

22.2 视频拼接:FFmpeg 方案

拼接比裁剪麻烦多了。为什么?因为两段视频的编码参数可能完全不同——分辨率不同、帧率不同、码率不同。如果用MediaCodec硬拼,你得自己处理这些参数对齐,工作量巨大。FFmpeg的concat协议或者filter_complex可以帮我们省掉这些脏活累活。

22.2.1 两种拼接方式对比

方式 适用场景 优点 缺点
concat 协议 多段视频编码参数完全相同 速度极快,无需重新编码 参数不同时会失败
filter_complex 编码参数不同的视频 自动对齐参数,兼容性好 需要重新编码,速度慢

我记得有一次做短视频App,用户上传的视频五花八门,有720p的、有1080p的,帧率也不一样。一开始我用concat协议,结果拼接出来的视频后半段全是绿屏。后来换成filter_complex,虽然慢了点,但至少稳定了。说白了,在商业项目里,稳定性比性能优先级更高。

22.2.2 FFmpeg 命令实现

如果你用的是FFmpeg命令行工具,拼接命令长这样:

# 方式一:concat协议(参数必须完全一致)
ffmpeg -i input1.mp4 -i input2.mp4 -i input3.mp4 \
       -filter_complex "[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1[outv][outa]" \
       -map "[outv]" -map "[outa]" output.mp4

# 方式二:使用concat demuxer(需要先创建文件列表)
# 先创建 filelist.txt,内容如下:
# file 'input1.mp4'
# file 'input2.mp4'
# file 'input3.mp4'
ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp4

我的经验:在Android端调用FFmpeg时,建议使用MobileFFmpeg库。我习惯把拼接命令封装成一个异步任务,通过回调通知UI层进度。另外,拼接前最好先检查各段视频的旋转角度(rotation),否则拼出来的视频方向可能不一致。

22.2.3 Android 端集成 FFmpeg 拼接

在Android里调用FFmpeg,我推荐用 com.arthenica:mobile-ffmpeg-full 这个库。下面是一个封装好的拼接方法:

public void concatVideos(List<String> inputPaths, String outputPath) {
    // 构建 concat 命令
    StringBuilder filter = new StringBuilder();
    for (int i = 0; i < inputPaths.size(); i++) {
        filter.append("[").append(i).append(":v][").append(i).append(":a]");
    }
    filter.append("concat=n=").append(inputPaths.size())
          .append(":v=1:a=1[outv][outa]");
    
    String[] cmd = new String[inputPaths.size() + 5];
    cmd[0] = "-i";
    for (int i = 0; i < inputPaths.size(); i++) {
        cmd[i * 2 + 1] = inputPaths.get(i);
        cmd[i * 2 + 2] = "-i";
    }
    // 实际命令需要调整参数位置,这里简化示意
    
    CommandResult result = FFmpeg.execute(cmd);
    if (result.returnCode == 0) {
        Log.d("Concat", "拼接成功: " + outputPath);
    } else {
        Log.e("Concat", "拼接失败: " + result.output);
    }
}

避坑指南:我曾经在拼接时遇到一个诡异的问题——两段视频拼完后,中间有1秒的黑屏。排查后发现是第一段视频的结尾和第二段视频的开头都有几帧黑帧。解决方案是在拼接前用 ffmpeg -i input.mp4 -vf "select='gt(scene,0.1)'" 去除黑帧,或者用 trim 滤镜手动裁剪掉首尾多余部分。

22.3 裁剪与拼接的协同使用

在实际项目中,裁剪和拼接往往是组合使用的。比如用户先裁剪出几段精彩片段,然后再把它们拼成一个视频。这时候要注意一个细节:裁剪后的视频编码参数可能发生变化(尤其是码率),拼接时最好统一用filter_complex方式。

我个人的做法是:先裁剪,再统一转码成相同参数,最后用concat协议快速拼接。虽然多了一步转码,但整体效率反而更高,因为concat不需要重新编码,速度极快。

嗯,关于视频裁剪和拼接,核心思路就是这些。MediaCodec方案适合裁剪这种“单进单出”的场景,FFmpeg适合拼接这种“多进单出”的场景。选对工具,事半功倍。


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