🎬 第五章 HLS流媒体协议
📄 一句话概括

三行 ffprobe 能看八成问题,剩下两成得自己写 dump。 因为现成工具不看「包级」的东西。

📝 本节和前面几节的关系

5.5.1 认清了包头字段,这一节讲怎么把它们量出来。 5.5.3 再把量出来的结果串成「PAT 找 PMT、PMT 找流」的路线。

一、三个常用命令

ffprobe -show_streams a.ts:看有几条流、编码是什么、分辨率多少。 排查「没声音」「黑屏」先跑这条。 ffprobe -show_packets a.ts:看每包大小、PTS/DTS。 它是最有用的一条——能直接看出「关键帧有多大」「GOP 多长」「时间戳有没有跳」。 ffmpeg -i a.ts -c copy out.mp4:转封装,不重编码。 这一步能验证「流本身是好的,只是容器不对」。

为什么 -show_packets 最有价值? 因为绝大多数「播不动」的问题不是编码错,而是时间戳错。 时间戳一跳,播放器就会卡住或者加速追帧, 而这两种表象让人第一反应去怀疑网络,方向就偏了。

二、沙箱里用 PyAV 做等价的事

沙箱里没装 ffprobe,所以用 PyAV 拆一遍同一个文件。 实测 9588 字节的文件里,25 个包、2 个关键帧、 最大包 1007 字节、最小包 41 字节。

这几个数字合起来能推出不少东西: ① 25 个包对 25 帧,说明一帧一个包,没有聚合。 ② 2 个关键帧,说明 GOP 是 25 帧、也就是 1 秒。 ③ 最大 1007、最小 41,差 24 倍,说明这是变码率编码。

第 ③ 条特别值得注意: 如果最小包也是 1000 左右,那说明码率几乎是恒定的, 这通常意味着编码器开了 CBR,画质会在复杂场景掉下来。 用「最大/最小包之比」粗判码率模式,是个很实用的技巧。

三、自己写一个 dump 也有用

现成工具看不见「包级」的东西:比如 PID 分布、连续计数跳变、适配字段占比。 这些才是排查「TS 为什么播不动」的关键。 所以实际运维里常备一个小脚本,几行就够(本节的 Demo 就是)。

一个排查顺序建议: ① 每 188 字节是不是 0x47,② PID 分布是否正常, ③ 有没有 PAT/PMT,④ 连续计数有没有跳。 四步能把大部分「TS 坏了」的问题定位到层。

这四步的顺序不是随便排的,是从「最容易坏」到「最不容易坏」: 对齐错了说明文件被截断或拼接过; PID 分布异常说明流本身没封好; 没有 PAT/PMT 说明切片开头没重发表; 连续计数跳说明传输过程丢了包。

四、动手 Demo:分析工具——从一行命令到自己写 dump

先列三个 ffprobe 命令各自的用途,再用 PyAV 把同一个文件拆一遍。 最后给一个四步排查顺序,从对齐一路查到连续计数。

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

tmp = os.path.join(tempfile.mkdtemp(), "a.ts")
c = av.open(tmp, "w", format="mpegts")
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()

print("文件 %d 字节" % os.path.getsize(tmp))
sizes, key = [], 0
with av.open(tmp) as ct:
    for pk in ct.demux(video=0):
        if pk.size == 0:
            continue
        sizes.append(pk.size)
        if pk.is_keyframe:
            key += 1
print("包数 %d,关键帧 %d" % (len(sizes), key))
print("最大 %d 字节,最小 %d 字节,比值 %.1f"
      % (max(sizes), min(sizes), max(sizes) / min(sizes)))

实跑输出:

--------------------------------------------------------------------
分析工具——从一行命令到自己写 dump
--------------------------------------------------------------------
**① 现成工具的三个常用命令**

  命令                               看什么
  -----------------------------------------------------------
  ffprobe -show_streams a.ts         流数量、编码、分辨率
  ffprobe -show_packets a.ts         每包大小、PTS/DTS
  ffmpeg -i a.ts -c copy out.mp4     转封装,不重编码

  **`-show_packets` 是最有用的一条**:**它把每一帧的大小与时间戳都打出来。**
  **能直接看出「关键帧有多大」「GOP 多长」「时间戳有没有跳」。**

**② 沙箱里用 PyAV 做等价的事**

  文件 9588 字节
  项         值
  ------------------------
  包数       25
  关键帧     2
  最大包     1007 字节
  最小包     41 字节

**③ 自己写一个 dump 也有用**

  **现成工具看不见「包级」的东西**:**比如 PID 分布、连续计数跳变、适配字段占比。**
  **这些才是排查「TS 为什么播不动」的关键。**
  **所以实际运维里常备一个小脚本,几行就够(本节的 Demo 就是)。**
  **一个排查顺序建议:**
  **① 每 188 字节是不是 0x47 → ② PID 分布是否正常 → ③ 有没有 PAT/PMT → ④ 连续计数有没有跳。**
  **四步能把大部分「TS 坏了」的问题定位到层。**

25 个包对 25 帧,说明编码器一帧出一个包,没有聚合。 2 个关键帧就是 GOP 25,换算成时间正好 1 秒。 最大/最小 24.6 倍,这是典型的变码率(CRF)特征,不是 CBR。

30 秒实验

  1. 把 g 从 25 改成 5,看关键帧数——变成 5 个,GOP 越短关键帧越多、文件越大。
  2. 把 crf 从 30 改成 18,看最大包——明显变大,画质提升的代价就是字节。
  3. 在 dump 循环里加一行统计 PID,看分布——能验证 5.5.1 里「视频占六成」的结论。

常见误区

  1. 「ffprobe 能看出 PID 分布」——不能,它看流级不看包级。
  2. 「包数一定等于帧数」——不一定,B 帧与聚合会打破这个关系。
  3. 「最大最小之比只说明画质」——它首先说明的是码率模式。
自测三问(点开对答案) 1. 三个 ffprobe 命令分别解决什么问题? —— **流信息、包级时间戳、转封装验证**;排查顺序也是这个顺序。 2. 四步排查顺序是什么? —— **对齐 → PID 分布 → PAT/PMT → 连续计数**,从易坏到不易坏。 3. 最大最小包之比能说明什么? —— **码率模式**:比值大是变码率,接近 1 说明可能开了 CBR。

相关笔记