从 AVPacket 到 AVFrame,H.264/H.265 在 FFmpeg 中是如何承上启下的

前言:为什么只懂 H.264/H.265 还不够

很多人学视频编码时,容易停在“标准知识”这一层。

比如知道:

  • H.264 和 H.265 是主流视频编码标准
  • I / P / B 帧分别是什么
  • SPS / PPS / VPS 是参数集
  • Annex B 和 AVCC 是两种常见码流组织方式

这些知识当然重要,但如果你真正做工程,只懂这些还远远不够。

因为在真实项目里,你面对的通常不是“抽象的 H.264 标准”,而是:

  • 一个 MP4 文件或者 RTSP 流怎么被送进系统
  • 一段压缩码流怎么进入解码器
  • 解码后为什么变成了 AVFrame
  • 滤镜为什么处理的是 frame,而不是 packet
  • 编码器为什么会“吃进去一帧,却不立刻吐出一个包”
  • 时间戳为什么总是乱
  • 为什么有时候明明是 H.264,却还是解不出来

所以说,标准知识是基础,FFmpeg 中的数据流转才是工程落地的关键。

如果把 H.264/H.265 比作“视频压缩语言”,那么 FFmpeg 就像一个完整的视频处理流水线,而 codec 层正处在这个流水线的中枢位置。

这篇文章的目标,就是把这个中枢位置讲清楚。


1. 先看全局:FFmpeg 整体链路到底在做什么

在工程里,FFmpeg 通常不只是“解码器”或者“编码器”,而是一整套多媒体处理框架。

一条典型的视频处理链路,经常可以抽象成下面这几个环节:

flowchart LR
    A[输入源\n文件 / 摄像头 / RTSP / RTP] --> B[Demux\n解封装]
    B --> C[Packet\n压缩数据包]
    C --> D[Decode\n解码]
    D --> E[Frame\n原始像素帧]
    E --> F[Filter\n滤镜/缩放/格式转换]
    F --> G[Encode\n编码]
    G --> H[Packet\n压缩数据包]
    H --> I[Mux\n封装]
    I --> J[输出\nMP4 / FLV / TS / 推流]

这条链路可以出现在很多场景里:

1.1 播放

如果是播放器,主线一般是:

demux -> decode -> render

也就是说:

  • 先把容器里的流拆出来
  • 再把压缩视频解成原始帧
  • 最后送给渲染模块显示

1.2 转码

如果是转码,主线一般是:

demux -> decode -> filter -> encode -> mux

也就是:

  • 先解封装
  • 再解码
  • 如果需要缩放、裁剪、叠字、转像素格式,就进入 filter
  • 然后重新编码
  • 最后再封装成新的输出文件

1.3 推流

如果是直播或推流,常见情况会更复杂:

  • 有时是采集原始帧,然后编码、封装、推送
  • 有时是收到压缩码流后,做转封装直接推送
  • 有时要先解码、处理、再重新编码推送

所以从工程角度看,H.264/H.265 并不是 FFmpeg 的全部,它只是整个媒体流水线中的 codec 层。


2. H.264/H.265 在 FFmpeg 中处于什么位置

如果把整个链路拆开,H.264/H.265 的位置其实很清楚。

2.1 输入侧:它可能是 demux 之后拿到的压缩码流

比如你打开一个 MP4 文件,FFmpeg 先做的不是“立刻解码”,而是先解封装。

MP4 只是容器,里面真正的视频内容,可能是:

  • H.264
  • H.265
  • MPEG-4
  • VP9
  • AV1

demux 的作用,是把容器层拆开,拿到压缩后的 packet。

这些 packet 里装着的,才是 H.264/H.265 码流数据。

2.2 解码侧:它会把 packet 变成 frame

如果这段 packet 是 H.264/H.265,那么接下来就交给对应解码器处理。

解码器做的事,本质上就是:

  • 解析码流结构
  • 熵解码
  • 反量化
  • 反变换
  • 做帧间/帧内重建
  • 输出原始图像帧

也就是说:

输入是压缩数据包 AVPacket,输出是原始图像帧 AVFrame

2.3 编码侧:frame 会被重新压成 packet

