FFmpeg 视频裁剪 av_seek_frame 与 avformat_seek_file 两种定位方式全解析

FFmpeg 视频裁剪 av_seek_frame 与 avformat_seek_file 两种定位方式全解析

本文在「解复用 → 解码 → 编码 → 复用」标准链路的基础上,重点讲解如何截取原片的某一段(如 10~20 秒)。代码以 C++ 调用 FFmpeg(libavformat / libavcodec / libswscale)实现,视频重编码、音频透传。

与常见“复用流程”文章最大的不同:本文用整整两章拆解裁剪的起点——关键帧定位,分别讲透 av_seek_frameavformat_seek_file函数原型、参数语义、实现要点,并给出两套已跑通、仅定位方式不同的完整代码流程图,让你既能“会用”,也知道“内部怎么选关键帧”。


目录


第零章 前置知识:先搞懂 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/4800010 × 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)

ptsdts 为什么是两个?因为有 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 本文到底要做什么(先看结论)

把上面四点连起来,我们要做的事就清晰了:

  1. 素材是“一段 0~N 秒的完整视频”。我们要它的 10~20 秒
  2. 但视频不能从 15 秒直接切——15 秒处多半是 P/B 帧,切了放不出来。所以先把读取位置移到 10 秒之前最近的关键帧(I 帧),这一步就是 av_seek_frame / avformat_seek_file 的活。
  3. 从那个 I 帧开始解码,10 秒之前的帧解出后立刻丢掉(但仍要解,为了给后面的帧攒参考帧)。
  4. 真正落在 10~20 秒的帧,重新编码、写进新文件;写的时候把时间戳“减去起点”,让新片段从 0 秒起播。
  5. 音频不解码,按时间戳直接挑 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。传 -1timestampAV_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_BACKWARD1向后找:落点时间戳 ≤ 请求值,且尽量靠近(即落到 ≤ start 的最近关键帧)。✅ 最常用、最安全
AVSEEK_FLAG_BYTE2timestamp字节偏移解释(而非时间),用于基于文件位置的 seek。❌ 裁剪不用
AVSEEK_FLAG_ANY4允许落到任意帧(含非关键帧)。❌ 千万别用,否则起点不是 I 帧 → 花屏
AVSEEK_FLAG_FRAME8timestamp帧号解释。❌ 裁剪不用

一句话: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_frametimestamp)。
  • min_ts / max_ts:允许的落点区间。demuxer 会在这个窗口内挑选一个“尽量靠近 ts 的关键帧”作为实际落点——而不是死板地一定 ≤ ts

典型组合:

想要的效果min_tstsmax_tsflags
等价于 av_seek_frame(BACKWARD)INT64_MINstartstartAVSEEK_FLAG_BACKWARD
允许 start 之前任意远、最多晚 2 秒INT64_MINstartstart + 2s0
严格落在 [start, start+1s] 内startstartstart + 1s0

裁剪场景的窗口陷阱(重点):上表第二行把 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_fileav_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_frameavformat_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)是前者的一般化

内部等价于

avformat_seek_file

窗口 [min_ts, max_ts]

可能比 start 略后

start

窗口内最合适 I 帧

定位更准 / 少解几帧

av_seek_frame

BACKWARD 强制 ≤ start

start

最近 I 帧


第五章 整体处理流程图 + 跟着一帧走一遍

5.1 流程图

< start

> end

区间内

音频包

EOF

打开输入 avformat_open_input

查找流信息 / 找解码器与编码器

创建输出 mux 上下文与音视频流

像素格式转换 sws_getContext

写入文件头 write_header

定位起始关键帧 seek

avcodec_flush_buffers 清参考帧

av_read_frame 读包

视频包?

送解码器 → 收帧

frame->pts 在区间?

丢弃 但已解码维持参考帧

video_done = true

sws 像素转换 + 首帧强制 I

送编码器 → 收包

rescale_packet_ts 时间戳归零

av_interleaved_write_frame

pkt->pts 在区间?

丢包

透传 rescale + 写盘

冲刷解码器 / 编码器

写 trailer 完成

对应文字版(裁剪 = 复用流程 + 5 处专属逻辑):

解复用 ──► ① 定位到起始关键帧(seek)
   │
   ▼
解码 ──► ② 解码后按 frame->pts 丢弃区间外的帧(维持参考帧)
   │
   ▼
缩放 ──► ③ 强制输出首帧为 I 帧
   │
   ▼
编码 ──► ④ 写包前把时间戳归零(减去起始偏移)
   │
   ▼
复用 ──► ⑤ 冲刷阶段同样做边界保护

其中 ②④ 是裁剪正确性的关键,①④ 是“播放器从 0 秒起播”的关键。

5.2 跟着“第一帧被保留的帧”走一遍(数字例子)

光看流程图还是抽象,下面把第一个真正写进新文件的视频帧从头到尾算一遍。假设:

  • 素材:30fps,输入视频流 time_base = 1/30000,GOP 每 3 秒一个 I 帧(I 在 9.0s、12.0s…)。
  • 截取区间 start=10s, end=20sstart_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_unrefcontinue——解了但不写

第 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/4800010 秒 的 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;
}

原因:

  1. 帧间依赖:start 之前的关键帧到 start 之间的帧,必须送进解码器累积参考帧,解出后立刻丢弃——否则 start 那帧无法正确解码(见 5.2 第 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 行是完整可运行程序。建议这样读:

  1. main 开头一堆变量 = “裁剪参数”(start/end、各流时间戳、起止标志)。
  2. /// 解复用 / 解码 / 编码 / 复用 / 像素格式转换 五段 = 0.2 流水线的“搭环境”。
  3. 真正裁剪专属逻辑只有 5 处(对应第 5.1 文字版 ①②③④⑤):定位段(本例 av_seek_frame)、起止判断首帧 I归零写包冲刷边界。其余都是标准复用流程。
  4. 本例定位段用 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 说的那样吃掉片段开头。


第十章 编译与运行

请添加图片描述

第十一章 进阶

  1. 让封装器替你归零:写头时传 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);
    
  2. 直接用 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_filemax_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_baseav_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透传音频不解码不编码,原样搬包

回到第零章再扫一遍,确认每个术语你都能对应上代码里的哪一行,这篇博客就算真正读透了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cpp_learners

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值