🎬 第八章 AVCodec编解码层理论及案例实战
📄 一句话概括

编解码是「压缩」与「解开」,方向的差别决定了上下文的两副面孔。 557 个编解码器里挑一个,靠的是名字后缀与方向,不是猜。

📝 本节和前面几节的关系

第七章管箱子,这一章管箱子里的货怎么压、怎么解。 8.2 解码、8.3 编码、8.4 时间基,三节各攻一面。

一、压缩与解开

两个方向各有一套对象,不能混用。 方向 进 出 难在哪


解码 压缩包 像素帧 不知道参数,要从码流读 编码 像素帧 压缩包 参数全得你给定

这一行「难在哪」是本章的分水岭。 解码时参数是别人给的,你只要读; 编码时参数得你给,一个不给自己就报错。 所以解码代码短、编码代码长,不是因为编码复杂,是因为责任在谁那边不一样。

二、557 个里怎么挑

先按编码名筛,再按后缀挑实现。 后缀 谁家的 特点


(没有) FFmpeg 自带 软解,最通用 _cuvid N 卡 硬解,要驱动 _qsv Intel 核显 硬解硬编 _nvenc N 卡 硬编,快但画质略差 libx264 开源库 软编,画质标杆

本机 h264 有九套实现:三套硬解、四套编码、两套软解。 挑哪套取决于「有没有那块硬件」与「要速度还是要画质」。 硬编硬解不一定更好:省 CPU,但同码率下画质通常不如 libx264。 直播这种要实时的地方才非它不可。 名字写错是最常见的入门坑:h264 只能解不能编,想编得写 libx264 或 h264_nvenc。

三、上下文打开前后

刚创建的时候,上下文里什么都没有。 时刻 宽高 像素格式 为什么


刚创建 0 None 还没读到任何信息 打开之后 有 有 解码器从码流读到了

解码方向是「打开之后才知道」:宽高、像素格式、档位、级别,全是从码流里读出来的,不是你设的。 编码方向反过来:这些都得你先设好,它才肯打开。同一个类,两副面孔,就看方向。 所以看到宽是 0、格式是 None,不是对象坏了,是还没喂给它数据。

四、解码器上读不到的那几样

这三样都是「编码时你让它怎么编」的设定。 属性 解码侧 为什么


time_base 抛异常 解码不需要另定刻度 gop_size 抛异常 关键帧间隔是编码决定的 max_b_frames 读得到 0 但 0 表示没人设过

前两样读的时候直接抛 RuntimeError,不是返回 0 或 None。 这是一个很有用的设计:它逼你意识到自己问错了对象。 第三样能读但值是 0,这个 0 不代表「没有 B 帧」,只代表「这个字段没被设定过」。 这种「假值」比报错更坑。

五、六个 API 的对应关系

C 里要一步步写,PyAV 里有两步被藏了。 C 的 API PyAV 里


avcodec_find_decoder CodecContext.create avcodec_alloc_context3 同上一句里做 avcodec_open2 codec_context.open() avcodec_send_packet decode(packet) avcodec_receive_frame 同上返回的列表 送 NULL 冲空包 decode(None)

C 里的 send 与 receive 是两个函数,PyAV 合成一个 decode,返回一个帧列表。 这层包装很省事,但也藏掉了「可能返回 0 帧」这件事。 所以千万别写成「送一个包必得一帧」:返回空列表是正常现象,代表解码器还在攒。 冲空包那一步两边都得做,不做就丢最后几帧。这一条在 8.2 会实测。

六、动手 Demo:557 个编解码器,怎么找到你要的那个

先数编解码器,再挑出 h264 家族,最后看上下文打开前后差多少。

import av

print(len(av.codecs_available))
print([n for n in sorted(av.codecs_available) if "264" in n])

dec = av.Codec("h264", "r")        # 解码
enc = av.Codec("libx264", "w")     # 编码
print(dec.name, enc.name)

c = av.open("tiny.mp4")
cc = c.streams.video[0].codec_context
print(cc.width, cc.height, cc.pix_fmt, cc.profile, cc.level)
print(cc.framerate)
try:
    print(cc.time_base)
except Exception as e:
    print("读不到", type(e).__name__)
c.close()

实跑输出:

--------------------------------------------------------------------
557 个编解码器,怎么找到你要的那个
--------------------------------------------------------------------

**① h264 这一个编码,本机有九套实现**

总共有 **557 个编解码器**,名字里带 264 的 **9 个**:

  名字             干什么
  -----------------------------------------
  h264             官方解码器(软解)
  h264_amf         AMD 编码
  h264_cuvid       N 卡解码
  h264_d3d12va     DX12 硬解
  h264_mf          系统媒体框架
  h264_nvenc       N 卡编码
  h264_qsv         Intel 核显,编解都有
  libx264          x264 软件编码
  libx264rgb       x264,保留 RGB

**同一个 h264,九套实现各管一段。** 软解一套、硬解三套、编码四套(含两个软件编码)。
**名字后面的后缀就是厂商名。**

**② 解码器与编码器是两个对象,不能混用**

  取法                      拿到的      类型
  ----------------------------------------------
  Codec("h264", "r")        h264        解码
  Codec("libx264", "w")     libx264     编码

**`h264` 按写方向去取会失败**,因为它只有解码器;`libx264` 反过来也只有编码器。
**方向写错,第一步就走不下去。**

**③ 上下文打开前后差多少**

  时刻         宽      像素格式
  ---------------------------------
  刚创建       0       None
  打开之后     160     yuv420p

**刚创建的时候一片空白:宽 0、像素格式 None。** 这些参数要等「打开」或「读到头」才填上。

读出来是 **160×120、yuv420p**,档位 **Constrained Baseline**、级别 **11**、帧率 **25**。

**档位与级别是解码器从码流里读出来的**,不是我们设的。
**这也是「同一个 h264 文件到处都能解」的原因**:参数都在码流里。

**④ 解码器身上有两样东西读不到**

  属性             解码器上
  ------------------------------------
  time_base        抛 RuntimeError
  gop_size         抛 RuntimeError
  max_b_frames     0

**前两样是「编码时你让它怎么编」的设定,解码侧当然没有。** 读它们会当场抛 RuntimeError,
不是返回 0。

**第三样能读,值是 0**,但那也是「没人设过」的意思,别当成「有 0 个 B 帧」。

**所以「上下文」这一个类,解码与编码两副面孔。** 写代码时先想清楚自己是哪一边。

h264 只能解、libx264 只能编,方向写错第一步就断。 cc.time_base 在解码器上直接抛 RuntimeError,不是返回 0。

30 秒实验

  1. 把 av.Codec("h264", "w") 跑一遍,看它报什么。
  2. 挑 h264_nvenc 建一个编码上下文,看本机能不能打开(提示:没显卡会失败)。
  3. 打印 av.codec.codecs_available 里名字含 aac 的那些。

常见误区

  1. 拿 h264 当编码器用。 它只有解码器,编码要写 libx264。
  2. 以为上下文一创建就有宽高。 解码侧要等打开之后。
  3. 把解码器上读到的 0 当真值。 max_b_frames 那个 0 是「没设过」。
自测三问(点开对答案) 1. 解码与编码最大的区别在哪? —— 解码的参数从码流里读,编码的参数得你自己设。 2. `h264` 与 `libx264` 有什么区别? —— 前者是 FFmpeg 自带的解码器,后者是 x264 软件编码器,方向不同。 3. 为什么解码器上读 `time_base` 会抛异常? —— 解码方向不需要另定刻度,时间戳跟着流的 time_base 走。

相关笔记