🎬 第八章 FFmpeg命令行实现流媒体功能及直播应用
📄 一句话概括

RTMP 从头到尾只用一条 TCP 连接,控制命令与媒体数据全挤在里面。 FFmpeg 里没有叫 rtmp 的封装格式——封装用 flv,传输靠 rtmp:// 前缀。

📝 本节和前面几节的关系

8.5 已经演示过 RTMP 握手,这一节把剩下的几层补齐。 8.6.3 与 8.6.4 再把推流端与播放端接上。

一、五层结构

RTMP 不是单一协议,而是五层叠起来的东西。

层 干什么 字节


TCP 连接 先建一条长连接,一直不关 — 握手 交换版本号与随机数 1537 + 3073 chunk 分块 把长消息切成小段 每段最多 128 AMF0 命令 connect、createStream、publish 几十字节 FLV 标签 音频、视频、元数据 11 + N + 4

跟 RTSP 最大的不同:RTMP 只有一条 TCP 连接。 RTSP 是控制走 554、媒体另开 RTP 通道;RTMP 把两件事塞进同一条线, 靠 chunk 里的消息类型区分——这条线既是指挥部,也是运输队。

二、首次建连要花多少字节

把三种协议的「第一次握手」摆在一起比,差距很直观。

协议 首次建连字节 说明


HTTP 73 一条请求,答完就完 RTSP 477 四问答(OPTIONS 到 PLAY) RTMP 6146 握手 1537 + 3073 + 1536

RTMP 贵得多,因为它一次建连之后就不再断开。 这 6 KB 只付一次,后面几百帧都复用这条连接。 摊到一小时直播上基本看不见,所以「贵」在这里不是缺点。

反过来说:连接数少是 RTMP 的优点,也是它的瓶颈。 每个观众占一条常连接,服务器上万观众就是上万条—— 这也是后来 HLS 与 HTTP-FLV 能流行起来的原因。

三、FFmpeg 里没有 rtmp 这个格式

这一条几乎人人踩过:写 format="rtmp" 会直接报错。

格式名 在不在


flv 有 rtmp 没有 mpegts 有 hls 有 rtsp 有

本机 libav 一共 414 个格式,rtmp 这个名字不在里面。 因为 RTMP 在 FFmpeg 里被拆成两半: 封装用 flv,传输靠 URL 里的 rtmp:// 前缀。

所以正确写法是 -f flv rtmp://127.0.0.1/live/stream。 -f flv 说的是「里面装什么」,rtmp:// 说的是「怎么送」。 把两者混成一件事,就会写出根本不存在的东西。

四、FLV 的头只有 13 个字节

FLV 的文件头极短,因为设计目标是「边收边解」。

偏移 字段 本例


0 到 2 签名 FLV 3 版本 0x01 4 标志位 0x01(有视频) 5 到 8 数据偏移 9 9 到 12 PreviousTagSize0 0

标志位是个位掩码:0x01 有视频、0x04 有音频,两个都有就是 0x05。

头后面那 4 个字节是 PreviousTagSize0,值恒为 0—— 它没有实际数据,只是为了让「每个标签前面都有一条长度记录」这个格式整齐。 所以第一个真正的标签从第 13 个字节开始。

第一个标签永远是 0x12(脚本),装的是 onMetaData: 宽高、帧率、码率、时长都写在这里,播放器靠它先知道「这是什么」。

五、chunk 的头有多大

一条命令不可能只有一个 chunk,它会被切成 128 字节一段。 拿一条 300 字节的 connect 命令算一遍:

项 值


消息长度 300 字节 chunk 大小 128 字节 切成几段 3 段 第一段头 12 字节 后续每段头 1 字节 实际发出 314 字节

第一段的头最胖,因为它要把消息长度、类型、流号一次说清。 后面的段只带 1 个字节,意思是「我还是上一段那条消息」。

314 减 300 等于 14,比裸数据多了不到 5%。 听着不多,但建连阶段全是几十字节的小命令, 128 的默认值会让开销比例飙到三成以上。

所以连上之后双方会协商一个更大的 chunk size(常见 4096), 开销立刻降到几乎看不见——这是 RTMP 省字节的关键一步。

六、动手 Demo:查格式清单、拆 FLV 头、算 chunk 开销

下面这段先翻一遍 libav 的格式清单,确认 rtmp 不在里面; 再把 50 帧封成 FLV,把前 13 个字节逐字段读出来。

import io, struct
from fractions import Fraction
import av

names = list(av.formats_available)
print(u"格式总数 %d" % len(names))
for k in ("flv", "rtmp", "mpegts", "hls", "rtsp"):
    print(u"  %-8s %s" % (k, "有" if k in names else "没有"))

try:
    o = av.open("rtmp://127.0.0.1:19999/live/x", "w", format="rtmp")
    o.close()
except Exception as e:
    print(u"format=rtmp -> %s" % type(e).__name__)

buf = io.BytesIO()
o = av.open(buf, "w", format="flv")
st = o.add_stream("libx264", rate=Fraction(25, 1))
st.width, st.height, st.pix_fmt = 160, 90, "yuv420p"
st.bit_rate = 400000
c, k = av.open("s7.mp4"), 0
for f in c.decode(video=0):
    for q in st.encode(f):
        o.mux(q)
    k += 1
    if k >= 50:
        break
