送一个包,收一遍帧,能收多少收多少——这是唯一正确的形状。 收不到帧不是错误,是解码器还在攒。
8.1 讲对象与 API,这一节把它们串成一次解码。 下一篇 8.3 是反方向:把帧压成包。
一、一条循环的形状
四行代码,但每一行都有讲究。 步 做什么 易错点
一 送包进去 空包要跳过 二 收帧,可能 0 个 别写成必得一个 三 处理帧 帧用完要释放 四 冲空包 不做就丢尾巴
第二步那个「可能 0 个」是新手的头号困惑。 送进去一个包却什么都不出来,很容易以为写错了。 原因在解码器内部:它要等到「能解出一帧」的条件成立才吐。 B 帧、缓冲、还没见到关键帧,都会让它先不说话。
二、为什么送包不一定出帧
三种情况,三种表现。 情况 表现 什么时候好转
有 B 帧 攒几帧才出 后面的参考帧到了 内部缓冲 开头几包不出 缓冲填满 还没见关键帧 一帧都不出 等到下一个关键帧
第三种最要命:从文件中间开始解,前面没有关键帧,解码器就一直不出帧。 所以跳转之后要先找到关键帧。 这也是「拖进度条之后画面花一下」的原因:从关键帧开始解,前面那几帧要靠后面的数据补齐。
三、帧上有什么
帧是解开的像素,不是压缩的字节。 属性 含义 用途
width/height 画面尺寸 检查对不对 format 像素格式 决定怎么读像素 pts 显示时刻 排输出顺序 planes 若干块平面 真正存数据的地方 pict_type 帧类型 I/P/B
planes 是 yuv420p 的关键:它不是 RGB 三个通道,而是 Y、U、V 三块分开的平面,
尺寸还各不相同。
这跟「一张图片」的直觉差很远,第一次处理会很不习惯。
要 RGB 得显式 reformat,那会多一次转换开销。
四、冲空包这一步
不算可选项,是必做项。 做法 结果
不冲 丢最后几帧 送一个空包 把存货全放出来
PyAV 里就是 decode(None),C 里是 send_packet(NULL)。
demo 里这次冲出来 0 帧,因为这批数据没有 B 帧、也没攒货。换个有 B 帧的视频就不一样了。
这一条特别容易在「小文件测试」时被忽略:数据少,丢几帧看不出来;
一上生产就是「最后半秒没了」。
五、资源要及时放
帧对象不小,占的是真实内存。 对象 一份多大 怎么放
1080p 的 yuv420p 帧 约 3 MB 用完立即放 4K 的 yuv420p 帧 约 12 MB 更要注意
一秒钟 25 帧就是 75 MB。 要是把帧都攒在列表里不释放,几秒钟就能把内存吃光。 常见错误是「先全解出来再统一处理」:看着代码清楚,实际是把流式问题变成了内存问题。 正确姿势是边解边处理边扔,一次只留你要的那几帧。这也正是「流式」两个字的本意。
六、动手 Demo:解码一次,送多少包收多少帧
一个包一个包送,一帧一帧收,最后冲一次空包。
import av
c = av.open("tiny.mp4")
st = c.streams.video[0]
sent = got = 0
for pk in c.demux(st):
if pk.size == 0:
break
sent += 1
try:
fr = st.codec_context.decode(pk)
except av.FFmpegError:
fr = []
got += len(fr)
flush = st.codec_context.decode(None)
got += len(flush)
c.close()
print("送", sent, "收", got, "冲空包", len(flush))
实跑输出:
--------------------------------------------------------------------
解码一次,送多少包收多少帧
--------------------------------------------------------------------
**① 一个一个包送进去,一帧一帧收回来**
送 **50 个包**,收 **50 帧**;最后冲空包又收 **0 帧**。
**这批数据是「一包一帧」的。** 但这不是定律——换个编码或换个参数,一个包可能出零帧、
也可能出好几帧。
**② 收上来的帧长什么样**
帧 pts 宽 高 像素格式
--------------------------------------------
0 0 160 120 yuv420p
1 512 160 120 yuv420p
2 1024 160 120 yuv420p
**帧是「解开的像素」,包是「压缩的字节」。** 一个帧对象里是三个平面(yuv420p 就是 Y、U、
V 各一块),不是常见的 RGB 三通道。
**③ 为什么送包不一定立刻出帧**
原因 现象
---------------------------------------------
B 帧 要等后面的帧到了才能解
缓冲区 解码器内部攒着几帧
还没收到关键帧 一帧都解不出来
**所以正确的写法是「送一个、收一遍,能收多少收多少」**,而不是「送一个、必须收一个」。
**④ 冲空包那一步不能省**
**解到文件末尾时,解码器里还存着几帧没吐出来。** 要送一个空包(`decode(None)`)告诉它「没了」,
它才会全放出来。
**这次冲出来 0 帧**——这批数据里解码器没攒货,所以是 0。**换个有 B 帧的视频,这里就会有货。**
这批数据是「一包一帧」,送 50 收 50,冲空包 0 帧。 换个有 B 帧的视频,冲空包就会有货。
30 秒实验
- 把
decode(pk)返回的列表长度打出来,看有没有 0 的情况。 - 把最后那个
decode(None)删掉,再统计帧数,看少不少。 - 用
st.codec_context.decode(pk)换成next(c.decode(st)),比较两种写法。
常见误区
- 写成「送一个包必得一帧」。 会少收很多帧。
- 忘了冲空包。 最后几帧永远出不来。
- 把所有帧攒在列表里。 一秒钟就是几十兆。
自测三问(点开对答案)
1. 送一个包却收到 0 帧,正常吗? —— 正常。解码器在攒,要等条件成立才吐帧。 2. 为什么必须冲空包? —— 解码器里还存着几帧,只有收到「没了」的信号才会全放出来。 3. yuv420p 的帧为什么有三块平面? —— Y、U、V 分开存,且 U、V 的尺寸是 Y 的四分之一。- ☐ (本节完,下一节 8.3 编码流程与案例实战)