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

裸存成 .yuv,字节数完全由「宽×高×帧数」决定,一个字节都不多。 代价是:这种文件里没有任何说明,别人拿到打不开。

📝 本节和前面几节的关系

上一节讲的是「压缩」,这一节反着来——把画面解压成最原始的像素。 后面做算法、做分析、喂给别的库,走的都是这条路。

一、为什么先讲 YUV 不讲 RGB

视频解码出来的第一站就是 YUV,转 RGB 是后面的事。 所以「提取原始数据」这个需求,天然应该先拿到 YUV,需要了再转。

平面 存什么 尺寸


Y 亮度 160×90 U 蓝色差 80×45 V 红色差 80×45

第三列正是 420 的含义:色度只有一半宽高,也就是四分之一的样本数。 合起来每像素 1.5 字节,是 RGB 的一半。

所以「先拿 YUV 再按需处理」不只是习惯问题:省一半内存,也省一半拷贝。

二、字节数可以手算

无压缩的意思就是:没有任何一个字节是「根据内容」变的。

项 公式 值


宽×高 160×90 14400 单帧字节 宽×高×1.5 21600 50 帧合计 单帧×帧数 1080000 实测文件 — 1080000

第三行与第四行分毫不差。记住这个手感:以后拿到一个 .yuv 文件, 除以「单帧字节」就知道里面有几帧,不需要任何头信息。

反过来说,如果除不尽,那就是三件事之一:尺寸猜错了、格式猜错了、文件被截断了。

三、没有头信息的三个麻烦

.yuv 里没有宽、没有高、没有帧率、没有像素格式,全靠你自己记:

场景 结果 原因


ffplay 加参数 能播 参数里补上了尺寸与格式 普通播放器双击 打不开 没有任何可读的说明 写脚本按偏移切帧 能读 你自己算的偏移

第二行是最常见的坑:文件明明是好文件,只是没人告诉播放器它多宽多高。 命令行里对应 -s 160x90 -pix_fmt yuv420p,少一个都放不出来。

还有一个隐蔽的坑:解码器有内部缓冲。取完所有包还得再 flush 一次, 否则最后几帧永远拿不到——本次实跑第一遍就少了 2 帧,数对不上才发现。

四、动手 Demo:把画面裸存成 .yuv,再对一遍字节数

用 rawvideo 封装器把每一帧原样写出去,得到一个没有任何头信息的 .yuv 文件。 然后用手算的结果和实测的文件大小对一遍,看看差多少。

import av, os

c1 = av.open(u"o.yuv", u"w", format=u"rawvideo")
s1 = c1.add_stream(u"rawvideo", rate=25)
s1.width, s1.height, s1.pix_fmt = 160, 90, u"yuv420p"
c0 = av.open(u"s5.mp4")
cc = c0.streams.video[0].codec_context
n = 0
for pk in c0.demux(video=0):
    if not pk.size:
        continue
    for f in cc.decode(pk):
        n += 1
        fr = f.reformat(width=160, height=90, format=u"yuv420p")
        for o in s1.encode(fr):
            c1.mux(o)
for f in cc.decode(None):
    n += 1
    fr = f.reformat(width=160, height=90, format=u"yuv420p")
    for o in s1.encode(fr):
        c1.mux(o)
for o in s1.encode(None):
    c1.mux(o)
c1.close()
c0.close()

per = 160 * 90 * 3 // 2
print(u"帧数 %d" % n)
print(u"单帧 %d  应占 %d  实测 %d"
      % (per, per * n, os.path.getsize(u"o.yuv")))

实跑输出:

--------------------------------------------------------------------
5.2.1 提取视频的 YUV 像素数据
--------------------------------------------------------------------

**① 裸存成 .yuv,字节数可以手算**

  项            公式          值
  ------------------------------------
  宽×高         160×90        14400
  单帧字节      宽×高×1.5     21600
  50 帧合计     单帧×帧数     1080000
  实测文件      —             1080000

**第二行乘第三行正好等于第四行——YUV 是「无压缩」的,存多少字节只由尺寸与帧数决定。**
**这就是「原始数据」的含义:没有压缩,也就没有「画面复杂度」这个变量。**
**顺便记一笔:解码器有内部缓冲,取完包还得再 flush 一次才吐得出最后两帧——**
**本次实跑第一遍只拿到 48 帧,补上 flush 才是 50 帧。**

**② 这种文件谁能打开**

  播放器             能不能播
  ----------------------------------------------
  ffplay 加参数      能,但必须告诉它尺寸与格式
  普通播放器双击     不能,没有任何头信息
  自己写脚本         能,按字节偏移切帧

**第二行是重点:`.yuv` 文件里没有宽、没有高、没有帧率,连「一帧多长」都得你自己算。**
**所以这种格式只在「自己写的程序之间」流通,不适合发给别人。**

format=u"rawvideo" 指定的是「封装器」,里面的编码也是 rawvideo。 本质上就是「什么包装都不要,字节原样倒进文件」。

f.reformat(...) 不能省:解码出来的帧可能带着对齐填充, 直接写会把填充字节也写进去,字节数就对不上了。

最后两行如果对不上,先查帧数,再查尺寸,最后才怀疑代码。 这类「无头文件」的调试顺序永远是:数量、尺寸、格式,三步定死。

30 秒实验

  1. 自己看:ffplay -f rawvideo -pixel_format yuv420p -video_size 160x90 文件.yuv。
  2. 反推帧数:文件字节 ÷ (宽×高×1.5),得到的商就是帧数。
  3. 少给一个参数:故意把 -video_size 去掉,看 ffplay 报什么错。

常见误区

  1. 把 .yuv 当成能双击播放的格式。它没有头,播放器不知道尺寸与格式。
  2. 忘了 flush 解码器。最后几帧会被内部缓冲扣住,帧数总是少两三帧。
  3. 用错像素格式算字节。按 420 算出来的尺寸去读 444 的文件,画面会斜着错位。
自测三问(点开对答案) 1. 160×90 的 yuv420p 单帧多少字节? —— 21600 字节。宽乘高乘 1.5,也就是亮度平面 14400 加两个色度平面各 3600。 2. 为什么 .yuv 文件普通播放器打不开? —— 文件里没有任何头信息,播放器不知道宽高、像素格式和帧率,无法解析。 3. 解码器和编码器为什么都要 flush? —— 它们内部有缓冲:解码器为处理 B 帧重排而压帧,编码器也要收尾。不 flush 就会丢尾部的帧。

相关笔记