如果你后面还要输出成 H.264/H.265,那么编码器又会做反过来的事:

  • 输入原始图像帧 AVFrame
  • 做预测、变换、量化、熵编码
  • 输出压缩后的 AVPacket

2.4 输出侧:packet 再被 mux 到容器里

编码出来的 packet 还不能直接说“这是个 MP4 文件”。

因为 packet 只是压缩码流数据,真正要落成 MP4 / FLV / TS,还要经过 mux,把这些 packet 写进对应容器结构中。

所以从前到后串起来就是:

1
容器 -> demux -> 压缩 packet -> decode -> 原始 frame -> filter -> encode -> 压缩 packet -> mux -> 新容器

这就是 H.264/H.265 在 FFmpeg 中“承上启下”的真正位置。


3. 解码主线:从 AVPacket 到 AVFrame

这部分是理解 FFmpeg codec 层最核心的一步。


3.1 AVPacket 是什么

AVPacket 可以把它理解成:

一段带有时间信息的压缩数据包。

它通常包含这些关键信息:

  • 指向压缩数据的缓冲区
  • 数据大小
  • pts
  • dts
  • stream index
  • flags(比如关键帧标记)

对视频来说,packet 里装的往往不是“完整一张图”,而是某段压缩码流。

注意,这里非常容易产生误解。

AVPacket 不等于一帧画面

很多初学者会下意识觉得:

一个 packet = 一帧视频

这不总成立。

原因有几个:

  • 有些封装格式里,一个 packet 可能对应一个 access unit
  • 有些场景下,一个 packet 可能不完整,需要 parser 进一步拆
  • 有时一帧图像的数据可能跨多个 packet
  • 音频里这种“一包不等于一帧”的情况更常见

所以更稳妥的理解是:

packet 是压缩码流在系统中的传输单位,而不是天然等于显示帧。


3.2 AVFrame 是什么

AVFrame 可以理解成:

一帧已经解码后的原始媒体数据。

对视频来说,里面通常是:

  • YUV 或 RGB 像素数据
  • 宽高
  • 像素格式
  • 每个平面的 data 指针
  • linesize
  • pts
  • 颜色空间、色度范围等附加信息

比如最常见的视频 frame,可能是:

  • AV_PIX_FMT_YUV420P
  • 1920
  • 1080

这时 frame 里的数据,已经不再是压缩码流,而是真正可以做:

  • 缩放
  • 裁剪
  • 颜色转换
  • 叠字
  • AI 推理
  • 渲染显示

的原始像素内容了。


3.3 AVCodec 和 AVCodecContext 在干什么

AVCodec

AVCodec 更像“编解码器的静态描述”。

它告诉你:

  • 这是什么 codec
  • 是编码器还是解码器
  • 支持哪些能力
  • 对应哪些回调实现

比如:

  • h264
  • hevc
  • libx264
  • h264_nvenc

AVCodecContext

AVCodecContext 则更像“某次编解码实例的运行上下文”。

里面会保存:

  • 编解码参数
  • 分辨率
  • 像素格式
  • 比特率
  • GOP 设置
  • B 帧数量
  • 线程数
  • extradata
  • 内部缓冲状态

如果类比面向对象,可以粗略理解为:

  • AVCodec 像类定义
  • AVCodecContext 像类实例

真正 send/receive 时,主要操作的就是 context。


3.4 avcodec_send_packet / avcodec_receive_frame

现代 FFmpeg 推荐的解码接口是:

  • avcodec_send_packet()
  • avcodec_receive_frame()

它的核心思想是:

输入和输出分离,允许内部缓存。

一个最小解码主线大致像这样:

1
2
3
4
5
6
7
8
9
10
while (av_read_frame(fmt_ctx, &pkt) >= 0) {
if (pkt.stream_index == video_stream_index) {
avcodec_send_packet(dec_ctx, &pkt);

while (avcodec_receive_frame(dec_ctx, frame) == 0) {
// 使用解码后的 frame
}
}
av_packet_unref(&pkt);
}

为什么要设计成 send/receive 两步?

因为解码器内部不一定是“送一个包,立刻出一帧”。

