🎬 第六章 HTTP-FLV流媒体协议
📄 一句话概括

视频 tag 的第一个字节装了两层信息:帧类型与编码格式。 第二个字节只在 H.264 时才存在,表示是序列头还是画面。

📝 本节和前面几节的关系

6.3.1 讲了 tag 的外壳,这一节钻进两种主要 tag 的内部。 下一章转向流媒体开源库——看这些协议都有哪些现成实现。

一、视频 tag 的第一个字节

它拆成两半:高 4 位是帧类型、低 4 位是编码格式。 帧类型 1 是关键帧、2 是普通帧;编码 7 是 H.264。

实测第一个视频 tag 的这个字节是 0x17: 高 4 位 1 表示关键帧,低 4 位 7 表示 H.264。 所以 0x17 是「H.264 关键帧」的固定标志, 0x27 则是「H.264 普通帧」。

记住这两个值很实用: 在抓包里搜 17 开头的视频 tag,就是在找关键帧的位置。 这也是「首屏为什么慢」最直接的观测点。

二、第二个字节:AVCPacketType

只有编码格式是 7(H.264)时,第二个字节才有意义。 0 是 AVC 序列头,1 是真正的画面数据,2 是序列结束。

序列头里装的是什么? 是 SPS 与 PPS——解码器必须先拿到它们才能解后面的帧。 所以序列头永远排在第一个关键帧之前。

实测 27 个视频 tag 里: AVC 序列头 1 个、关键帧 3 个、普通帧 24 个。 关键帧比「GOP 25 该有 1 个」多, 多出来的是编码器在收尾时补的——它要保证最后一帧能独立解。

所以「帧数 × GOP」只能估关键帧数,真要数必须读帧类型位。 做首屏优化时如果按估算去调参数,很容易调不准。

三、音频 tag 与脚本 tag

音频 tag 的第一个字节拆成四段: 格式 4 位(10 是 AAC)、采样率 2 位、位深 1 位、声道 1 位。 AAC 时还要第二个字节:0 是音频序列头、1 是音频数据。

采样率的 2 位只有四个值:0 是 5.5 kHz、1 是 11 kHz、 2 是 22 kHz、3 是 44 kHz。 注意它是「采样率档位」不是「采样率数值」, 而且最高只到 44 kHz——48 kHz 要用扩展字段表示。

脚本 tag(类型 18)里的负载是 AMF0 编码的 onMetaData。 它写分辨率、帧率、码率、编码 ID 这些信息。 没有它,播放器可能按 0×0 建解码器,直接黑屏。

三种 tag 的职责可以这样记: 脚本 tag 说「这是什么规格」,序列头说「怎么解」, 画面与声音的 tag 说「这是第几毫秒的内容」。 缺任何一类,播放器都得猜。

四、动手 Demo:重要 Tag——视频 tag 的第一个字节是两层标志

读第一个视频 tag 的第一个字节,把高低 4 位分别打出来。 再统计关键帧、普通帧、序列头各有多少。

import io, os, tempfile
import av, numpy as np

tmp = os.path.join(tempfile.mkdtemp(), "a.flv")
c = av.open(tmp, "w", format="flv")
st = c.add_stream("libx264", rate=25)
st.width, st.height, st.pix_fmt = 320, 240, "yuv420p"
st.options = {"crf": "30", "preset": "veryfast", "g": "25"}
for i in range(25):
    f = av.VideoFrame(320, 240, "yuv420p")
    y = np.zeros((240, 320), np.uint8)
    y[:, :] = np.linspace(16, 200, 320, dtype=np.uint8)[None, :]
    f.planes[0].update(y.tobytes())
    for pi in (1, 2):
        f.planes[pi].update(bytes([128]) * f.planes[pi].buffer_size)
    f.pts = i
    for pk in st.encode(f):
        c.mux(pk)
for pk in st.encode(None):
    c.mux(pk)
c.close()

