裸流读回来要自己报尺寸;而且每帧都被标成了 I 帧,不清掉体积要大三倍。 帧率也得自己给——猜错时长就变。
5.2.1 把画面存成了 .yuv,这一节把它编回 H.264。 这一步会撞上三个「文件里本来应该有、但这个文件没有」的东西。
一、三个参数,少给与给错
裸流没有头,三个参数得自己填:
参数 作用 不给会怎样
video_size 每帧切多少字节 直接报错 pix_fmt 三个平面怎么排 有默认值,但可能猜错 framerate 时间轴怎么解释 有默认值,猜错就错
第一行是唯一「不给就死」的,后两行有默认值。 这意味着「少给参数」经常不报错,只是结果悄悄变了——这比报错更难查。
还有一种更隐蔽的情况:尺寸给错。这时打开文件是成功的, 要到解码中途才发现字节切歪了,报错信息也不会提「尺寸」两个字。
二、裸流的帧全被标成了 I 帧
这是本节最值得记住的一条:
做法 字节 关键帧数
照原样编 14191 50 先清掉帧类型标记 4478 1
rawvideo 解出来的每一帧都带着「我是 I 帧」的标记,编码器就老老实实按 I 帧编。 于是一帧都不做帧间预测,体积大三倍,而且这个错误不会报任何错。
命令行不用操心这件事,ffmpeg 的 rawvideo 输入会把标记抹平。
自己写代码时,喂帧之前先确认帧类型标记是不是 None。
三、帧率是「解释方式」,不是数据
同一份像素,按不同帧率解释:
按几帧算 帧数 时长
25 fps 50 2.0 秒 50 fps 50 1.0 秒
帧数一样、内容一样,只有「每秒放几帧」这个解释变了,时长就翻了一倍。 裸流最典型的错误就是这样产生的:内容完全正确,时间轴全错。
所以处理裸流的第一件事永远是三个问题:多宽多高、什么格式、多少帧。 问清楚了再动手,比事后对齐时间轴省事得多。
四、动手 Demo:读裸流、清帧类型、再编回 H.264
先按正确参数读 o.yuv,再故意不给参数、给错尺寸各试一次。 然后用「照原样编」与「清掉帧类型再编」对比,看关键帧数与字节的差距。
import av, os
opts = {u"video_size": u"160x90", u"pix_fmt": u"yuv420p",
u"framerate": u"25"}
PT = av.video.frame.PictureType
def enc(dst, clear):
c1 = av.open(dst, u"w")
s1 = c1.add_stream(u"libx264", rate=25)
s1.width, s1.height, s1.pix_fmt = 160, 90, u"yuv420p"
s1.options = {u"crf": u"26"}
c0 = av.open(u"o.yuv", u"r", format=u"rawvideo", options=opts)
for f in c0.decode(video=0):
if clear:
f.pict_type = PT.NONE
for o in s1.encode(f):
c1.mux(o)
for o in s1.encode(None):
c1.mux(o)
c1.close()
c0.close()
enc(u"raw_asis.mp4", False)
enc(u"raw_clear.mp4", True)
for tag in (u"asis", u"clear"):
p = u"raw_%s.mp4" % tag
c = av.open(p)
k = sum(1 for pk in c.demux(video=0) if pk.size and pk.is_keyframe)
c.close()
print(u"%s %d 字节 关键帧 %d" % (tag, os.path.getsize(p), k))
实跑输出:
--------------------------------------------------------------------
5.4.1 YUV 编码为 H.264——裸流要自己报尺寸
--------------------------------------------------------------------
**① 三个参数,少给和给错的后果不一样**
给的参数 结果 帧数 尺寸
-------------------------------------------------------------
一个都不给 报错(returned 22) — —
只给 video_size 成功 50 160x90
尺寸给成 320x180 报错(returned 22) — —
三个都给 成功 50 160x90
**第二行说明 pix_fmt 与帧率是有默认值的,少给两个也能跑;**
**第三行才是真正的坑:尺寸给错时报错不在打开文件那一刻,而是解码到一半才炸。**
**因为打开时只检查「参数是否合法」,只有真按这个尺寸切字节,才发现切歪了。**
**② 裸流读回来的帧,全被标成了 I 帧**
做法 字节 关键帧数
----------------------------------------
照原样编 14191 50
先清掉帧类型标记 4478 1
**第一行的 50 帧全成了关键帧:rawvideo 解出来的每帧都带着「我是 I 帧」的标记,**
**编码器就老老实实按 I 帧编,一帧都不做帧间预测,体积自然大三倍。**
**清掉标记之后只剩 1 个关键帧,字节掉到 4478——这才是正常的 H.264 结构。**
**命令行不需要操心这件事,`ffmpeg` 的 rawvideo 输入会把标记抹平;写代码时要自己记得。**
**③ 清掉标记后,三档 CRF 各编一遍**
CRF 字节
-------------
20 6288
26 4478
32 3981
**三档从 6288 到 3981,跨度只有 1.6 倍,比想象中平。**
**因为这段合成画面本来就简单,编码器没什么可省的;换成实拍素材,跨度会大得多。**
**但顺序是对的:CRF 越小字节越多。看到「从裸流编出来异常大」,先查帧类型标记。**
**④ 帧率是「猜的」,猜错时长就变**
按几帧算 帧数 字节 时长
--------------------------------------
25 fps 50 4478 2.0 秒
50 fps 50 4217 1.0 秒
**同一份像素,按 25 算就是 2 秒,按 50 算就是 1 秒——帧率不是数据的一部分,是解释方式。**
**字节也跟着变了:码率是按秒算的,同样的帧放进更短的时长,每秒要花更多比特。**
PT.NONE 就是「别规定帧类型,让编码器自己决定」。
清掉之后 x264 才能按内容安排 I、P、B。
关键帧用 desmux 出来的包标记数,不需要解码,快得多。 判断「这个文件能不能快速拖动」,看这个数就够了。
读裸流的 options 里,framerate 必须写成字符串,不能写整数。
选项表是按字符串传的,写整数会被忽略,然后你会得到一个时长错误的文件。
30 秒实验
- 命令行对照:
ffmpeg -f rawvideo -pixel_format yuv420p -video_size 160x90 -framerate 25 -i 出.yuv -c:v libx264 出.mp4。 - 不给尺寸:把
-video_size去掉,看它报什么。 - 改帧率:
-framerate 50,对比时长。
常见误区
- 以为裸流能自动识别。它连一帧多长都得算,不可能自动。
- 忘了清帧类型标记。体积大三倍,一条警告都没有。
- 把 framerate 写成整数。选项表要字符串,整数会被静默忽略。
自测三问(点开对答案)
1. 读裸流必须给的参数是哪个? —— video_size。pix_fmt 与 framerate 有默认值,缺了不报错但结果会变。 2. 为什么裸流编出来的文件会大三倍? —— rawvideo 的每帧都被标成 I 帧,编码器不做帧间预测,50 帧全成了关键帧。 3. 帧率为什么能改时长? —— 帧率是解释方式,不是数据。同样的 50 帧按 25 算 2 秒,按 50 算 1 秒。- ☐ (本节完,下一节 5.4.2 MP4 格式转码为 FLV 格式)