原因包括:

  • B 帧重排序
  • 参考帧等待
  • 输入 packet 不完整
  • 解码器内部缓存机制

所以 send/receive 的模型,本质上比过去的“单次调用即完成全部工作”的接口更贴近真实编解码过程。


3.5 为什么 packet 不等于 frame

这件事值得单独强调一次。

原因 1:压缩层和显示层不是一个粒度

packet 是压缩层的数据组织单位。
frame 是解码后的图像单位。

它们不是同一个层次的概念。

原因 2:编码器/解码器可能缓存数据

尤其是有 B 帧时,显示顺序和编码顺序本来就可能不同。

所以:

  • 你送进去的 packet,未必马上对应输出 frame
  • 你拿到的 frame,也未必对应刚送进去的那个 packet

原因 3:封装层还会进一步影响 packet 组织

不同容器、不同流协议、不同 parser 行为,都会让 packet 和“自然的一帧”之间的关系变得没那么直接。

所以做工程时,别把 packet 想成“压缩版 frame”,那样很容易在时间戳、缓存和 parser 上踩坑。


4. 编码主线:从 AVFrame 到 AVPacket

如果说解码是“压缩码流还原成原始图像”,那编码就是反过来的过程。


4.1 avcodec_send_frame / avcodec_receive_packet

现代 FFmpeg 推荐的编码接口是:

  • avcodec_send_frame()
  • avcodec_receive_packet()

一个最小编码主线可以抽象成这样:

1
2
3
4
5
6
avcodec_send_frame(enc_ctx, frame);

while (avcodec_receive_packet(enc_ctx, &pkt) == 0) {
// 写出编码后的 packet
av_packet_unref(&pkt);
}

和解码一样,它也不是简单的一进一出。

编码器内部可能需要:

  • 做 lookahead
  • 等待更多参考帧
  • 执行 B 帧重排序
  • 控制码率波动
  • 积累足够数据后再输出

所以编码天然就带缓存特性。


4.2 编码延迟是怎么来的

很多人第一次接触编码接口时会困惑:

为什么我送进去一帧,编码器没立刻吐一个 packet?

因为编码器不只是“压缩当前帧”,它还要做全局上的决策。

例如:

  • 这一帧是不是该做 I 帧
  • 后面几帧的运动情况如何
  • B 帧要插在哪里
  • 当前码率预算够不够

这些都可能让编码器先缓存若干帧,再决定怎么输出。

所以编码延迟的来源,本质上包括:

  • B 帧带来的重排序
  • lookahead 机制
  • 码率控制策略
  • 编码器内部缓冲

这也是为什么“低延迟编码”通常要主动关掉一些高压缩效率能力。


4.3 为什么 B 帧会影响输出时序

B 帧会同时参考前后帧。

这意味着编码器想正确生成某个 B 帧,往往需要先看到后面的参考帧。

结果就是:

  • 输入顺序
  • 编码顺序
  • 解码顺序
  • 显示顺序

这几个顺序可能并不相同。

这也是很多时间戳问题的根源之一。

在 FFmpeg 里,如果你不理解 B 帧重排序,很容易在这些地方出问题:

  • pts / dts 弄混
  • mux 后播放器时序异常
  • 低延迟场景莫名其妙多出缓存
  • 以为编码器“卡住了”

4.4 为什么编码器可能“吃进去一帧,不立刻吐出一包”

这件事其实就是编码器缓存特性的直观表现。

工程上你应该默认:

编码器不是纯函数,而是带状态机和缓冲区的模块。

因此在编码结束时,还必须记得 flush。

典型做法是:

1
2
3
4
avcodec_send_frame(enc_ctx, NULL);
while (avcodec_receive_packet(enc_ctx, &pkt) == 0) {
// 把剩余 packet 全部取出
}

如果不 flush,最后几帧可能根本出不来。


5. parser、extradata 和 codec id 是怎么把码流接起来的

这一节是工程里特别容易被忽略,但又非常关键的一层。


5.1 demux 出来的东西为什么不一定能直接送解码器

很多人觉得:

av_read_frame() 读出来 packet,直接送 avcodec_send_packet() 不就好了?

很多时候可以,但不是永远都行。