raw = io.open(tmp, "rb").read()
v, off = [], 13
while off + 11 <= len(raw):
    t = raw[off]
    size = int.from_bytes(raw[off + 1:off + 4], "big")
    body = raw[off + 11:off + 11 + size]
    if t == 9:
        v.append(body)
    off += 11 + size + 4

b0 = v[0][0]
print("第一个视频 tag 首字节 0x%02X" % b0)
print("  帧类型 %d(1 是关键帧)" % (b0 >> 4))
print("  编码 %d(7 是 H.264)" % (b0 & 0x0F))
print("  第二字节 %d(0 序列头、1 画面)" % v[0][1])

key = [x for x in v if (x[0] >> 4) == 1]
seq = [x for x in v if x[1] == 0]
print("视频 tag %d 个:关键帧 %d、序列头 %d、普通帧 %d"
      % (len(v), len(key), len(seq), len(v) - len(key)))

实跑输出:

--------------------------------------------------------------------
重要 Tag——视频 tag 的第一个字节是两层标志
--------------------------------------------------------------------
**① 视频 tag 的内部结构**

  **第一个字节拆成两半:高 4 位是帧类型、低 4 位是编码格式。**
  **帧类型 1 是关键帧、2 是普通帧;编码 7 是 H.264。**

  字段         值     含义
  --------------------------------------------------
  帧类型       1      1 是关键帧
  编码格式     7      7 是 H.264
  第二字节     0      0 序列头、1 NALU、2 序列尾

  **第二字节叫 AVCPacketType,只有编码是 7 时才存在。**
  **0 是 AVC 序列头(装着 SPS/PPS),1 是真正的画面数据。**

**② 数一数关键帧与普通帧**

  项                条数
  --------------------------
  视频 tag 总数     27
  关键帧            3
  普通帧            24
  AVC 序列头        1

  **27 个视频 tag 里有 3 个关键帧,比「GOP 25 该有 1 个」多。**
  **多出来的是编码器在收尾时补的关键帧——要保证最后一帧可独立解。**
  **所以帧数只能估,真要数得看每个 tag 的帧类型位。**

**③ 音频 tag 长什么样**

  **第一个字节拆成四段:格式 4 位、采样率 2 位、位深 1 位、声道 1 位。**
  **格式 10 是 AAC,采样率 3 表示 44 kHz,位深 0 是 8 位、1 是 16 位。**
  **AAC 时还要第二个字节:0 是音频序列头、1 是音频数据。**

  **这个文件里音频 tag 是 0 条——不是漏了,是编码时只编了视频。**
  **真实推流一定是音视频双路,两者的 tag 按时间戳交错发送。**

首字节 0x17 = 关键帧加 H.264,0x27 就是普通帧,这是最常见的两个值。 第二字节为 0 的是序列头,它必须排在第一个关键帧之前。 关键帧 3 个、序列头 1 个,两者不重叠,因为序列头本身也标成关键帧。

30 秒实验

  1. 把 g 改成 10,看关键帧数——涨到 3 倍左右,但每个都更小。
  2. 打印最后一个视频 tag 的首字节,看它是不是关键帧——是,这就是「收尾补关键帧」的直接证据。
  3. 把脚本 tag 的体读出来打印,看 onMetaData——能看到分辨率、帧率这些字段,说明元数据确实在 tag 里。

常见误区

  1. 「首字节就是编码格式」——高 4 位是帧类型,低 4 位才是编码。
  2. 「第二字节总是存在」——只有 H.264 才有。
  3. 「关键帧数 = 帧数/GOP」——只能估,收尾会多补。
自测三问(点开对答案) 1. 0x17 与 0x27 分别是什么意思? —— **0x17 是 H.264 关键帧,0x27 是 H.264 普通帧。** 2. AVCPacketType 的三个值? —— **0 序列头、1 画面数据、2 序列结束。** 3. 脚本 tag 里装的是什么? —— **AMF0 编码的 onMetaData**,写分辨率、帧率、码率这些元数据。

相关笔记