编解码是「压缩」与「解开」,方向的差别决定了上下文的两副面孔。 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 秒实验
- 把
av.Codec("h264", "w")跑一遍,看它报什么。 - 挑
h264_nvenc建一个编码上下文,看本机能不能打开(提示:没显卡会失败)。 - 打印
av.codec.codecs_available里名字含aac的那些。
常见误区
- 拿
h264当编码器用。 它只有解码器,编码要写 libx264。 - 以为上下文一创建就有宽高。 解码侧要等打开之后。
- 把解码器上读到的 0 当真值。 max_b_frames 那个 0 是「没设过」。
自测三问(点开对答案)
1. 解码与编码最大的区别在哪? —— 解码的参数从码流里读,编码的参数得你自己设。 2. `h264` 与 `libx264` 有什么区别? —— 前者是 FFmpeg 自带的解码器,后者是 x264 软件编码器,方向不同。 3. 为什么解码器上读 `time_base` 会抛异常? —— 解码方向不需要另定刻度,时间戳跟着流的 time_base 走。- ☐ (本节完,下一节 8.2 解码流程与案例实战)