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 写入新文件。听起来简单,但实现起来有几个关键点:

  1. 时间戳对齐:裁剪起始时间必须对齐到关键帧
  2. 音视频同步:音频和视频的裁剪区间要一致
  3. 编码格式:输出格式要和输入保持一致,否则需要转码

我曾经在做一个视频剪辑App时,遇到一个诡异的问题:裁剪后的视频,前几秒有声音没画面。排查了半天,发现是 MediaExtractorseekTo() 方法默认只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 知识体系总览

下面这张图总结了视频裁剪与拼接的核心技术路径:

视频裁剪与拼接技术体系 视频裁剪 FFmpeg裁剪 MediaCodec裁剪 关键帧对齐 视频拼接 concat协议(无损) concat滤镜(重编码) 参数一致性检查 转场效果 xfade滤镜 OpenGL ES实现 性能优化 选择建议 • 离线处理 → FFmpeg(功能全,性能要求低) • 实时处理 → MediaCodec + OpenGL ES(性能优先)

这张图把裁剪、拼接、转场三条技术路线分开了。你实际开发时,可以根据场景选择不同的组合。比如做一个短视频编辑App,裁剪用MediaCodec保证实时性,拼接用FFmpeg保证质量,转场用OpenGL ES保证流畅度。

好了,这一章的内容就到这里。裁剪和拼接看似基础,但里面涉及的时间戳对齐、参数一致性、性能优化等问题,都是实战中容易踩坑的地方。希望这些经验能帮你少走弯路。


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