因为 demux 出来的 packet,取决于:

  • 容器格式
  • 封装方式
  • 码流组织形式
  • 是否带完整参数集
  • 是否已经对齐成 access unit

举个典型例子:

  • MP4 里的 H.264,常常是 AVCC 格式
  • 裸流或 TS 里更常见的是 Annex B

如果解码器期待的数据组织方式和你当前拿到的不一致,就需要 parser 或 bitstream filter 来帮你转换。


5.2 parser 的作用

parser 的作用可以粗略理解为:

在压缩码流和解码器之间,做进一步整理和切分。

它常见的职责包括:

  • 边界对齐
  • 提取帧级信息
  • 补充必要的解析结构
  • 帮助解码器正确识别 access unit

所以 parser 不是“解码器的一部分”,但它常常是码流真正稳定进入解码器前的重要中间层。


5.3 extradata 的作用

extradata 是 FFmpeg 里一个非常重要的概念。

它通常保存 codec 初始化所需的额外参数信息,比如:

  • H.264 的 SPS / PPS
  • H.265 的 VPS / SPS / PPS
  • 某些音频 codec 的 header 信息

为什么要有它?

因为解码器启动时,必须先知道:

  • 分辨率
  • profile / level
  • 编码参数
  • 参考结构的一些基础信息

这些信息并不一定每个 packet 都重复带一份,所以经常会被提取到 AVCodecContextextradata 里。


5.4 SPS/PPS/VPS 在 FFmpeg 里常常如何保存

这几个参数集在工程里常见有两种存在方式:

方式 1:跟在码流里

比如 Annex B 裸流中,SPS/PPS/VPS 可能直接以内联 NALU 的方式出现在数据流里。

方式 2:放在 extradata 里

比如 MP4 这类容器里,常常把参数集提到容器头部或 codec extradata 中,而不是每次都直接写进 packet 数据主体。

这就是为什么你经常会遇到这种问题:

  • 同样是 H.264,MP4 里的 packet 和裸流里的 packet 长得不一样
  • 同样是 hevc,写入 FLV / RTMP 前经常还得做额外处理

说到底,问题不在于“是不是 H.264/H.265”,而在于:

码流组织方式是否和上下游预期一致。


6. filter 为什么一般处理的是 frame,而不是 packet

这件事其实很好理解。

6.1 packet 是压缩域数据

packet 里的内容,还是编码后的码流。

在这个层面,你拿到的不是清晰的像素矩阵,而是:

  • NALU
  • 熵编码结果
  • 预测残差组织
  • 参数集和切片信息

这层数据更适合:

  • 存储
  • 传输
  • 封装
  • 解封装

但不适合直接做大多数图像处理。

6.2 frame 是像素域数据

frame 里才是真正的图像像素。

所以像这些操作:

  • 缩放
  • 裁剪
  • 旋转
  • 叠字
  • 水印
  • 色彩空间转换
  • AI 检测
  • 预处理

都天然更适合在 frame 层做。

因此 FFmpeg 里的 filter graph,绝大多数时候处理的都是 AVFrame

6.3 为什么也存在“压缩域处理”但不常见

理论上,某些处理也可以在压缩域做,比如:

  • 某些码流级分析
  • 参数集修改
  • bitstream 层面重写

但这些通常不是通用滤镜干的事,而更接近:

  • bitstream filter
  • 特殊码流处理模块
  • 封装前后适配逻辑

所以你在工程里默认理解成:

packet 更偏传输与封装,frame 更偏内容处理。

这个模型通常是对的。


7. 常见 H.264/H.265 编码器在 FFmpeg 中的对应关系

理解 FFmpeg 的工程语境,还得知道常见编码器实现都是什么角色。

7.1 libx264

这是最经典的 H.264 软件编码器接口。

特点通常是:

  • 压缩效率高
  • 参数丰富
  • 社区成熟
  • CPU 开销相对大

很多“高质量 H.264 编码”讨论,绕不开它。

7.2 libx265

这是 H.265 的主流软件编码器接口。

特点通常是:

  • 压缩效率更强
  • 编码复杂度更高
  • 参数体系更复杂
  • CPU 压力更明显

