23、视频裁剪与拼接:FFmpeg裁剪、MediaCodec裁剪、无损拼接、转场效果
视频裁剪和拼接,是音视频开发里最基础也最常用的操作。说白了,就是把一段视频「切掉不要的部分」,或者把几段视频「粘在一起」。听起来简单,但实际做起来坑不少。
我最早接触这个需求,是在做一个短视频编辑SDK的时候。产品经理说:「用户要能裁剪视频,还要能拼接,最好还能加转场。」我当时心想,这不就是调几个API的事吗?结果一做就是两个月……嗯,今天我把这些经验整理出来,希望能帮你少走弯路。
23.1 FFmpeg裁剪:命令行与代码实现
FFmpeg裁剪视频,最常用的就是 -ss 和 -t 参数。-ss 指定起始时间,-t 指定持续时间。比如:
ffmpeg -i input.mp4 -ss 00:01:00 -t 00:00:30 -c copy output.mp4
这条命令的意思是:从第1分钟开始,截取30秒,直接复制编码(不重新编码)。
关键点: -ss 放在 -i 前面还是后面,效果完全不同。
放在 -i 前面,FFmpeg会先跳转到指定时间点再开始解码,速度快但精度差(关键帧对齐)。放在 -i 后面,则是先解码再跳转,精度高但速度慢。我个人习惯是:
- 快速预览:
-ss放前面,配合-c copy - 精确裁剪:
-ss放后面,配合重新编码
在代码里调用FFmpeg,我一般用 MediaMetadataRetriever 先获取视频时长,再拼接命令行参数。举个例子:
String[] cmd = {
"ffmpeg",
"-ss", startTime,
"-i", inputPath,
"-t", duration,
"-c", "copy",
"-avoid_negative_ts", "1",
outputPath
};
这里 -avoid_negative_ts 1 是我踩坑踩出来的。不加这个参数,裁剪后的视频开头可能会有几帧黑屏或花屏。为什么?因为关键帧的PTS可能为负值,FFmpeg默认会保留这些负值时间戳,导致播放器解析异常。
小技巧: 如果你需要裁剪后保持原视频的旋转信息,记得加上 -metadata:s:v rotate="0",否则有些播放器会转错方向。
23.2 MediaCodec裁剪:Android原生方案
FFmpeg虽然强大,但在Android上有个硬伤——它走的是软编软解,性能差、发热大。如果你的App需要实时裁剪(比如用户拖动进度条预览),那必须用MediaCodec。
MediaCodec裁剪的核心思路是:用 MediaExtractor 读取指定时间范围的帧,然后用 MediaMuxer 写入新文件。听起来简单,但实现起来有几个关键点:
- 时间戳对齐:裁剪起始时间必须对齐到关键帧
- 音视频同步:音频和视频的裁剪区间要一致
- 编码格式:输出格式要和输入保持一致,否则需要转码
我曾经在做一个视频剪辑App时,遇到一个诡异的问题:裁剪后的视频,前几秒有声音没画面。排查了半天,发现是 MediaExtractor 的 seekTo() 方法默认只seek到关键帧,而音频没有关键帧的概念,所以音频从指定时间开始,视频却从上一个关键帧开始,导致音画不同步。
解决方案是:手动计算视频的关键帧位置,让音频也从这个位置开始。代码大概长这样:
// 视频轨道
int videoTrackIndex = extractor.getTrackIndex(MediaFormat.MIME_TYPE_VIDEO);
extractor.selectTrack(videoTrackIndex);
extractor.seekTo(startTimeUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
// 音频轨道
int audioTrackIndex = extractor.getTrackIndex(MediaFormat.MIME_TYPE_AUDIO);
extractor.selectTrack(audioTrackIndex);
// 注意:音频不需要seek,直接从视频的关键帧位置开始读取
// 但需要丢弃关键帧之前的音频数据
注意: MediaCodec裁剪不支持 -c copy 这种无损操作。因为MediaCodec的编码器输出的是压缩数据,你无法直接「复制」原始码流。所以MediaCodec裁剪本质上是「解码-裁剪-编码」的过程,会有画质损失。
23.3 无损拼接:MP4格式的特殊处理
无损拼接,指的是不重新编码,直接把两段视频的码流「接」在一起。听起来很美好,但MP4格式有个问题——它的 moov box(元数据)在文件末尾,拼接时需要重新计算。
FFmpeg提供了 concat 协议来实现无损拼接:
ffmpeg -i "concat:input1.mp4|input2.mp4" -c copy output.mp4
但这个方法有个限制:所有输入视频的编码参数必须完全一致。包括分辨率、帧率、编码器、码率、甚至GOP大小。只要有一个参数不同,拼接出来的视频就会出问题。
我在项目中遇到过更复杂的情况——需要拼接不同分辨率的视频。这时候就不能用 concat 协议了,得用 concat 滤镜:
ffmpeg -i input1.mp4 -i input2.mp4 \
-filter_complex "[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[outv][outa]" \
-map "[outv]" -map "[outa]" output.mp4
这个命令会重新编码,所以不是无损的。但好处是它可以处理不同参数的视频,甚至可以在拼接时做缩放、裁剪等处理。
核心原则: 无损拼接的前提是「同源同参」。如果视频来源不同,老老实实重新编码。
23.4 转场效果:从简单到复杂
转场效果,说白了就是两段视频之间的过渡动画。最简单的转场是「硬切」——直接切换。稍微复杂一点的有「淡入淡出」、「滑动」、「缩放」等。
FFmpeg的 xfade 滤镜支持多种转场效果:
ffmpeg -i input1.mp4 -i input2.mp4 \
-filter_complex "xfade=transition=fade:duration=1:offset=4" \
output.mp4
这里 transition=fade 表示淡入淡出效果,duration=1 是转场时长1秒,offset=4 是转场开始时间(第4秒开始过渡)。
FFmpeg内置了上百种转场效果,常用的有:
| 效果名称 | 说明 | 适用场景 |
|---|---|---|
| fade | 淡入淡出 | 通用,最安全 |
| slideleft | 向左滑动 | 图片切换 |
| circleopen | 圆形展开 | 创意效果 |
| pixelize | 像素化 | 复古风格 |
但说实话,FFmpeg的转场效果在移动端性能堪忧。我试过在骁龙865上跑 circleopen 效果,1080p视频直接掉到15帧。所以如果你要做移动端App,建议用OpenGL ES自己实现转场shader,性能会好很多。
我的经验: 转场效果不要超过1秒。用户看视频时,转场时间太长会打断观看节奏。0.5秒的淡入淡出是最舒服的。
23.5 知识体系总览
下面这张图总结了视频裁剪与拼接的核心技术路径:
这张图把裁剪、拼接、转场三条技术路线分开了。你实际开发时,可以根据场景选择不同的组合。比如做一个短视频编辑App,裁剪用MediaCodec保证实时性,拼接用FFmpeg保证质量,转场用OpenGL ES保证流畅度。
好了,这一章的内容就到这里。裁剪和拼接看似基础,但里面涉及的时间戳对齐、参数一致性、性能优化等问题,都是实战中容易踩坑的地方。希望这些经验能帮你少走弯路。
公众号:蓝海资料掘金营,微信deep3321