FFmpeg 视频裁剪 av_seek_frame 与 avformat_seek_file 两种定位方式全解析
本文在「解复用 → 解码 → 编码 → 复用」标准链路的基础上,重点讲解如何截取原片的某一段(如 10~20 秒)。代码以 C++ 调用 FFmpeg(
libavformat/libavcodec/libswscale)实现,视频重编码、音频透传。与常见“复用流程”文章最大的不同:本文用整整两章拆解裁剪的起点——关键帧定位,分别讲透
av_seek_frame和avformat_seek_file的函数原型、参数语义、实现要点,并给出两套已跑通、仅定位方式不同的完整代码与流程图,让你既能“会用”,也知道“内部怎么选关键帧”。
目录
- 第零章 前置知识:先搞懂 5 个核心概念
- 第一章 为什么裁剪必须先“定位关键帧”
- 第二章 av_seek_frame 用法与实现
- 第三章 avformat_seek_file 用法与实现
- 第四章 两者对比
- 第五章 整体处理流程图 + 跟着一帧走一遍
- 第六章 视频裁剪的 6 个核心知识点
- 第七章 完整案例一:av_seek_frame 版
- 第八章 完整案例二:avformat_seek_file 版
- 第九章 两版差异点汇总
- 第十章 编译与运行
- 第十一章 进阶
- 第十二章 常见坑与排错(FAQ)
- 第十三章 小结 + 术语表
第零章 前置知识:先搞懂 5 个核心概念
这一章不写代码,只把后面反复用到、又最容易让人懵的 5 个概念讲清楚。建议先读到“能用自己的话复述”再往下。
0.1 一条视频文件里到底有什么
一个 .mp4 文件不是“一整段画面”,而是容器(container)里装了若干条“流(stream)”,常见的流有:
- 视频流:一连串压缩后的画面(H.264/H.265…)。
- 音频流:一连串压缩后的声音(AAC/MP3…)。
- 其它:字幕、章节等(本文忽略)。
FFmpeg 处理时,数据有两种形态在流水线里流动:
| 形态 | 类型 | 是什么 | 在哪一步出现 |
|---|---|---|---|
| 包(Packet) | AVPacket | 压缩后的一小段数据(一个压缩帧 / 一段音频),带时间戳 | 解复用读出、复用写入 |
| 帧(Frame) | AVFrame | 解码后的裸数据(一帧 YUV 画面 / 一段 PCM 采样),不带压缩 | 解码后、编码前 |
一句话:包是“压缩的、在文件里传输的”;帧是“解开的、能直接处理像素的”。 解复用产出包,解码把包变成帧,编码把帧变回包,复用到文件。
0.2 处理的四步走(流水线)
任何“转码 / 裁剪 / 加水印”本质都是这条线:
解复用 解码 处理 编码 复用
文件 ──► [读包] ──► [包→帧] ──► [改像素/丢帧/改时间戳] ──► [帧→包] ──► [写包] ──► 新文件
demux decode (我们加逻辑) encode mux
- 解复用(demux):把容器拆开,按流吐出
AVPacket。对应avformat_open_input/av_read_frame。 - 解码(decode):把压缩包解成裸
AVFrame。对应avcodec_send_packet/avcodec_receive_frame。 - (处理):本文在这里插入“裁剪”——丢区间外的帧、强制首帧为关键帧、改时间戳。
- 编码(encode):把裸帧重新压成
AVPacket。对应avcodec_send_frame/avcodec_receive_packet。 - 复用(mux):把包按规矩写进新容器。对应
avformat_write_header/av_interleaved_write_frame/av_write_trailer。
音频在本方案里不走解码/编码,直接把解复用读出的包原样写进新文件(叫“透传 / copy”),所以音频那条线在“处理”处只是“判断时间戳后直接搬运”,没有帧形态。
0.3 time_base 与 pts/dts:FFmpeg 的“尺子”和“刻度”
这是初学者第一道坎,也是全文最关键的背景。先把直觉建立起来:
- 真实时间(秒)是连续的:
10.0 秒、10.033 秒…… - 但计算机里时间戳只能存整数。time_base(时间基)就是一把“尺子的刻度单位”:它告诉 FFmpeg “1 个刻度 = 多少秒”。
- 例如
time_base = 1/30000,表示 1 个刻度 = 1/30000 秒。 - 于是
真实秒数 = 刻度值 × time_base。反过来刻度值 = 真实秒数 ÷ time_base = 真实秒数 × 30000。
- 例如
- pts(Presentation TimeStamp,显示时间戳):这帧/这包“应该在第几秒被看到”,存的是刻度值(整数)。
- dts(Decode TimeStamp,解码时间戳):这帧/这包“应该在第几秒被解码”,存的是刻度值。
生活比喻:time_base 就像“米”还是“厘米”。同一段 10 米的距离,用“米”记是
10,用“厘米”记是1000。FFmpeg 里不同流用的“尺子”不一样(视频可能是 1/30000,音频可能是 1/48000,编码器可能是 1/30),所以同一个“10 秒”,在不同流里的 pts 整数值完全不同。这正是后面所有换算的根源。
不同流 time_base 不同的具体例子(本文用的素材):
| 流 | time_base | “10 秒”对应的 pts(刻度值) |
|---|---|---|
| 输入视频流 | 1/30000(30k tbn) | 10 × 1e6 × (1/30000) = 300000 |
| 输入音频流 | 1/48000 | 10 × 1e6 × (1/48000) = 480000 |
| 编码器(输出) | 1/30(帧率倒数) | 10 ÷ (1/30) = 300 |
注意:换算时通常先转成微秒(
AV_TIME_BASE = 1000000)这个“中间单位”,再换到目标 time_base,官方函数av_rescale_q就是干这个的。记住一句:“秒 → 任意 time_base” =av_rescale_q(秒 × AV_TIME_BASE, AV_TIME_BASE_Q, 目标_time_base)。
pts 与 dts 为什么是两个?因为有 B 帧(见 0.4),解码顺序和显示顺序不一致,dts 管“先解哪个”,pts 管“先放哪个”。
0.4 GOP 与 I/P/B 帧:为什么不能随便从中间开始解码
视频压缩为了省体积,不会每帧都存完整画面:
- I 帧(关键帧 / Intra):自带完整画面,像一张 JPEG,不依赖任何其它帧就能解码。
- P 帧(Predictive):只存“和前面某帧的差值”,解码时必须先有参考帧。
- B 帧(Bi-directional):存“前后两帧的差值”,解码时要前后都备齐(所以解码顺序 ≠ 显示顺序)。
一组“从 I 帧开始、到下一个 I 帧之前”的画面叫一个 GOP(Group Of Pictures)。解码器必须从 GOP 开头的 I 帧起步;如果从中间的 P/B 帧开始,它会引用一个根本不存在的参考帧 → 画面花掉(绿块/马赛克)。
一句话:关键帧(I 帧)是唯一能“独立站稳”的起点。 这正是“裁剪必须先定位关键帧”的根本原因——下一章展开。
0.5 本文到底要做什么(先看结论)
把上面四点连起来,我们要做的事就清晰了:
- 素材是“一段 0~N 秒的完整视频”。我们要它的 10~20 秒。
- 但视频不能从 15 秒直接切——15 秒处多半是 P/B 帧,切了放不出来。所以先把读取位置移到 10 秒之前最近的关键帧(I 帧),这一步就是
av_seek_frame/avformat_seek_file的活。 - 从那个 I 帧开始解码,10 秒之前的帧解出后立刻丢掉(但仍要解,为了给后面的帧攒参考帧)。
- 真正落在 10~20 秒的帧,重新编码、写进新文件;写的时候把时间戳“减去起点”,让新片段从 0 秒起播。
- 音频不解码,按时间戳直接挑 10~20 秒的包搬运。
带着这个骨架往下读,每一章都是在回答“这一步具体怎么写、为什么非这样不可”。
第一章 为什么裁剪必须先“定位关键帧”
(读完第零章,这一章就很好懂了。这里把“定位关键帧”这件事从动机到解法串一遍。)
H.264/H.265 等编码存在 I / P / B 帧(复习 0.4):
- I 帧(关键帧):独立完整,不依赖其他帧即可解码。
- P / B 帧:需要引用前后帧才能解码。
因此解码必须从一张完整画面(关键帧)起步。如果直接跳到 10 秒处的 P 帧,它会引用一个不存在的参考 → 花屏/绿块。
直觉验证:想象一本书,第 1 页是完整故事开头,后面每页都写“接上页”。你直接翻到第 50 页开始读,必然读不懂——除非第 50 页本身就是一个“重新开始讲”的章节开头。关键帧就是那种“章节开头页”。
解法:把读取位置(AVFormatContext 的内部文件偏移)前移到一个关键帧,再开始解码。负责这件事的就是本文的主角:
av_seek_frame:经典接口,给一个“目标时间戳 + 方向标志”。avformat_seek_file:更强大的接口(前者的超集),额外允许给一个“时间窗口”。
提示:音频是透传(不解码不编码,AAC 每包独立可解),它不需要“定位关键帧”,包层按
pkt->pts直接丢区间外的包即可。下面的 seek 只针对视频流。
1.1 一张图看懂:关键帧到底落在哪
解码时间轴(示例:关键帧 I 约每 3 秒一个,30fps,start = 10s):
0s 3s 6s 9s 10s(start) 12s 15s 18s 20s(end)
|---------|----------|----------|----------|-----------|----------|----------|----------|
I I I I | I I I I
▲ ▲ ▲
│ │ │
av_seek_frame avformat_seek_file ⚠ 危险:若把窗口上界
(BACKWARD) (窗口上界=start) max_ts 设成 start+2s,
落点 = 9s 处 I 落点 = 9s 处 I demuxer 可能挑 12s 处 I
│ │ │
└── 解出 9~10s 的帧 └── 同样落 9s 或恰为 └── 10~12s 内容根本没
后立刻丢弃 10s 的 I(更近) 被读进来 → 开头被吃!
要点:
av_seek_frame(BACKWARD)与avformat_seek_file(窗口上界=start)在裁剪场景下落点几乎一致(都落在 ≤ start 的最近 I 帧)。- 只有把窗口上界推过 start,demuxer 才可能选 start 之后的 I 帧——但代价是静默丢掉
start ~ 该I帧这段内容(见第三章陷阱说明)。 - 落点 I 帧到 start 之间的帧仍会被解码、
frame->pts判定 < start 后丢弃——这就是 6.3 说的"必须先解再丢"。
第二章 av_seek_frame 用法与实现
2.1 先建立直觉:这个函数是干嘛的
av_seek_frame 解决一个问题:“告诉 FFmpeg‘请把读取位置挪到某个时间点附近’,并约定‘落点允许在前还是在后’”。它不直接解码,只是移动 AVFormatContext 内部的“文件指针”,之后 av_read_frame 读出来的第一个包就位于落点附近。
2.2 函数原型
/**
* Seek to the keyframe at timestamp.
* @param s 解复用上下文
* @param stream_index 参考流索引(-1 表示用 AV_TIME_BASE 时间单位)
* @param timestamp 目标时间戳,单位 = stream_index 对应流的 time_base
* @param flags 方向/模式标志(见下)
* @return >=0 成功;<0 失败
*/
int av_seek_frame(AVFormatContext *s, int stream_index,
int64_t timestamp, int flags);
2.3 参数详解(逐个拆开讲)
| 参数 | 说明 | 新手注意 |
|---|---|---|
s | 解复用上下文 demux_ctx。就是 avformat_open_input 打开的那个。 | 一定是已打开、且 avformat_find_stream_info 过的。 |
stream_index | 用哪条流的时间基来解读 timestamp。裁剪视频一般用 in_video_index。传 -1 时 timestamp 按 AV_TIME_BASE(微秒)解释。 | 时间戳单位由这条流决定——这是 time_base 知识(0.3)的落地。 |
timestamp | 想跳到的目标时间戳,单位是 streams[stream_index]->time_base。例如 30k tbn 的视频,10 秒 = 300000。 | 不是“秒”!是“刻度值”。用 0.3 的 av_rescale_q 算出来。 |
flags | 控制“落点怎么选”,见下。 | 裁剪几乎只用 AVSEEK_FLAG_BACKWARD。 |
关键标志位:
| 标志 | 值 | 含义 | 裁剪能用吗 |
|---|---|---|---|
AVSEEK_FLAG_BACKWARD | 1 | 向后找:落点时间戳 ≤ 请求值,且尽量靠近(即落到 ≤ start 的最近关键帧)。 | ✅ 最常用、最安全 |
AVSEEK_FLAG_BYTE | 2 | timestamp 按字节偏移解释(而非时间),用于基于文件位置的 seek。 | ❌ 裁剪不用 |
AVSEEK_FLAG_ANY | 4 | 允许落到任意帧(含非关键帧)。 | ❌ 千万别用,否则起点不是 I 帧 → 花屏 |
AVSEEK_FLAG_FRAME | 8 | timestamp 按帧号解释。 | ❌ 裁剪不用 |
一句话:
BACKWARD= “往回找最近的关键帧”;其它三个要么用错单位、要么落到非关键帧,裁剪场景都不要。
2.4 实现要点(内部在做什么)
- 它内部会调用 demuxer 的
read_seek/read_timestamp,在索引里找到满足方向约束的关键帧作为落点。 AVSEEK_FLAG_BACKWARD保证“落点一定在 start 之前或恰好等于”,所以之后所有包都必须先送进解码器(维持参考帧),即使 10 秒之前的帧解出后也会被丢弃——这正是后文“视频起止判断必须在解码后做”的原因。- seek 之后必须
avcodec_flush_buffers(ctx):清掉解码器里缓存的旧参考帧,否则残留的参考帧会和新的 GOP 串味 → 花屏。
2.5 用法示例(代码片段)
// 把“秒”换算成输入视频流自己的时间戳
start_video_ts = av_rescale_q(start_sec * AV_TIME_BASE, AV_TIME_BASE_Q,
demux_ctx->streams[in_video_index]->time_base);
// 向后定位到 <= start 的最近关键帧(I 帧)
re = av_seek_frame(demux_ctx, in_video_index, start_video_ts, AVSEEK_FLAG_BACKWARD);
if (0 > re) {
std::cout << "av_seek_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
avcodec_flush_buffers(decodec_ctx); // 清掉旧参考帧,避免花屏
读代码顺序:
start_video_ts是“10 秒在输入视频流 time_base 下的刻度值”→ 告诉 FFmpeg“往回找这个刻度附近的关键帧”→ 成功就清参考帧。三步一个都不能少。
第三章 avformat_seek_file 用法与实现
3.1 直觉:比 av_seek_frame 多了“一个范围”
av_seek_frame 只能说“我想去时间点 X,往回找”。avformat_seek_file 更进一步:“我想去 X,但允许在 [min, max] 这个范围内挑一个最合适的关键帧”。多了 min_ts / max_ts 两个参数,定位更灵活。
3.2 函数原型
/**
* Seek to timestamp ts; with a window [min_ts, max_ts].
* @param s 解复用上下文
* @param stream_index 参考流索引(-1 表示用 AV_TIME_BASE)
* @param min_ts 时间窗口下界,单位 = 该流 time_base
* @param ts 目标时间戳(首选落点),单位 = 该流 time_base
* @param max_ts 时间窗口上界,单位 = 该流 time_base
* @param flags 同 av_seek_frame 的方向/模式标志
* @return >=0 成功;<0 失败
*/
int avformat_seek_file(AVFormatContext *s, int stream_index,
int64_t min_ts, int64_t ts, int64_t max_ts,
int flags);
3.3 参数详解(重点是“时间窗口”)
与 av_seek_frame 相比,它多了 min_ts / max_ts 两个参数,三者都基于同一流的 time_base:
ts:你最想落到的目标时间戳(等价于av_seek_frame的timestamp)。min_ts/max_ts:允许的落点区间。demuxer 会在这个窗口内挑选一个“尽量靠近ts的关键帧”作为实际落点——而不是死板地一定 ≤ ts。
典型组合:
| 想要的效果 | min_ts | ts | max_ts | flags |
|---|---|---|---|---|
等价于 av_seek_frame(BACKWARD) | INT64_MIN | start | start | AVSEEK_FLAG_BACKWARD |
| 允许 start 之前任意远、最多晚 2 秒 | INT64_MIN | start | start + 2s | 0 |
| 严格落在 [start, start+1s] 内 | start | start | start + 1s | 0 |
⚠ 裁剪场景的窗口陷阱(重点):上表第二行把
max_ts = start + 2s当作"更灵活"的示范,但对"截取"是危险的。
avformat_seek_file会在[min_ts, max_ts]内挑一个尽量靠近ts的关键帧。若窗口越过 start,demuxer 可能挑中start之后的那个 I 帧(如图 1.1 的 12s 处),结果start ~ 该I帧这段内容根本没被读进解码器 → 截出来的片段开头被静默吃掉,输出从 ~12s 而非 10s 起。
结论:做"截取 10~20s"时,窗口上界应等于start(或干脆用AVSEEK_FLAG_BACKWARD),让落点永远 ≤ start;"允许晚于 start"只适合"定位后整段播放/转码"这类不care丢头的场景。
3.4 它和 av_seek_frame 的关系
avformat_seek_file 是 av_seek_frame 的超集:实际上 av_seek_frame(s, idx, ts, flags) 在内部就是退化成 avformat_seek_file(s, idx, INT64_MIN, ts, ts, flags) 来调用的。所以:
- 想用
BACKWARD语义?把窗口退化成“点”min_ts = max_ts = ts并传AVSEEK_FLAG_BACKWARD即可,完全等价。 - 想更灵活(比如允许落在 start 略微之后、定位更准、少解几帧)?就敞开
max_ts用时间窗口。
3.5 实现要点
- 落点的最终选择权交给 demuxer 的具体实现(
read_seek2)。它会在[min_ts, max_ts]内权衡“距离 ts 的远近”与“是否为关键帧”,挑一个最合适的位置。 - 同样,seek 后必须
avcodec_flush_buffers。 - 因为落点可能比
start略微靠后(当窗口允许时),所以“10 秒之前的帧仍要解码以维持参考帧”那段逻辑依然成立——起止判断照常在解码后做。
3.6 用法示例(代码片段)
// 单位约定:min_ts / ts / max_ts 都基于输入视频流 time_base
int64_t seek_min_ts = INT64_MIN; // 不限制下限:允许落在 start 之前的最近关键帧
int64_t seek_max_ts = start_video_ts +
av_rescale_q(2 * AV_TIME_BASE, AV_TIME_BASE_Q,
demux_ctx->streams[in_video_index]->time_base); // 允许比 start 晚至多 2 秒
// flags=0:不强制 BACKWARD/ANY,由 demuxer 在窗口内自行选最合适的关键帧
re = avformat_seek_file(demux_ctx, in_video_index, seek_min_ts, start_video_ts, seek_max_ts, 0);
// —— 想要与 av_seek_frame(..., AVSEEK_FLAG_BACKWARD) 完全等价?把窗口退化成“点”即可:
// avformat_seek_file(demux_ctx, in_video_index, INT64_MIN, start_video_ts, start_video_ts, AVSEEK_FLAG_BACKWARD);
if (0 > re) {
std::cout << "avformat_seek_file failed!" << std::endl;
PrintErr(re);
goto Failed;
}
avcodec_flush_buffers(decodec_ctx); // 清掉旧参考帧,避免花屏
第四章 两者对比
| 维度 | av_seek_frame | avformat_seek_file |
|---|---|---|
| 定位能力 | 单点 + 方向标志(BACKWARD/ANY/BYTE/FRAME) | 单点 + 时间窗口 [min_ts, max_ts],能力覆盖前者 |
| 能否落在 start 之后 | 不能(BACKWARD 强制 ≤ start) | 能(敞开 max_ts 即可,定位通常更准、少解几帧) |
| 本质 | avformat_seek_file 的退化封装 | 更底层的超集接口 |
| 适用场景 | 简单裁剪、喜欢“一定从 start 之前起步”的确定性 | 想要更精细控制落点、减少多余解码、追求定位精度 |
| 关系 | av_seek_file(s,i,ts,fl) == avformat_seek_file(s,i,INT64_MIN,ts,ts,fl) | 是前者的一般化 |
第五章 整体处理流程图 + 跟着一帧走一遍
5.1 流程图
对应文字版(裁剪 = 复用流程 + 5 处专属逻辑):
解复用 ──► ① 定位到起始关键帧(seek)
│
▼
解码 ──► ② 解码后按 frame->pts 丢弃区间外的帧(维持参考帧)
│
▼
缩放 ──► ③ 强制输出首帧为 I 帧
│
▼
编码 ──► ④ 写包前把时间戳归零(减去起始偏移)
│
▼
复用 ──► ⑤ 冲刷阶段同样做边界保护
其中 ②④ 是裁剪正确性的关键,①④ 是“播放器从 0 秒起播”的关键。
5.2 跟着“第一帧被保留的帧”走一遍(数字例子)
光看流程图还是抽象,下面把第一个真正写进新文件的视频帧从头到尾算一遍。假设:
- 素材:30fps,输入视频流
time_base = 1/30000,GOP 每 3 秒一个 I 帧(I 在 9.0s、12.0s…)。 - 截取区间
start=10s, end=20s。start_video_ts = 300000(见 0.3 算法)。 - 编码器
time_base = 1/30。
第 1 步:定位。 av_seek_frame(BACKWARD, 300000) 落点 = 9.0s 的 I 帧(≤10s 最近关键帧)。flush_buffers 清参考帧。
第 2 步:解码 9.0s~10.0s 的帧(维持参考帧,但丢弃)。
比如 9.033s、9.067s…这些 frame->pts < 300000,走 6.3 的“< start 丢弃”分支:av_frame_unref 后 continue——解了但不写。
第 3 步:遇到 10.033s 的帧(显示顺序第 301 帧)。
- 它的
frame->pts(输入 tb 1/30000)≈10.033 × 30000 = 301000>300000→ 进入“保留”分支。 sws_scale做像素格式转换(YUV 内部格式 → 编码器要的 YUV420P)。sws_frame->pts = av_rescale_q(301000, 1/30000, 1/30) = 301000 ÷ 1000 = 301(编码器 tb 下的刻度值)。- 因为
first_frame==true,设pict_type = AV_PICTURE_TYPE_I(强制首帧为关键帧),first_frame=false。
第 4 步:编码 + 归零写盘。
- 编码器吐出包,
pkt->pts(编码器 tb 1/30)=301。 rescale_packet_ts(..., start_video_enc_ts=300):先减偏移301 - 300 = 1,再换到 mux tb(也是 1/30)→1。av_interleaved_write_frame写入。
结果:新片段里这帧的显示时间戳 = 1(编码器 tb 1/30 ≈ 0.033 秒)。新文件从 0 秒附近起播,第一段就是 10.033s 的画面——完美接上开头。
这个例子同时印证了两件事:(1) 起点之前那些帧“必须解但不写”,是为了让 10.033s 这帧有正确参考;(2) 归零(减 300)让新片段时间轴从 0 开始,而不是从 10 秒开始。把这套数字在脑子里跑通,后面 6.1~6.5 就好懂了。
第六章 视频裁剪的 6 个核心知识点
每个点按「一句话记住 → 是什么 → 为什么 → 怎么做」展开。
6.1 time_base 与时间戳:秒 → pts 的换算
一句话记住:FFmpeg 里所有时间戳都是「数值 × time_base = 真实秒数」,不同流尺子不同,换尺子用 av_rescale_q。
FFmpeg 里所有时间戳都是「数值 × time_base = 真实秒数」。不同流时间基不同:
- 输入视频流:
1/30000(30k tbn)→10 秒的 pts =10 × 1e6 × (1/30000) = 300000 - 输入音频流:
1/48000→10 秒的 pts =480000 - 编码器(输出):
1/30(由帧率倒数得到)
换算统一用 av_rescale_q:
start_video_ts = av_rescale_q(start_sec * AV_TIME_BASE, AV_TIME_BASE_Q,
demux_ctx->streams[in_video_index]->time_base);
AV_TIME_BASE = 1000000(微秒),AV_TIME_BASE_Q = {1, 1000000}。先转微秒再换算到目标 time_base,是官方推荐的「秒 → 任意 time_base」写法。不懂回看 0.3。
6.2 关键帧(I 帧)与 GOP:为什么必须 seek 到关键帧
一句话记住:解码器只能从 I 帧起步,所以先把读取位置挪到 start 之前最近 I 帧。
见第一章与 0.4。代码上:av_seek_frame(BACKWARD) 或 avformat_seek_file(窗口) 落到最近关键帧,再 avcodec_flush_buffers。
6.3 起止判断:为什么视频在「解码后」、音频在「包层」
一句话记住:视频帧有前后依赖 + 显示/解码顺序不一致 → 必须解出帧、用 frame->pts 判;音频每包独立 → 用 pkt->pts 直接判。
视频必须在解码后、按 frame->pts 判断:
if (AV_NOPTS_VALUE == frame->pts || start_video_ts > frame->pts) {
av_frame_unref(frame);
continue; // 跳过(但仍要先解码,维持参考帧)
}
if (end_video_ts < frame->pts) {
video_done = true;
av_frame_unref(frame);
break;
}
原因:
-
帧间依赖:start 之前的关键帧到 start 之间的帧,必须送进解码器累积参考帧,解出后立刻丢弃——否则 start 那帧无法正确解码(见 5.2 第 2 步)。
-
显示顺序 vs 解码顺序:有 B 帧时,压缩包的「解码顺序」和「显示顺序」不一致。只有解码出来的
frame->pts(显示时间戳)才准确,不能用pkt->pts。用一张图说明为什么必须用
frame->pts(B 帧是"双向预测",解码时要先拿到后面的参考帧):文件里的包顺序(解码顺序): I₀ P₁ B₂ P₃ B₄ P₅ B₆ ... │ │ ↑ │ ↑ │ ↑ │ └───┴───┘ └───┴───┘ (B 帧依赖前后 P/I) ▼ 真正播放的顺序(显示顺序): I₀ B₂ P₁ B₄ P₃ B₆ P₅ ... 0 1 2 3 4 5 6 ← frame->pts ↑ ↑ pkt->pts 可能=2 pkt->pts 可能=4 (解码时 B₂ 是第 3 个包) (解码时 P₃ 是第 5 个包) → 若拿 pkt->pts 判断"是否进入 10~20s 区间",会和真实播放位置错开(B 帧尤其明显) → 解出帧后必须用 frame->pts(显示时间戳)判断,才和"肉眼看到的时间"对齐
音频在「包层」即可判断(透传、每包独立可解,不依赖前后帧):
} else if (in_audio_index == pkt->stream_index) {
if (AV_NOPTS_VALUE == pkt->pts || start_audio_ts > pkt->pts) {
av_packet_unref(pkt);
continue; // AAC 每包独立可解,直接丢包
}
...
}
6.4 首帧强制 I 帧:让片段能独立解码
一句话记住:截出来的片段会被当独立文件播放,第一帧必须是关键帧,否则一打开就花。
截出来的片段会被当独立文件播放,第一帧必须是关键帧:
sws_frame->pts = av_rescale_q(frame->pts,
demux_ctx->streams[in_video_index]->time_base,
encodec_ctx->time_base);
av_frame_unref(frame);
if (first_frame) { // 只有真正被保留的第一帧会触发
first_frame = false;
sws_frame->pict_type = AV_PICTURE_TYPE_I; // 给编码器的输入提示
}
re = avcodec_send_frame(encodec_ctx, sws_frame);
必须放在「起止判断之后、送编码器之前」,且 pict_type 作用在 AVFrame 上、编码之前。(为什么是“之后”?因为要等通过了起止判断、确定这帧会被保留,才轮到它当“第一帧”。)
6.5 输出时间戳归零:start_video_enc_ts 的单位换算(核心坑)
一句话记住:视频包来自编码器,它的 pts 单位是“编码器 time_base”,和输入的 start 单位差 1000 倍,必须先 av_rescale_q 换算偏移再减,否则 pts 变负 → 播放器错乱。
我们希望输出片段从 0 秒起播。视频包来自编码器,pts 单位是编码器 time_base(1/30);而 start_video_ts 单位是输入视频流 time_base(1/30000),数量级差 1000 倍:
start_video_ts = 300000 (1/30000)
start_video_enc_ts = 300 (1/30) ← 必须先换算!
不换算直接减 → 负数 pts → 播放器时间轴错乱(如“从 11 秒起”、Win10 丢视频轨)。正确做法:先把偏移换算到编码器 time_base,写包时统一减去:
start_video_enc_ts = av_rescale_q(start_video_ts,
demux_ctx->streams[in_video_index]->time_base,
encodec_ctx->time_base);
rescale_packet_ts(pkt, encodec_ctx->time_base,
mux_ctx->streams[out_video_index]->time_base, start_video_enc_ts);
rescale_packet_ts 负责“先减 offset、再换 time_base”:
void rescale_packet_ts(AVPacket *&pkt, AVRational cur_time_base,
AVRational target_time_base, int64_t offset = 0) {
pkt->pts = av_rescale_q_rnd(pkt->pts - offset, cur_time_base, target_time_base,
(AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt->dts = av_rescale_q_rnd(pkt->dts - offset, cur_time_base, target_time_base,
(AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt->duration = av_rescale_q(pkt->duration, cur_time_base, target_time_base);
pkt->pos = -1;
}
音频透传分支单位一致(包来自解复用器,单位即输入音频流 time_base),直接用
start_audio_ts当 offset 即可,不踩这个坑。
6.6 冲刷阶段的边界保护(B 帧尾部溢出)
一句话记住:B 帧让显示顺序 ≠ 解码顺序,冲刷逼出的尾帧可能还 ≥ end,必须再判一次丢掉。
视频流有 B 帧时,解码顺序与显示顺序不一致,冲刷解码器逼出的尾帧可能 pts 仍 ≥ end,必须丢弃:
if (end_video_ts < frame->pts) {
av_frame_unref(frame);
break; // 边界保护:超出 end 的尾帧不再写出
}
编码器/解码器冲刷段同样走“收包 → 归零 → 写盘”,偏移一致用 start_video_enc_ts。
第七章 完整案例一:av_seek_frame 版
代码导读:下面 300 行是完整可运行程序。建议这样读:
main开头一堆变量 = “裁剪参数”(start/end、各流时间戳、起止标志)。/// 解复用 / 解码 / 编码 / 复用 / 像素格式转换五段 = 0.2 流水线的“搭环境”。- 真正裁剪专属逻辑只有 5 处(对应第 5.1 文字版 ①②③④⑤):定位段(本例
av_seek_frame)、起止判断、首帧 I、归零写包、冲刷边界。其余都是标准复用流程。- 本例定位段用
av_seek_frame(demux_ctx, in_video_index, start_video_ts, AVSEEK_FLAG_BACKWARD)。
#include <iostream>
#include <string>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}
// 打印错误信息
void PrintErr(int err) {
char buf[1024] = { 0 };
av_strerror(err, buf, sizeof(buf));
std::cout << buf << std::endl;
}
// 把包的 pts/dts/duration 从旧 time_base 重算到新 time_base
// 注意 offset 的单位必须和 cur_time_base 一致:
void rescale_packet_ts(AVPacket *&pkt, AVRational cur_time_base, AVRational target_time_base, int64_t offset = 0) {
pkt->pts = av_rescale_q_rnd(pkt->pts - offset, cur_time_base,target_time_base,
(AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt->dts = av_rescale_q_rnd(pkt->dts - offset, cur_time_base,target_time_base,
(AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt->duration = av_rescale_q(pkt->duration, cur_time_base, target_time_base);
pkt->pos = -1;
}
int main(void) {
// 解复用
std::string in_file_path = "广西之旅.mp4";
AVFormatContext *demux_ctx = NULL;
int in_video_index = -1;
int in_audio_index = -1;
// 解码
const AVCodec *decodec = NULL;
AVCodecContext *decodec_ctx = NULL;
// 编码
const AVCodec *encodec = NULL;
AVCodecContext *encodec_ctx = NULL;
// 复用
std::string out_file_path = "out_file.mp4";
AVFormatContext *mux_ctx = nullptr;
int out_video_index = -1;
int out_audio_index = -1;
// 像素格式转换
SwsContext *sws_ctx = nullptr;
AVFrame *sws_frame = av_frame_alloc();
// 其它
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
int re = -1;
// ===== 裁剪参数 =====
// 下面这组变量描述“截取原片的哪一段”,以及起止位置在各自时间基下的时间戳
// start_sec / end_sec:截取区间(秒),这里写死为 10~20 秒
double start_sec = 10.0;
double end_sec = 20.0;
// 视频起止时间戳:单位是“输入视频流 time_base”
// - 用于 av_seek_frame 定位(落到 <= start 的最近关键帧)
// - 用于解码后按 frame->pts 丢弃区间外的帧
int64_t start_video_ts = 0;
int64_t end_video_ts = 0;
// 音频起止时间戳:单位是“输入音频流 time_base”
// - 音频是透传(不解码),直接按 pkt->pts 丢弃区间外的压缩包
int64_t start_audio_ts = 0;
int64_t end_audio_ts = 0;
// 两路流是否都已截到末尾:都满足后主循环可以提前 break,避免空转
bool video_done = false;
bool audio_done = false;
// 是否为“输出片段的第一帧”:只有第一个通过起止判断、真正被编码的帧会把它置 false
// —— 用来强制该帧编成关键帧(I 帧),让截出来的片段能独立解码
bool first_frame = true;
// 视频起始偏移换算到“编码器 time_base”后的值
// - 待写出的视频包来自编码器,其 pts 单位是编码器 time_base
// - 写包时统一减去它,片段即可从 0 秒起播(否则会“从 11 秒起”)
int64_t start_video_enc_ts = 0;
// ===== seek 相关(本例用 av_seek_frame,未用到下面两个变量)=====
int64_t seek_min_ts = 0;
int64_t seek_max_ts = 0;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// 解复用
// 打开视频文件
re = avformat_open_input(&demux_ctx, in_file_path.c_str(), NULL, NULL);
if (0 != re) {
std::cout << "avformat_open_input failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 查找流信息
re = avformat_find_stream_info(demux_ctx, NULL);
if (0 > re) {
std::cout << "avformat_find_stream_info failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 获得流索引
for (int i = 0; i < demux_ctx->nb_streams; ++i) {
if (AVMEDIA_TYPE_VIDEO == demux_ctx->streams[i]->codecpar->codec_type) {
in_video_index = i;
} else if (AVMEDIA_TYPE_AUDIO == demux_ctx->streams[i]->codecpar->codec_type) {
in_audio_index = i;
} else {
std::cout << "other index: " << i;
}
}
std::cout << "in video index: " << in_video_index << "\nin audio index: " << in_audio_index << std::endl;
av_dump_format(demux_ctx, in_video_index, in_file_path.c_str(), 0);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// 解码
// 查找视频解码器
decodec = avcodec_find_decoder(demux_ctx->streams[in_video_index]->codecpar->codec_id);
if (!decodec) {
std::cout << "avcodec_find_decoder failed!" << std::endl;
goto Failed;
}
// 创建解码器上下文
decodec_ctx = avcodec_alloc_context3(decodec);
if (!decodec_ctx) {
std::cout << "avcodec_alloc_context3 failed!" << std::endl;
goto Failed;
}
// 将流参数复制到解码器上下文
re = avcodec_parameters_to_context(decodec_ctx, demux_ctx->streams[in_video_index]->codecpar);
if (0 > re) {
std::cout << "avcodec_parameters_to_contextv failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 打开解码器
re = avcodec_open2(decodec_ctx, decodec, NULL);
if (0 != re) {
std::cout << "avcodec_open2 failed!" << std::endl;
PrintErr(re);
goto Failed;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// 编码
// 查找视频编码器
encodec = avcodec_find_encoder(demux_ctx->streams[in_video_index]->codecpar->codec_id);
if (!encodec) {
std::cout << "avcodec_find_encoder failed!" << std::endl;
goto Failed;
}
// 创建编码器上下文
encodec_ctx = avcodec_alloc_context3(encodec);
if (!encodec_ctx) {
std::cout << "avcodec_alloc_context3 failed!" << std::endl;
goto Failed;
}
// 设置编码器基础参数
{
encodec_ctx->width = decodec_ctx->width;
encodec_ctx->height = decodec_ctx->height;
// 指定格式为yuv420p
encodec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
// time_base
AVRational framerate = demux_ctx->streams[in_video_index]->avg_frame_rate;
if (0 >= framerate.den || 0 >= framerate.num) {
// 根据参数‘猜测’结果
framerate = av_guess_frame_rate(demux_ctx, demux_ctx->streams[in_video_index], NULL);
}
if (0 >= framerate.den || 0 >= framerate.num) {
framerate = (AVRational){ 25, 1 };
}
encodec_ctx->framerate = framerate;
encodec_ctx->time_base = av_inv_q(framerate);
// 像素宽高比
encodec_ctx->sample_aspect_ratio = decodec_ctx->sample_aspect_ratio;
}
// 打开编码器
re = avcodec_open2(encodec_ctx, encodec, NULL);
if (0 != re) {
std::cout << "avcodec_open2 failed!" << std::endl;
PrintErr(re);
goto Failed;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// 复用
// 创建复用上下文
re = avformat_alloc_output_context2(&mux_ctx, NULL, NULL, out_file_path.c_str());
if (0 > re) {
std::cout << "avformat_alloc_output_context2 failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 创建音视频流
// 创建【视频流】
if (0 <= in_video_index) {
AVStream *stream = avformat_new_stream(mux_ctx, NULL); // 新建视频流
stream->time_base = encodec_ctx->time_base; // 输出流 time_base 先与编码器保持一致
out_video_index = stream->index;
avcodec_parameters_from_context(stream->codecpar, encodec_ctx); // 把编码器参数写进流
}
// 创建【音频流】:走“透传”——不解码不编码,直接复制参数、原样搬运压缩包
if (0 <= in_audio_index) {
AVStream *stream = avformat_new_stream(mux_ctx, NULL);
avcodec_parameters_copy(stream->codecpar, demux_ctx->streams[in_audio_index]->codecpar);
stream->time_base = demux_ctx->streams[in_audio_index]->time_base;
out_audio_index = stream->index;
}
std::cout << "out video index: " << out_video_index << "\nout audio index: " << out_audio_index << std::endl;
// 打开io
re = avio_open2(&mux_ctx->pb, out_file_path.c_str(), AVIO_FLAG_WRITE, NULL, NULL);
if (0 > re) {
std::cout << "avio_open2 failed!" << std::endl;
PrintErr(re);
goto Failed;
}
av_dump_format(mux_ctx, out_video_index, out_file_path.c_str(), 1);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// 像素格式转换
sws_ctx = sws_getContext(decodec_ctx->width, decodec_ctx->height, (AVPixelFormat)decodec_ctx->pix_fmt,
encodec_ctx->width, encodec_ctx->height, (AVPixelFormat)encodec_ctx->pix_fmt,
SWS_BILINEAR, NULL, NULL, NULL);
if (!sws_ctx) {
std::cout << "sws_getContext failed! " << std::endl;
return -1;
}
// 分配内存,用于接收转换后的一帧图像
sws_frame->width = encodec_ctx->width;
sws_frame->height = encodec_ctx->height;
sws_frame->format = encodec_ctx->pix_fmt;
re = av_frame_get_buffer(sws_frame, 32);
if (0 > re) {
std::cout << "av_frame_get_buffer failed! " << std::endl;
PrintErr(re);
goto Failed;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 写入文件头
re = avformat_write_header(mux_ctx, NULL);
if (0 > re) {
std::cout << "avformat_write_header failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// ===== 裁剪:把“秒”换算成各流自己的时间戳 =====
// 真实秒数 × AV_TIME_BASE 得到微秒,再 av_rescale_q 到对应流的 time_base,即该流下的 pts 值
// 例如视频 30k tbn:start_video_ts = 10 * 1e6 * (1/30000) = 300000
// 音频 48k tbn:start_audio_ts = 10 * 1e6 * (1/48000) = 480000
start_video_ts = av_rescale_q(start_sec * AV_TIME_BASE, AV_TIME_BASE_Q,
demux_ctx->streams[in_video_index]->time_base);
end_video_ts = av_rescale_q(end_sec * AV_TIME_BASE, AV_TIME_BASE_Q,
demux_ctx->streams[in_video_index]->time_base);
if (0 <= in_audio_index) {
start_audio_ts = av_rescale_q(start_sec * AV_TIME_BASE, AV_TIME_BASE_Q,
demux_ctx->streams[in_audio_index]->time_base);
end_audio_ts = av_rescale_q(end_sec * AV_TIME_BASE, AV_TIME_BASE_Q,
demux_ctx->streams[in_audio_index]->time_base);
}
// ★ 关键:视频偏移必须换算到“编码器 time_base”
// 解复用器里 start_video_ts 单位是输入视频流 time_base
// 但待写出的视频包来自编码器,其 pts 单位是编码器 time_base
// 两者数量级差 1000 倍,直接减会让 pts 变成负数 —— 必须先 av_rescale_q 换算
start_video_enc_ts = av_rescale_q(start_video_ts, demux_ctx->streams[in_video_index]->time_base, encodec_ctx->time_base);
// ===== 裁剪:定位到起始关键帧 =====
// 向后定位到 <= 开始 的最近关键帧(I 帧):保证解码能从一张完整画面起步
// 之后所有包都必须送进解码器(维持参考帧),即使 10s 之前的帧解出后也会被丢弃
re = av_seek_frame(demux_ctx, in_video_index, start_video_ts, AVSEEK_FLAG_BACKWARD);
if (0 > re) {
std::cout << "av_seek_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
avcodec_flush_buffers(decodec_ctx); // 清掉旧参考帧,避免花屏
while (1) {
// 读取数据包
re = av_read_frame(demux_ctx, pkt);
// 已经读完
if (AVERROR_EOF == re) {
break;
}
if (0 > re) {
std::cout << "av_read_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
if (in_video_index == pkt->stream_index) {
// 发送数据包到解码器
re = avcodec_send_packet(decodec_ctx, pkt);
av_packet_unref(pkt);
if (0 > re) {
std::cout << "avcodec_send_packet failed!" << std::endl;
PrintErr(re);
goto Failed;
}
while (1) {
// 接收解码帧
re = avcodec_receive_frame(decodec_ctx, frame);
// 还未有完整的数据 | 已经读完
if (AVERROR(EAGAIN) == re || AVERROR_EOF == re) {
break;
}
if (0 > re) {
std::cout << "avcodec_receive_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// ===== 裁剪:视频起止判断(必须在“解码后”做)=====
// 因为 H.264 带 B/P 帧,解码需要参考前后帧;必须先解码再丢弃区间外的帧,否则画面花屏
// 用 frame->pts(显示顺序时间戳)判断,而非 pkt->pts(解码顺序,B 帧下不一致)
// 跳过起始位置之前:这些帧仍要解码以维持参考帧,只是解出后直接丢弃
if (AV_NOPTS_VALUE == frame->pts || start_video_ts > frame->pts) {
av_frame_unref(frame);
continue; // 跳过
}
// 到达截取末尾位置:标记结束并跳出当前解码循环(后续包仍会被读,但不再编码)
if (end_video_ts < frame->pts) {
video_done = true;
av_frame_unref(frame);
break;
}
// 像素格式转换
re = sws_scale(sws_ctx, frame->data, frame->linesize, 0, frame->height, sws_frame->data, sws_frame->linesize);
if (0 > re) {
std::cout << "sws_scale failed! " << std::endl;
PrintErr(re);
goto Failed;
}
// 把帧的时间单位“翻译”成编码器能看懂的单位
// 将解码后视频帧的显示时间戳(PTS),从“输入流的时间基”换算成“编码器的时间基”
sws_frame->pts = av_rescale_q(frame->pts, demux_ctx->streams[in_video_index]->time_base, encodec_ctx->time_base);
av_frame_unref(frame);
// ===== 裁剪:强制输出首帧为关键帧 =====
// 放在“起止判断之后、送编码器之前”:只有真正被保留的第一帧会触发
// 设 pict_type=I 是给编码器的输入提示,必须作用在 AVFrame 上、编码之前;
if (first_frame) {
first_frame = false;
sws_frame->pict_type = AV_PICTURE_TYPE_I;
}
// 发送数据包到编码器
re = avcodec_send_frame(encodec_ctx, sws_frame);
if (0 > re) {
std::cout << "avcodec_send_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
while (1) {
// 接收编码帧
re = avcodec_receive_packet(encodec_ctx, pkt);
// 还未有完整的数据 | 已经读完
if (AVERROR(EAGAIN) == re || AVERROR_EOF == re) {
break;
}
if (0 > re) {
std::cout << "avcodec_receive_packet failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 重新计算pts、dts、duration(视频偏移用 start_video_enc_ts:单位与编码器 time_base 一致)
rescale_packet_ts(pkt, encodec_ctx->time_base, mux_ctx->streams[out_video_index]->time_base, start_video_enc_ts);
// 写入文件
re = av_interleaved_write_frame(mux_ctx, pkt);
av_packet_unref(pkt);
if (0 != re) {
std::cout << "av_interleaved_write_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
std::cout << ". ";
}
}
} else if (in_audio_index == pkt->stream_index) {
// ===== 裁剪:音频起止判断(在“包”层做即可)=====
// 音频是透传、每包独立可解,不依赖前后帧,故可直接按 pkt->pts 丢弃区间外的压缩包
if (AV_NOPTS_VALUE == pkt->pts || start_audio_ts > pkt->pts) {
av_packet_unref(pkt);
continue;
}
if (end_audio_ts < pkt->pts) {
av_packet_unref(pkt);
audio_done = true;
if (video_done) {
break;
}
continue;
}
// 重新计算pts、dts、duration
// 音频是"透传"——包是从解复用器直接搬过来的,它的 pts/dts/duration 单位是输入音频流的 time_base(1/48000),从头到尾没碰过编码器
// 偏移用 start_audio_ts(单位一致),直接交给 rescale_packet_ts 扣掉即可
rescale_packet_ts(pkt, demux_ctx->streams[in_audio_index]->time_base, mux_ctx->streams[out_audio_index]->time_base, start_audio_ts);
// 写入文件
re = av_interleaved_write_frame(mux_ctx, pkt);
av_packet_unref(pkt);
if (0 != re) {
std::cout << "av_interleaved_write_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
} else {
std::cout << "other index: " << pkt->stream_index << std::endl;
av_packet_unref(pkt);
}
if (video_done && audio_done) {
break;
}
}
// 冲刷解码器(送出 NULL 包,逼出缓冲中的尾帧)
re = avcodec_send_packet(decodec_ctx, NULL);
if (0 > re) {
std::cout << "avcodec_send_packet failed!" << std::endl;
PrintErr(re);
goto Failed;
}
while (1) {
// 接收解码
re = avcodec_receive_frame(decodec_ctx, frame);
// 还未有完整的数据 | 已经读完
if (AVERROR(EAGAIN) == re || AVERROR_EOF == re) {
break;
}
if (0 > re) {
std::cout << "avcodec_receive_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 边界保护:B 帧重排后尾部可能超出 end,直接丢弃,不再写出
if (end_video_ts < frame->pts) {
av_frame_unref(frame);
break;
}
// 像素格式转换
re = sws_scale(sws_ctx, frame->data, frame->linesize, 0, frame->height, sws_frame->data, sws_frame->linesize);
if (0 > re) {
std::cout << "sws_scale failed! " << std::endl;
PrintErr(re);
goto Failed;
}
sws_frame->pts = av_rescale_q(frame->pts, demux_ctx->streams[in_video_index]->time_base, encodec_ctx->time_base);
av_frame_unref(frame);
// 发送编码
re = avcodec_send_frame(encodec_ctx, sws_frame);
if (0 > re) {
std::cout << "avcodec_send_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
while (1) {
// 接收编码
re = avcodec_receive_packet(encodec_ctx, pkt);
// 还未有完整的数据 | 已经读完
if (AVERROR(EAGAIN) == re || AVERROR_EOF == re) {
break;
}
if (0 > re) {
std::cout << "avcodec_receive_packet failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 重新计算pts、dts、duration
rescale_packet_ts(pkt, encodec_ctx->time_base, mux_ctx->streams[out_video_index]->time_base, start_video_enc_ts);
// 写入文件
re = av_interleaved_write_frame(mux_ctx, pkt);
av_packet_unref(pkt);
if (0 != re) {
std::cout << "av_interleaved_write_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
std::cout << "- ";
}
}
// 冲刷编码器(送出 NULL 帧,逼出最后缓冲的包)
re = avcodec_send_frame(encodec_ctx, NULL);
if (0 > re) {
std::cout << "avcodec_send_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
while (1) {
// 接收编码
re = avcodec_receive_packet(encodec_ctx, pkt);
// 还未有完整的数据 | 已经读完
if (AVERROR(EAGAIN) == re || AVERROR_EOF == re) {
break;
}
if (0 > re) {
std::cout << "avcodec_receive_packet failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 重新计算pts、dts、duration
rescale_packet_ts(pkt, encodec_ctx->time_base, mux_ctx->streams[out_video_index]->time_base, start_video_enc_ts);
// 写入文件
re = av_interleaved_write_frame(mux_ctx, pkt);
av_packet_unref(pkt);
if (0 != re) {
std::cout << "av_interleaved_write_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
std::cout << "= ";
}
// 写入文件尾部
re = av_write_trailer(mux_ctx);
if (0 != re) {
std::cout << "av_write_trailer failed!" << std::endl;
PrintErr(re);
goto Failed;
}
std::cout << "\nend!" << std::endl;
Failed:
// 释放所有资源
avformat_close_input(&demux_ctx);
avcodec_free_context(&decodec_ctx);
avcodec_free_context(&encodec_ctx);
if (mux_ctx) {
if (mux_ctx->pb) {
avio_closep(&mux_ctx->pb);
}
avformat_free_context(mux_ctx);
}
if (sws_ctx) sws_freeContext(sws_ctx);
av_frame_free(&sws_frame);
av_packet_free(&pkt);
av_frame_free(&frame);
return 0;
}
第八章 完整案例二:avformat_seek_file 版
代码导读:与案例一唯一区别在定位段——把
av_seek_frame换成avformat_seek_file的「时间窗口」,并额外#include <cstdint>(用到INT64_MIN)。其余裁剪逻辑(起止判断、首帧 I、归零、冲刷边界)完全照搬,连seek_min_ts/seek_max_ts两个变量都在案例一里预先声明了(只是案例一没用)。⚠ 注意下面定位段:原文示例把
seek_max_ts设成start + 2s(更灵活示范),但裁剪场景这会把窗口推过 start,可能静默丢开头(见 3.3 陷阱)。真正截取时建议改成seek_max_ts = start_video_ts。本例保留原文写法并加了警示注释,方便你对照理解“危险在哪”。
#include <iostream>
#include <string>
#include <cstdint>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}
// 打印错误信息
void PrintErr(int err) {
char buf[1024] = { 0 };
av_strerror(err, buf, sizeof(buf));
std::cout << buf << std::endl;
}
// 把包的 pts/dts/duration 从旧 time_base 重算到新 time_base
// 注意 offset 的单位必须和 cur_time_base 一致:
void rescale_packet_ts(AVPacket *&pkt, AVRational cur_time_base, AVRational target_time_base, int64_t offset = 0) {
pkt->pts = av_rescale_q_rnd(pkt->pts - offset, cur_time_base,target_time_base,
(AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt->dts = av_rescale_q_rnd(pkt->dts - offset, cur_time_base,target_time_base,
(AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt->duration = av_rescale_q(pkt->duration, cur_time_base, target_time_base);
pkt->pos = -1;
}
int main(void) {
// 解复用
std::string in_file_path = "广西之旅.mp4";
AVFormatContext *demux_ctx = NULL;
int in_video_index = -1;
int in_audio_index = -1;
// 解码
const AVCodec *decodec = NULL;
AVCodecContext *decodec_ctx = NULL;
// 编码
const AVCodec *encodec = NULL;
AVCodecContext *encodec_ctx = NULL;
// 复用
std::string out_file_path = "out_file.mp4";
AVFormatContext *mux_ctx = nullptr;
int out_video_index = -1;
int out_audio_index = -1;
// 像素格式转换
SwsContext *sws_ctx = nullptr;
AVFrame *sws_frame = av_frame_alloc();
// 其它
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
int re = -1;
// ===== 裁剪参数 =====
// 下面这组变量描述“截取原片的哪一段”,以及起止位置在各自时间基下的时间戳
// start_sec / end_sec:截取区间(秒),这里写死为 10~20 秒
double start_sec = 10.0;
double end_sec = 20.0;
// 视频起止时间戳:单位是“输入视频流 time_base”
// - 用于关键帧定位(落到 <= start 的最近关键帧)
// - 用于解码后按 frame->pts 丢弃区间外的帧
int64_t start_video_ts = 0;
int64_t end_video_ts = 0;
// 音频起止时间戳:单位是“输入音频流 time_base”
// - 音频是透传(不解码),直接按 pkt->pts 丢弃区间外的压缩包
int64_t start_audio_ts = 0;
int64_t end_audio_ts = 0;
// 两路流是否都已截到末尾:都满足后主循环可以提前 break,避免空转
bool video_done = false;
bool audio_done = false;
// 是否为“输出片段的第一帧”:只有第一个通过起止判断、真正被编码的帧会把它置 false
// —— 用来强制该帧编成关键帧(I 帧),让截出来的片段能独立解码
bool first_frame = true;
// 视频起始偏移换算到“编码器 time_base”后的值
// - 待写出的视频包来自编码器,其 pts 单位是编码器 time_base
// - 写包时统一减去它,片段即可从 0 秒起播(否则会“从 11 秒起”)
int64_t start_video_enc_ts = 0;
// ===== seek 相关(本例用 avformat_seek_file,下面两个变量真正用上)=====
int64_t seek_min_ts = 0;
int64_t seek_max_ts = 0;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// 解复用
// 打开视频文件
re = avformat_open_input(&demux_ctx, in_file_path.c_str(), NULL, NULL);
if (0 != re) {
std::cout << "avformat_open_input failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 查找流信息
re = avformat_find_stream_info(demux_ctx, NULL);
if (0 > re) {
std::cout << "avformat_find_stream_info failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 获得流索引
for (int i = 0; i < demux_ctx->nb_streams; ++i) {
if (AVMEDIA_TYPE_VIDEO == demux_ctx->streams[i]->codecpar->codec_type) {
in_video_index = i;
} else if (AVMEDIA_TYPE_AUDIO == demux_ctx->streams[i]->codecpar->codec_type) {
in_audio_index = i;
} else {
std::cout << "other index: " << i;
}
}
std::cout << "in video index: " << in_video_index << "\nin audio index: " << in_audio_index << std::endl;
av_dump_format(demux_ctx, in_video_index, in_file_path.c_str(), 0);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// 解码
// 查找视频解码器
decodec = avcodec_find_decoder(demux_ctx->streams[in_video_index]->codecpar->codec_id);
if (!decodec) {
std::cout << "avcodec_find_decoder failed!" << std::endl;
goto Failed;
}
// 创建解码器上下文
decodec_ctx = avcodec_alloc_context3(decodec);
if (!decodec_ctx) {
std::cout << "avcodec_alloc_context3 failed!" << std::endl;
goto Failed;
}
// 将流参数复制到解码器上下文
re = avcodec_parameters_to_context(decodec_ctx, demux_ctx->streams[in_video_index]->codecpar);
if (0 > re) {
std::cout << "avcodec_parameters_to_contextv failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 打开解码器
re = avcodec_open2(decodec_ctx, decodec, NULL);
if (0 != re) {
std::cout << "avcodec_open2 failed!" << std::endl;
PrintErr(re);
goto Failed;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// 编码
// 查找视频编码器
encodec = avcodec_find_encoder(demux_ctx->streams[in_video_index]->codecpar->codec_id);
if (!encodec) {
std::cout << "avcodec_find_encoder failed!" << std::endl;
goto Failed;
}
// 创建编码器上下文
encodec_ctx = avcodec_alloc_context3(encodec);
if (!encodec_ctx) {
std::cout << "avcodec_alloc_context3 failed!" << std::endl;
goto Failed;
}
// 设置编码器基础参数
{
encodec_ctx->width = decodec_ctx->width;
encodec_ctx->height = decodec_ctx->height;
// 指定格式为yuv420p
encodec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
// time_base
AVRational framerate = demux_ctx->streams[in_video_index]->avg_frame_rate;
if (0 >= framerate.den || 0 >= framerate.num) {
// 根据参数‘猜测’结果
framerate = av_guess_frame_rate(demux_ctx, demux_ctx->streams[in_video_index], NULL);
}
if (0 >= framerate.den || 0 >= framerate.num) {
framerate = (AVRational){ 25, 1 };
}
encodec_ctx->framerate = framerate;
encodec_ctx->time_base = av_inv_q(framerate);
// 像素宽高比
encodec_ctx->sample_aspect_ratio = decodec_ctx->sample_aspect_ratio;
}
// 打开编码器
re = avcodec_open2(encodec_ctx, encodec, NULL);
if (0 != re) {
std::cout << "avcodec_open2 failed!" << std::endl;
PrintErr(re);
goto Failed;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// 复用
// 创建复用上下文
re = avformat_alloc_output_context2(&mux_ctx, NULL, NULL, out_file_path.c_str());
if (0 > re) {
std::cout << "avformat_alloc_output_context2 failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 创建音视频流
// 创建【视频流】
if (0 <= in_video_index) {
AVStream *stream = avformat_new_stream(mux_ctx, NULL); // 新建视频流
stream->time_base = encodec_ctx->time_base; // 输出流 time_base 先与编码器保持一致
out_video_index = stream->index;
avcodec_parameters_from_context(stream->codecpar, encodec_ctx); // 把编码器参数写进流
}
// 创建【音频流】:走“透传”——不解码不编码,直接复制参数、原样搬运压缩包
if (0 <= in_audio_index) {
AVStream *stream = avformat_new_stream(mux_ctx, NULL);
avcodec_parameters_copy(stream->codecpar, demux_ctx->streams[in_audio_index]->codecpar);
stream->time_base = demux_ctx->streams[in_audio_index]->time_base;
out_audio_index = stream->index;
}
std::cout << "out video index: " << out_video_index << "\nout audio index: " << out_audio_index << std::endl;
// 打开io
re = avio_open2(&mux_ctx->pb, out_file_path.c_str(), AVIO_FLAG_WRITE, NULL, NULL);
if (0 > re) {
std::cout << "avio_open2 failed!" << std::endl;
PrintErr(re);
goto Failed;
}
av_dump_format(mux_ctx, out_video_index, out_file_path.c_str(), 1);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// 像素格式转换
sws_ctx = sws_getContext(decodec_ctx->width, decodec_ctx->height, (AVPixelFormat)decodec_ctx->pix_fmt,
encodec_ctx->width, encodec_ctx->height, (AVPixelFormat)encodec_ctx->pix_fmt,
SWS_BILINEAR, NULL, NULL, NULL);
if (!sws_ctx) {
std::cout << "sws_getContext failed! " << std::endl;
return -1;
}
// 分配内存,用于接收转换后的一帧图像
sws_frame->width = encodec_ctx->width;
sws_frame->height = encodec_ctx->height;
sws_frame->format = encodec_ctx->pix_fmt;
re = av_frame_get_buffer(sws_frame, 32);
if (0 > re) {
std::cout << "av_frame_get_buffer failed! " << std::endl;
PrintErr(re);
goto Failed;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 写入文件头
re = avformat_write_header(mux_ctx, NULL);
if (0 > re) {
std::cout << "avformat_write_header failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// ===== 裁剪:把“秒”换算成各流自己的时间戳 =====
// 真实秒数 × AV_TIME_BASE 得到微秒,再 av_rescale_q 到对应流的 time_base,即该流下的 pts 值
// 例如视频 30k tbn:start_video_ts = 10 * 1e6 * (1/30000) = 300000
// 音频 48k tbn:start_audio_ts = 10 * 1e6 * (1/48000) = 480000
start_video_ts = av_rescale_q(start_sec * AV_TIME_BASE, AV_TIME_BASE_Q,
demux_ctx->streams[in_video_index]->time_base);
end_video_ts = av_rescale_q(end_sec * AV_TIME_BASE, AV_TIME_BASE_Q,
demux_ctx->streams[in_video_index]->time_base);
if (0 <= in_audio_index) {
start_audio_ts = av_rescale_q(start_sec * AV_TIME_BASE, AV_TIME_BASE_Q,
demux_ctx->streams[in_audio_index]->time_base);
end_audio_ts = av_rescale_q(end_sec * AV_TIME_BASE, AV_TIME_BASE_Q,
demux_ctx->streams[in_audio_index]->time_base);
}
// ★ 关键:视频偏移必须换算到“编码器 time_base”
// 解复用器里 start_video_ts 单位是输入视频流 time_base
// 但待写出的视频包来自编码器,其 pts 单位是编码器 time_base
// 两者数量级差 1000 倍,直接减会让 pts 变成负数 —— 必须先 av_rescale_q 换算
start_video_enc_ts = av_rescale_q(start_video_ts, demux_ctx->streams[in_video_index]->time_base, encodec_ctx->time_base);
// ===== 裁剪:定位到起始关键帧 =====
// avformat_seek_file 比 av_seek_frame 更灵活:除了“目标时间戳 ts”,还允许给一个“时间窗口”[min_ts, max_ts]
// demuxer 会在该窗口内挑选一个“尽量靠近 ts 的关键帧(I 帧)”作为落点
// —— 既能像 BACKWARD 那样落 start 之前,也允许接受 start 附近(含略微之后)的关键帧,定位更准
// 单位约定:min_ts / ts / max_ts 都基于 stream_index 对应流的 time_base(这里即输入视频流 time_base)
// 下面给出“允许落后任意远、但最多比 start 晚 2 秒”的窗口:
seek_min_ts = INT64_MIN; // 不限制下限:允许落在 start 之前的最近关键帧
seek_max_ts = start_video_ts +
av_rescale_q(2 * AV_TIME_BASE, AV_TIME_BASE_Q,
demux_ctx->streams[in_video_index]->time_base); // 允许比 start 晚至多 2 秒
// ⚠ 注意:裁剪场景下这会把窗口推过 start,可能静默丢掉开头(见 3.3 陷阱说明)。
// 真正做"截取"时,建议把上面这行改成 seek_max_ts = start_video_ts(上界=start,绝不丢头)。
// flags=0:不强制 BACKWARD/ANY,由 demuxer 在窗口内自行选最合适的关键帧
re = avformat_seek_file(demux_ctx, in_video_index, seek_min_ts, start_video_ts, seek_max_ts, 0);
// —— 想要与 av_seek_frame(..., AVSEEK_FLAG_BACKWARD) 完全等价?把窗口退化成“点”即可:
// avformat_seek_file(demux_ctx, in_video_index, INT64_MIN, start_video_ts, start_video_ts, AVSEEK_FLAG_BACKWARD);
if (0 > re) {
std::cout << "avformat_seek_file failed!" << std::endl;
PrintErr(re);
goto Failed;
}
avcodec_flush_buffers(decodec_ctx); // 清掉旧参考帧,避免花屏
while (1) {
// 读取数据包
re = av_read_frame(demux_ctx, pkt);
// 已经读完
if (AVERROR_EOF == re) {
break;
}
if (0 > re) {
std::cout << "av_read_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
if (in_video_index == pkt->stream_index) {
// 发送数据包到解码器
re = avcodec_send_packet(decodec_ctx, pkt);
av_packet_unref(pkt);
if (0 > re) {
std::cout << "avcodec_send_packet failed!" << std::endl;
PrintErr(re);
goto Failed;
}
while (1) {
// 接收解码帧
re = avcodec_receive_frame(decodec_ctx, frame);
// 还未有完整的数据 | 已经读完
if (AVERROR(EAGAIN) == re || AVERROR_EOF == re) {
break;
}
if (0 > re) {
std::cout << "avcodec_receive_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// ===== 裁剪:视频起止判断(必须在“解码后”做)=====
// 因为 H.264 带 B/P 帧,解码需要参考前后帧;必须先解码再丢弃区间外的帧,否则画面花屏
// 用 frame->pts(显示顺序时间戳)判断,而非 pkt->pts(解码顺序,B 帧下不一致)
// 跳过起始位置之前:这些帧仍要解码以维持参考帧,只是解出后直接丢弃
if (AV_NOPTS_VALUE == frame->pts || start_video_ts > frame->pts) {
av_frame_unref(frame);
continue; // 跳过
}
// 到达截取末尾位置:标记结束并跳出当前解码循环(后续包仍会被读,但不再编码)
if (end_video_ts < frame->pts) {
video_done = true;
av_frame_unref(frame);
break;
}
// 像素格式转换
re = sws_scale(sws_ctx, frame->data, frame->linesize, 0, frame->height, sws_frame->data, sws_frame->linesize);
if (0 > re) {
std::cout << "sws_scale failed! " << std::endl;
PrintErr(re);
goto Failed;
}
// 把帧的时间单位“翻译”成编码器能看懂的单位
// 将解码后视频帧的显示时间戳(PTS),从“输入流的时间基”换算成“编码器的时间基”
sws_frame->pts = av_rescale_q(frame->pts, demux_ctx->streams[in_video_index]->time_base, encodec_ctx->time_base);
av_frame_unref(frame);
// ===== 裁剪:强制输出首帧为关键帧 =====
// 放在“起止判断之后、送编码器之前”:只有真正被保留的第一帧会触发
// 设 pict_type=I 是给编码器的输入提示,必须作用在 AVFrame 上、编码之前;
if (first_frame) {
first_frame = false;
sws_frame->pict_type = AV_PICTURE_TYPE_I;
}
// 发送数据包到编码器
re = avcodec_send_frame(encodec_ctx, sws_frame);
if (0 > re) {
std::cout << "avcodec_send_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
while (1) {
// 接收编码帧
re = avcodec_receive_packet(encodec_ctx, pkt);
// 还未有完整的数据 | 已经读完
if (AVERROR(EAGAIN) == re || AVERROR_EOF == re) {
break;
}
if (0 > re) {
std::cout << "avcodec_receive_packet failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 重新计算pts、dts、duration(视频偏移用 start_video_enc_ts:单位与编码器 time_base 一致)
rescale_packet_ts(pkt, encodec_ctx->time_base, mux_ctx->streams[out_video_index]->time_base, start_video_enc_ts);
// 写入文件
re = av_interleaved_write_frame(mux_ctx, pkt);
av_packet_unref(pkt);
if (0 != re) {
std::cout << "av_interleaved_write_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
std::cout << ". ";
}
}
} else if (in_audio_index == pkt->stream_index) {
// ===== 裁剪:音频起止判断(在“包”层做即可)=====
// 音频是透传、每包独立可解,不依赖前后帧,故可直接按 pkt->pts 丢弃区间外的压缩包
if (AV_NOPTS_VALUE == pkt->pts || start_audio_ts > pkt->pts) {
av_packet_unref(pkt);
continue;
}
if (end_audio_ts < pkt->pts) {
av_packet_unref(pkt);
audio_done = true;
if (video_done) {
break;
}
continue;
}
// 重新计算pts、dts、duration
// 音频是"透传"——包是从解复用器直接搬过来的,它的 pts/dts/duration 单位是输入音频流的 time_base(1/48000),从头到尾没碰过编码器
// 偏移用 start_audio_ts(单位一致),直接交给 rescale_packet_ts 扣掉即可
rescale_packet_ts(pkt, demux_ctx->streams[in_audio_index]->time_base, mux_ctx->streams[out_audio_index]->time_base, start_audio_ts);
// 写入文件
re = av_interleaved_write_frame(mux_ctx, pkt);
av_packet_unref(pkt);
if (0 != re) {
std::cout << "av_interleaved_write_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
} else {
std::cout << "other index: " << pkt->stream_index << std::endl;
av_packet_unref(pkt);
}
if (video_done && audio_done) {
break;
}
}
// 冲刷解码器(送出 NULL 包,逼出缓冲中的尾帧)
re = avcodec_send_packet(decodec_ctx, NULL);
if (0 > re) {
std::cout << "avcodec_send_packet failed!" << std::endl;
PrintErr(re);
goto Failed;
}
while (1) {
// 接收解码
re = avcodec_receive_frame(decodec_ctx, frame);
// 还未有完整的数据 | 已经读完
if (AVERROR(EAGAIN) == re || AVERROR_EOF == re) {
break;
}
if (0 > re) {
std::cout << "avcodec_receive_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 边界保护:B 帧重排后尾部可能超出 end,直接丢弃,不再写出
if (end_video_ts < frame->pts) {
av_frame_unref(frame);
break;
}
// 像素格式转换
re = sws_scale(sws_ctx, frame->data, frame->linesize, 0, frame->height, sws_frame->data, sws_frame->linesize);
if (0 > re) {
std::cout << "sws_scale failed! " << std::endl;
PrintErr(re);
goto Failed;
}
sws_frame->pts = av_rescale_q(frame->pts, demux_ctx->streams[in_video_index]->time_base, encodec_ctx->time_base);
av_frame_unref(frame);
// 发送编码
re = avcodec_send_frame(encodec_ctx, sws_frame);
if (0 > re) {
std::cout << "avcodec_send_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
while (1) {
// 接收编码
re = avcodec_receive_packet(encodec_ctx, pkt);
// 还未有完整的数据 | 已经读完
if (AVERROR(EAGAIN) == re || AVERROR_EOF == re) {
break;
}
if (0 > re) {
std::cout << "avcodec_receive_packet failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 重新计算pts、dts、duration
rescale_packet_ts(pkt, encodec_ctx->time_base, mux_ctx->streams[out_video_index]->time_base, start_video_enc_ts);
// 写入文件
re = av_interleaved_write_frame(mux_ctx, pkt);
av_packet_unref(pkt);
if (0 != re) {
std::cout << "av_interleaved_write_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
std::cout << "- ";
}
}
// 冲刷编码器(送出 NULL 帧,逼出最后缓冲的包)
re = avcodec_send_frame(encodec_ctx, NULL);
if (0 > re) {
std::cout << "avcodec_send_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
while (1) {
// 接收编码
re = avcodec_receive_packet(encodec_ctx, pkt);
// 还未有完整的数据 | 已经读完
if (AVERROR(EAGAIN) == re || AVERROR_EOF == re) {
break;
}
if (0 > re) {
std::cout << "avcodec_receive_packet failed!" << std::endl;
PrintErr(re);
goto Failed;
}
// 重新计算pts、dts、duration
rescale_packet_ts(pkt, encodec_ctx->time_base, mux_ctx->streams[out_video_index]->time_base, start_video_enc_ts);
// 写入文件
re = av_interleaved_write_frame(mux_ctx, pkt);
av_packet_unref(pkt);
if (0 != re) {
std::cout << "av_interleaved_write_frame failed!" << std::endl;
PrintErr(re);
goto Failed;
}
std::cout << "= ";
}
// 写入文件尾部
re = av_write_trailer(mux_ctx);
if (0 != re) {
std::cout << "av_write_trailer failed!" << std::endl;
PrintErr(re);
goto Failed;
}
std::cout << "\nend!" << std::endl;
Failed:
// 释放所有资源
avformat_close_input(&demux_ctx);
avcodec_free_context(&decodec_ctx);
avcodec_free_context(&encodec_ctx);
if (mux_ctx) {
if (mux_ctx->pb) {
avio_closep(&mux_ctx->pb);
}
avformat_free_context(mux_ctx);
}
if (sws_ctx) sws_freeContext(sws_ctx);
av_frame_free(&sws_frame);
av_packet_free(&pkt);
av_frame_free(&frame);
return 0;
}
第九章 两版差异点汇总
| 位置 | av_seek_frame 版 | avformat_seek_file 版 |
|---|---|---|
| 头文件 | <iostream> <string> | 多出 <cstdint>(INT64_MIN 需要) |
| 定位调用 | av_seek_frame(..., start_video_ts, AVSEEK_FLAG_BACKWARD) | avformat_seek_file(..., seek_min_ts, start_video_ts, seek_max_ts, 0) |
| 落点语义 | 一定 ≤ start 的最近关键帧 | 窗口 [min_ts, max_ts] 内最合适关键帧(可略后) |
| 其余裁剪逻辑 | 起止判断 / 首帧 I / 归零 / 冲刷边界 | 完全相同 |
两版"定位段"的差异(一眼 diff):
--- 案例一:av_seek_frame 定位段
+++ 案例二:avformat_seek_file 定位段
// 把"秒"换算成输入视频流自己的时间戳(两版相同,略)
- re = av_seek_frame(demux_ctx, in_video_index, start_video_ts, AVSEEK_FLAG_BACKWARD);
+ // 裁剪场景建议把上界卡在 start,别推过 start(否则静默丢头,见 3.3)
+ int64_t seek_min_ts = INT64_MIN;
+ int64_t seek_max_ts = start_video_ts; // ← 原文示例用了 start+2s,裁剪时不推荐
+ re = avformat_seek_file(demux_ctx, in_video_index, seek_min_ts, start_video_ts, seek_max_ts, 0);
if (0 > re) { /* ... */ }
avcodec_flush_buffers(decodec_ctx); // 两版都必须
一句话:案例二只是把"定位"这一步换成了更灵活的
avformat_seek_file,裁剪主体逻辑一行未动。实际项目里两者都能跑通,选哪个取决于你是否需要"允许略过 start 之后的关键帧来减少解码量"。但做截取时务必把窗口上界卡在start,否则会像 3.3 说的那样吃掉片段开头。
第十章 编译与运行

第十一章 进阶
- 让封装器替你归零:写头时传
avoid_negative_ts=make_zero,就不需要手动算start_video_enc_ts,官方已填平单位坑:AVDictionary *opts = NULL; av_dict_set(&opts, "avoid_negative_ts", "make_zero", 0); avformat_write_header(mux_ctx, &opts); av_dict_free(&opts); - 直接用 ffmpeg 命令行(生产首选):
# 重编码 + 音频透传,输入前 seek(快) ffmpeg -ss 00:00:10 -to 00:00:20 -i 广西之旅.mp4 -c:v libx264 -c:a copy out.mp4 # 无损快剪(仅关键帧,不重编码) ffmpeg -ss 00:00:10 -to 00:00:20 -i 广西之旅.mp4 -c copy out.mp4
第十二章 常见坑与排错(FAQ)
实战里最容易踩的坑,按"现象 → 根因 → 修复"列出来,方便对照自查:
| 现象 | 根因 | 修复 |
|---|---|---|
| 片段开头花屏 / 绿块 | seek 后没 avcodec_flush_buffers,解码器残留旧参考帧 | seek 之后立刻 avcodec_flush_buffers(decodec_ctx) |
| 起点不是完整画面、整段花 | 用了 AVSEEK_FLAG_ANY,落点落到非关键帧 | 用 AVSEEK_FLAG_BACKWARD,保证落 I 帧 |
| 播放器从 11 秒起 / 时间轴错乱 | start_video_enc_ts 没换算到编码器 time_base,视频 pts 变负 | 先 av_rescale_q 到编码器 tb 再减(见 6.5) |
| 音画不同步 | 视频用 start_video_enc_ts 归零,音频却用错 offset(单位不一致) | 音频用 start_audio_ts(输入音频流 tb),见 6.5 注 |
| 片段比预期短 / 开头被吃 | avformat_seek_file 的 max_ts > start,落点晚于 start | 裁剪时 max_ts = start 或改用 BACKWARD(见 3.3) |
| seek 直接返回 -1 失败 | 容器无索引 / stream_index 传错 | 先 avformat_find_stream_info;用 in_video_index |
| 开头几秒卡顿后才正常 | 落点 I 帧离 start 太远,前段帧解出丢弃是正常开销 | 属正常;缩短 GOP 或选更近 I 可缓解 |
记住两条主线:花屏类几乎都来自"关键帧/参考帧"没处理好(seek 落点 + flush);时间轴类几乎都来自"time_base 单位"没换算对(偏移单位必须和待处理包的时间基一致)。
第十三章 小结 + 术语表
13.1 小结
- 裁剪 = 复用流程 + 「seek 关键帧 → 解码端丢帧 → 首帧强制 I → 输出归零」。
av_seek_frame:经典、简单,BACKWARD保证落到 start 之前最近关键帧,确定性强。avformat_seek_file:前者的超集,靠min_ts/ts/max_ts时间窗口让 demuxer 选最合适关键帧,定位更灵活、可略过 start 之后减少解码;窗口退化成“点”即等价于av_seek_frame(BACKWARD)。- 两者定位后都必须
avcodec_flush_buffers,且裁剪主体逻辑完全一致。 - 最大的坑是时间戳单位——任何“减偏移”操作,offset 的单位都必须和待处理包的时间基一致,否则负数 pts 会让播放器直接错乱。理解
time_base与av_rescale_q,就理解了 FFmpeg 裁剪的半壁江山。
13.2 术语表(Glossary)
| 术语 | 中文 | 一句话 |
|---|---|---|
| Container / 容器 | 容器(mp4/mkv…) | 装多条“流”的文件外壳 |
| Stream / 流 | 流 | 容器里的一条独立数据轨(视频/音频/字幕) |
AVPacket | 包 | 压缩后的小段数据,带时间戳,在解复用/复用间流动 |
AVFrame | 帧 | 解码后的裸数据(一帧画面 / 一段采样) |
| Demux / 解复用 | 解复用 | 把容器拆成包 |
| Decode / 解码 | 解码 | 包 → 帧 |
| Encode / 编码 | 编码 | 帧 → 包 |
| Mux / 复用 | 复用 | 包 → 新容器文件 |
| time_base | 时间基 | “1 个刻度 = 多少秒”,FFmpeg 的尺子单位 |
| PTS | 显示时间戳 | 这帧“该在第几秒被看到”(刻度值) |
| DTS | 解码时间戳 | 这帧“该在第几秒被解码”(刻度值) |
| GOP | 图像组 | 从一个 I 帧到下一个 I 帧之前的一组帧 |
| I / P / B 帧 | 关键帧 / 前向预测 / 双向预测 | 帧的三种类型,B 帧让解码≠显示顺序 |
av_rescale_q | 时间基换算 | 把时间戳从一个 time_base 换到另一个 |
| 透传 / copy | 透传 | 音频不解码不编码,原样搬包 |
回到第零章再扫一遍,确认每个术语你都能对应上代码里的哪一行,这篇博客就算真正读透了。

1009

被折叠的 条评论
为什么被折叠?