for q in st.encode(None):
    o.mux(q)
o.close()
c.close()
d = buf.getvalue()

print(u"FLV %d 字节" % len(d))
print(u"  签名   %s" % d[:3].decode("ascii"))
print(u"  版本   0x%02X" % d[3])
print(u"  标志位 0x%02X" % d[4])
print(u"  偏移   %d" % struct.unpack(">I", d[5:9])[0])
print(u"  Tag0   %d" % struct.unpack(">I", d[9:13])[0])
print(u"  首标签 0x%02X" % d[13])

msg, chunk = 300, 128
parts = (msg + chunk - 1) // chunk
print(u"%d 字节切 %d 段,共发 %d 字节"
      % (msg, parts, msg + 12 + (parts - 1)))

实跑输出:

--------------------------------------------------------------------
8.6.1 RTMP 简介
--------------------------------------------------------------------

**① RTMP 分五层,一层一层往下扒**

  层             干什么                             字节
  ------------------------------------------------------------------
  TCP 连接       先建一条长连接,一直不关           —
  握手           交换版本号与随机数                 1537 + 3073
  chunk 分块     把长消息切成小段                   每段最多 128
  AMF0 命令      connect、createStream、publish     几十字节
  FLV 标签       音频、视频、元数据                 11 + N + 4

**跟 RTSP 最大的不同:RTMP 只有一条 TCP 连接,**
**控制命令与媒体数据全挤在这条线上,靠 chunk 里的消息类型区分。**

**② 三家的首次建连开销对比**

  协议     首次建连字节     说明
  -------------------------------------------------------
  HTTP     73               一条请求,答完就完
  RTSP     477              四问答(OPTIONS 到 PLAY)
  RTMP     6146             握手 1537 + 3073 + 1536

**RTMP 贵得多,因为它一次建连之后就不再断开——**
**这 6 KB 只付一次,后面几百帧都复用这条连接。**
**数字看着吓人,摊到一小时直播上基本看不见。**

**③ 实测:本机 libav 里有没有 rtmp 这个格式**

  格式名      在不在
  ----------------------
  flv         有
  rtmp        没有
  mpegts      有
  hls         有
  segment     有
  rtsp        有
  rtp         有

**一共 414 个格式,`rtmp` 这个名字不在里面。**
**因为 RTMP 在 FFmpeg 里不是一个「封装格式」,而是一套协议:**
**封装用 `flv`,传输靠 URL 里的 `rtmp://` 前缀。**

**所以写 `format="rtmp"` 会直接抛 `ValueError`——名字根本不存在。**
**正确的写法是 `format="flv"`,地址写 `rtmp://`。**

**④ 实测:50 帧封成 FLV 有多大,头长什么样**

  项             值
  ---------------------------------------------
  FLV 总字节     30271
  封装耗时       56 ms
  文件头         FLV
  版本           0x01
  标志位         0x01(有视频)
  数据偏移       9
  头 13 字节     464c5601010000000900000000

**「FLV」三个字母开头,版本 1,标志位 0x01 表示「只有视频没有音频」,**
**数据偏移 9 表示「头后面有 9 个字节才开始第一个标签」。**

**头之后紧跟 4 字节的 PreviousTagSize0,值恒为 0——这是 FLV 的起手式。**
**所以第一个标签从第 13 个字节开始,不多不少。**

**⑤ chunk 的头有多大:一条 300 字节的命令要切几段**

  项             值
  ---------------------------
  消息长度       300 字节
  chunk 大小     128 字节
  切成几段       3 段
  第一段头       12 字节
  后续每段头     1 字节
  实际发出       314 字节

**第一段的头最胖,因为它要把消息长度、类型、流号一次说清;**
**后面的段只带 1 个字节,说「我还是上一段那条消息」。**

**128 这个默认值很小,所以 RTMP 建连阶段会很啰嗦。**
**连上之后双方会协商一个更大的 chunk size(常见 4096),开销立刻降下来。**

rtmp 那一行标「没有」,format="rtmp" 也确认抛 ValueError。

头 13 个字节读出来是「FLV + 01 + 01 + 00000009 + 00000000」, 第一个标签的类型是 0x12,不是视频——很多人以为第一段就是画面。

30 秒实验

  1. 把标志位那个字节改成 0x05:看解复用器会不会以为「有音频」。
  2. 把 format="flv" 去掉只留 rtmp://:libav 会按扩展名硬猜,猜不对就报错。
  3. 把 chunk 从 128 改成 4096:算一遍 300 字节的命令还剩多少开销。

常见误区

  1. 以为 RTMP 是个封装格式。封装是 flv,RTMP 只是运输通道。
  2. 以为 FLV 第一个标签是视频。第一个永远是 0x12 脚本标签。
  3. 把 PreviousTagSize0 当成有效数据。它恒为 0,只是占位。
自测三问(点开对答案) 1. RTMP 为什么要比 RTSP 多花十几倍字节建连? —— 它要建立一条长期复用的 TCP 连接,握手与随机数交换都在里面。 2. `-f flv rtmp://…` 两个部分各管什么? —— `-f flv` 管「里面装什么」,`rtmp://` 管「怎么送出去」。 3. 一条 300 字节的命令按 128 切,实际发多少字节? —— 314 字节:300 加第一段 12 字节头,加后两段各 1 字节。

相关笔记