7.3 h264_nvenc / hevc_nvenc

这是 NVIDIA 硬件编码路线。

优点通常是:

  • 编码速度快
  • CPU 压力小
  • 适合实时场景

但也常见这些 trade-off:

  • 参数能力和软件编码器不同
  • 某些场景下主观质量未必最优
  • 对显卡和驱动环境有依赖

7.4 h264_qsv / hevc_qsv

这是 Intel Quick Sync Video 路线。

它的核心价值也是:

  • 利用硬件加速
  • 降低 CPU 占用
  • 提升实时编码能力

7.5 VAAPI 路线

这是 Linux / Intel / AMD 常见的一类硬件加速接口路线。

在部署时,它经常和这些问题绑在一起:

  • 驱动兼容性
  • 设备节点权限
  • 像素格式限制
  • 零拷贝路径是否打通

所以硬编硬解从来不只是“更快”,它还涉及:

  • 数据在 CPU / GPU 间怎么流
  • 像素格式是否匹配
  • 是否需要额外 upload / download
  • 设备上下文怎么管理

8. 工程里最常见的几个坑

这一节很值钱,因为很多问题不是“不会 API”,而是工程理解不到位。


8.1 时间戳问题

这是最常见也最烦的一类坑。

典型表现包括:

  • 播放卡顿
  • 音画不同步
  • mux 后文件异常
  • 推流端时序漂移

核心往往在于:

  • pts / dts 理解不清
  • time_base 换算错误
  • B 帧重排序没处理好
  • 输入和输出时钟体系不一致

8.2 像素格式不匹配

比如:

  • 解码出来是 yuvj420p
  • 编码器只接受 yuv420p
  • 硬件编码器要求 nv12
  • filter 输出和 encoder 输入不一致

这时如果不主动做格式转换,轻则报错,重则结果异常。

8.3 分辨率变化

有些输入流中途会变分辨率,比如:

  • 摄像头动态切换
  • 网络流源端重协商
  • 某些文件源中途参数变化

如果工程链路里默认“分辨率恒定”,那 context、buffer、filter graph 都可能出问题。

8.4 B 帧重排序

B 帧会影响:

  • 编码输出时机
  • 解码显示顺序
  • pts / dts 排布
  • flush 行为

如果你只按“送一帧出一包”的直觉写逻辑,基本迟早踩坑。

8.5 参数集缺失

比如 H.264/H.265 流里如果缺:

  • SPS
  • PPS
  • VPS

那解码器很可能根本起不来。

这在这些场景尤其常见:

  • 裸流截断
  • 推流首包不完整
  • 转封装时丢失参数集
  • Annex B / AVCC 转换没做好

8.6 Annex B 和 AVCC 混用

这是 H.264/H.265 工程中极常见的坑。

看起来都是同一个 codec,实际上:

  • 数据边界表达方式不一样
  • 参数集组织方式不一样
  • 某些 muxer / decoder 预期不一样

所以“明明是 H.264,为什么这个能播那个不能播”,很多时候问题就在这里。

8.7 解码器 / 编码器缓存刷新问题

如果没有正确 flush:

  • 最后几帧解码结果可能取不干净
  • 最后几个编码 packet 可能直接丢失

这是很多新手 demo 经常藏着的 bug。


9. 一个最小转码流程怎么理解

如果你想把这篇文章前面的内容全部串起来,一个最典型的例子就是最小转码流程。

flowchart LR
    A[输入文件] --> B[avformat_open_input]
    B --> C[av_read_frame]
    C --> D[AVPacket]
    D --> E[avcodec_send_packet]
    E --> F[avcodec_receive_frame]
    F --> G[AVFrame]
    G --> H[sws_scale / filter graph]
    H --> I[avcodec_send_frame]
    I --> J[avcodec_receive_packet]
    J --> K[AVPacket]
    K --> L[av_interleaved_write_frame]
    L --> M[输出文件]

如果把它翻译成人话,就是:

  1. 打开输入文件
  2. 解封装拿到压缩 packet
  3. 把 packet 送给解码器
  4. 从解码器拿出原始 frame
  5. 如果需要,对 frame 做处理
  6. 把处理后的 frame 送给编码器
  7. 从编码器拿出新的压缩 packet
  8. 把 packet 写进输出容器

