🎬 第五章 FFmpeg命令行实现音视频转码
📄 一句话概括

裸流读回来要自己报尺寸;而且每帧都被标成了 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 秒实验

  1. 命令行对照:ffmpeg -f rawvideo -pixel_format yuv420p -video_size 160x90 -framerate 25 -i 出.yuv -c:v libx264 出.mp4。
  2. 不给尺寸:把 -video_size 去掉,看它报什么。
  3. 改帧率:-framerate 50,对比时长。

常见误区

  1. 以为裸流能自动识别。它连一帧多长都得算,不可能自动。
  2. 忘了清帧类型标记。体积大三倍,一条警告都没有。
  3. 把 framerate 写成整数。选项表要字符串,整数会被静默忽略。
自测三问(点开对答案) 1. 读裸流必须给的参数是哪个? —— video_size。pix_fmt 与 framerate 有默认值,缺了不报错但结果会变。 2. 为什么裸流编出来的文件会大三倍? —— rawvideo 的每帧都被标成 I 帧,编码器不做帧间预测,50 帧全成了关键帧。 3. 帧率为什么能改时长? —— 帧率是解释方式,不是数据。同样的 50 帧按 25 算 2 秒,按 50 算 1 秒。

相关笔记