22、视频裁剪与拼接:使用MediaCodec实现视频裁剪、使用FFmpeg实现视频拼接
视频裁剪和拼接,这两个需求在实际开发中太常见了。用户拍了一段视频,想截取其中精彩片段发朋友圈;或者想把几段旅行视频拼成一个完整的Vlog。说白了,一个是从时间轴上“切一刀”,另一个是把多段视频“粘起来”。
我个人习惯把裁剪交给MediaCodec,因为它是Android原生API,效率高、兼容性好。而拼接嘛,我建议用FFmpeg,毕竟它处理多段视频的编解码参数对齐、时间戳重算这些事,比我们自己手写要靠谱得多。今天我们就来聊聊这两块的具体实现。
22.1 视频裁剪:MediaCodec + MediaMuxer 方案
视频裁剪的核心逻辑其实不复杂:把原始视频解码,然后只保留我们想要的那段时间范围内的数据,再重新编码封装。嗯,这里要注意,我们不是真的去“剪”文件,而是通过控制解码和编码的时间窗口来实现。
22.1.1 裁剪流程设计
先看一张流程图,帮你快速建立整体认知:
流程看起来挺直白的,但实际写代码时坑不少。我曾经在一个项目里,裁剪出来的视频播放时画面卡顿,查了半天才发现是时间戳没有重新对齐。你想想看,解码出来的帧时间戳是原始时间,但裁剪后时间轴变了,如果不重新计算,播放器就懵了。
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适合拼接这种“多进单出”的场景。选对工具,事半功倍。