这条链路看起来不复杂,但工程中的绝大多数问题,几乎都藏在这些节点之间:

  • packet 是否完整
  • frame 的像素格式是否匹配
  • 时间戳是否正确传递
  • 编码器是否缓存了数据
  • mux 侧是否接受当前码流组织方式

所以理解“链路之间的接口语义”,往往比死记 API 名字更重要。


10. 面试里怎么讲 FFmpeg 中的编解码链路

如果你面试音视频岗,被问到 FFmpeg 编解码主线,不建议只背 API。

你更应该把它讲成一条数据流转链路。

可以用下面这个思路。

10.1 什么是 packet

packet 是压缩域的数据组织单位,通常由 demux 模块从容器里读出来,里面携带码流数据和时间戳信息。

10.2 什么是 frame

frame 是解码后的原始媒体数据。对视频来说,通常是 YUV/RGB 像素数据,也是滤镜、渲染、AI 处理更常操作的对象。

10.3 编解码器上下文是干什么的

AVCodecContext 保存了编解码实例的配置和内部状态,比如分辨率、像素格式、码率、GOP、extradata 和内部缓存。

10.4 为什么 filter 处理 frame

因为 packet 还是压缩码流,内容没有展开。多数图像处理逻辑都必须建立在像素域数据上,所以 filter graph 通常工作在 frame 层。

10.5 为什么硬编硬解不只是“更快”

因为它还会涉及:

  • 硬件设备上下文管理
  • CPU/GPU 间数据搬运
  • 支持的像素格式限制
  • 零拷贝路径是否成立
  • 驱动与平台兼容性

所以硬件编解码是“性能 + 架构 + 兼容性”的综合问题,不只是速度问题。

如果你能把这些点讲顺,说明你已经不只是会调 API,而是真的理解这条链路在工程里怎么跑。


11. 总结:H.264/H.265 在 FFmpeg 里真正承上启下的位置

回到这篇文章最核心的问题:

H.264/H.265 在 FFmpeg 中,到底处于哪一层?

答案是:

它处在 codec 层,前面接 demux / 原始采集,后面接 filter / mux / 渲染 / 推流,是整条媒体处理链路里最关键的中间桥梁。

更具体一点说:

  • 在输入侧,它承接 demux 拿到的压缩 packet
  • 在解码侧,它把 packet 变成 frame
  • 在编码侧,它把 frame 压回 packet
  • 在输出侧,它把 packet 交给 mux 进入容器或网络协议

所以真正理解 FFmpeg,不只是会背几个模块名字,而是要理解:

压缩域和像素域如何切换,packet 和 frame 如何流动,codec 层如何把上下游串起来。

这也是为什么,一旦你真正理解 codec 层的位置,整个音视频工程感会明显提升。

因为你不再只是“会调用 FFmpeg API”,而是开始知道:

  • 数据从哪来
  • 在哪一层被改变形态
  • 为什么某些处理必须发生在 frame 层
  • 为什么某些坑总出现在 packet、时间戳和参数集上

这类理解,才是真正能支撑你继续往下走到:

  • RTSP / RTP
  • 时间戳同步
  • 硬编硬解
  • 零拷贝
  • 直播链路
  • 车载视频链路

这些更复杂工程主题的基础。


后记

如果说前两篇主要是在建立“编码世界观”和“编解码流程理解”,那么这一篇的作用,就是把这些知识拉回工程现场。

因为真正做项目时,最难的从来不是“知道 H.264 有哪些 NALU 类型”,而是:

  • 知道一段码流现在处于哪一层
  • 知道它该进哪个模块
  • 知道它为什么在这里出问题
  • 知道怎么把上下游重新接顺

这也是从“学知识”走向“做工程”的关键一步。


从 AVPacket 到 AVFrame,H.264/H.265 在 FFmpeg 中是如何承上启下的
https://breaker505.github.io/2026/04/01/h264-h265-in-ffmpeg/
作者
爱发呆的鱼
发布于
2026年4月1日
许可协议