🎬 第十三章 FFmpeg直播综合案例实战
📄 一句话概括

握手是六包定长字节,一共 4610 个,一个业务数据都不传。 本机没有服务器,那就自己扮一次,把这两边的字节数清楚。

📝 本节和前面几节的关系

13.1 说了第五段不归 FFmpeg 管,这一节就去看那一段在干什么。 看完之后 13.3 再来推流,就知道自己在推给谁。

一、本机没有服务器

先把「装没装」这件事查清。 端口 用途 结果


1935 RTMP 默认端口 没有(TimeoutError) 8080 常给 HTTP-FLV 没有(TimeoutError)

所以这一节不讲「怎么装服务器」,讲「服务器在握手这一层干什么」。 这一段能真跑,而且跑完能看懂所有 RTMP 资料里的那几张图。 注意这里用的是裸 socket,不是 FFmpeg——因为握手是传输层的事,用 socket 看反而最清楚。

二、握手就是六包定长字节

RTMP 连接的第一件事不是发命令,是交换固定长度的块。 包 谁发 长度 装什么


C0 客户端 1 版本号,现在都是 3 C1 客户端 1536 时间戳、零、随机数 S0 服务端 1 版本号 S1 服务端 1536 时间戳、零、随机数 C2 客户端 1536 把 S1 原样回抄 S2 服务端 1536 把 C1 原样回抄

加起来 1+1536 乘 3 再加 1,正好 4610 字节。 握手不传任何业务数据,只证明「双方都能收发定长块」。 为什么非要定长: 定长的东西最好校验——收到 1536 个字节就是 1536 个,少一个都说明连接有问题。 后面真正的数据才用变长的 chunk 分块。 真实 RTMP 还有复杂握手的 digest 校验(把一段密钥混进 C1 的随机区),这一节只做简单握手, 因为要讲清的是「交换固定长度块」这个骨架,不是那几行哈希。

三、真跑一遍两端握手

服务端与客户端都自己写,两个线程对着跑。 项 值


客户端发的 C0 1 字节(版本 3) 客户端发的 C1 1536 字节 服务端收到 C0/C1 1/1536 服务端回的字节 3073 客户端收到 S0 3 服务端收到 C2 1536 S2 与 C1 一样吗 True 全程耗时 不到 1 毫秒

3073 = 1 + 1536 + 1536,就是 S0 加 S1 加 S2,一个不多一个不少。 可见握手这一层完全没有额外开销。 S2 与 C1 一模一样,这就是握手的设计:服务端把客户端给的随机块原样回抄, 客户端看到回抄正确,就知道对面真收到了。 这种「原样回抄」是最省事的连通性证明。 耗时不到 1 毫秒也说明一件事:握手本身不慢。 直播的延迟不是被握手吃掉的,是被后面的缓冲吃掉的(13.1 那张表)。

四、真服务器怎么选

三个常见的,都开源、都能跑在 Linux 上。 服务器 特点 最小配置难点


SRS 国产、文档全、功能多 conf 里开 rtmp 段 nginx-rtmp 挂在 nginx 上,最常见 要自己编模块 MediaMTX 单文件、零依赖 改一个 yml

三者的共同点是「监听 1935、收 RTMP、转给播放端」,差别只在配置形态与附带功能(录制、 鉴权、转 HLS、转 WebRTC)。 自己练手推荐 MediaMTX:一个二进制文件、一个 yml,解压就能跑,不折腾编译。 真上生产再考虑 SRS 或 nginx-rtmp,因为它们能挂到已有的运维体系里。

五、对接服务器只要会三件事

握手之后还有 chunk 分块、AMF 命令、发布点鉴权三层, 但都不是「FFmpeg 的 API」能控制的——那是服务器的事。 要会的 例子


地址格式 rtmp://域名/应用名/流名 流名与鉴权 stream key 或带 token 的查询串 失败怎么分 先分网络、再分鉴权、最后看编码

第一件事最容易错:应用名与流名是两段,中间的斜杠不能少。 写成 rtmp://host/live 是把流名也省了,服务器多半直接拒。 第三件事是排障的顺序: 先 ping 或 socket 试端口(网络),再用带鉴权的地址试一次(鉴权),最后才怀疑编码参数。 反过来查,最常见的结局是改了半天码率,其实是 stream key 过期了。

六、动手 Demo:自己扮一次 RTMP 握手

两个线程:一个扮服务端收 C0+C1 回 S0+S1+S2,一个扮客户端发 C0+C1 再回 C2。

import socket
import struct
import threading

def server(srv, out):
    srv.settimeout(3.0)
    c, _ = srv.accept()
    c0 = c.recv(1)                       # 版本号
    c1 = b""
    while len(c1) < 1536:                 # 定长 1536
        c1 += c.recv(1536 - len(c1))
    s1 = struct.pack(">II", 0, 0) + bytes(range(256)) * 5 + bytes(248)
    c.sendall(bytes([3]) + s1 + c1[:1536])   # S0 + S1 + S2
    c2 = b""
    while len(c2) < 1536:
        c2 += c.recv(1536 - len(c2))
    out["c0"], out["c2"] = len(c0), len(c2)
    c.close()

srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 0))
srv.listen(1)
out = {}
threading.Thread(target=server, args=(srv, out), daemon=True).start()

cli = socket.create_connection(("127.0.0.1", srv.getsockname()[1]))
c1 = struct.pack(">II", 0, 0) + bytes(range(256)) * 5 + bytes(248)
cli.sendall(bytes([3]) + c1)          # C0 + C1
buf = b""
while len(buf) < 1 + 1536 + 1536:      # 等 S0 + S1 + S2
    buf += cli.recv(65536)
cli.sendall(buf[1:1537])               # C2 回抄 S1
print(out, buf[0], buf[1537:3073] == c1[:1536])

实跑输出:

--------------------------------------------------------------------
自己扮一次 RTMP 握手
--------------------------------------------------------------------

**① 本机没有流媒体服务器**

  端口     结果
  ---------------------------------
  1935     没有(TimeoutError)
  8080     没有(TimeoutError)

**1935 是 RTMP 的默认端口,8080 常被拿去做 HTTP-FLV。两个都没人听。**
**所以这一节不讲「怎么装服务器」,讲「服务器在握手这一层干什么」。**

**② 握手就是四包定长字节**

  包     谁发       长度     装什么
  -------------------------------------------------
  C0     客户端     1        版本号,现在都是 3
  C1     客户端     1536     时间戳、零、随机数
  S0     服务端     1        版本号
  S1     服务端     1536     时间戳、零、随机数
  S2     服务端     1536     把 C1 原样回抄
  C2     客户端     1536     把 S1 原样回抄

**加起来 1+1536 乘 3 再加 1,正好 4610 字节。**
**握手不传任何业务数据,只证明「双方都能收发定长块」。**

**③ 真跑一遍**

  项                   值
  ------------------------------------
  客户端发的 C0        1(版本 3)
  客户端发的 C1        1536 字节
  服务端收到 C0/C1     1/1536
  服务端回的字节       3073
  客户端收到 S0        3
  服务端收到 C2        1536
  S2 与 C1 一样吗      True
  全程耗时             1.17 毫秒

**S2 与 C1 一模一样,这就是握手的设计**:**服务端把客户端给的随机块原样回抄**,
客户端看到回抄正确,就知道对面真收到了。**这个「原样回抄」是一种最省事的连通性证明。**

**④ 真服务器怎么选**

  服务器         特点                      最小配置难点
  ------------------------------------------------------------------
  SRS            国产、文档全、功能多      conf 文件里开 rtmp 段
  nginx-rtmp     挂在 nginx 上,最常见     要自己编模块
  MediaMTX       单文件、零依赖            改一个 yml 就行

**三者的共同点是「监听 1935、收 RTMP、转给播放端」**,
**差别只在配置形态与附带功能(录制、鉴权、转 HLS)。**

**⑤ 为什么这一段只能讲到这**

**RTMP 握手之后还有 chunk 分块、AMF 命令、发布点鉴权三层**,
**每一层都有细节,但都不是「FFmpeg 的 API」能控制的**——**那是服务器的事。**

**对接服务器时真正要会的只有三件**:
**地址格式(`rtmp://域名/应用名/流名`)、流名与鉴权参数、以及推流失败时先分清是网络还是鉴权。**

服务端回 3073 字节 = 1+1536+1536,握手一个字节的额外开销都没有。 S2 与 C1 完全相同,这就是「原样回抄」的连通性证明。

30 秒实验

  1. 把 C0 的版本号从 3 改成 6,看服务端会不会拒(提示:这一节的服务端没校验)。
  2. 把 C1 只发 1000 字节,看服务端会不会一直等(提示:定长循环卡住)。
  3. 改一下 S1 的内容,看客户端回抄的 C2 跟着变不变。

常见误区

  1. 以为 RTMP 一上来就发业务命令。 先交换 4610 字节的定长块。
  2. 以为握手慢。 它不到 1 毫秒,慢的是后面的缓冲。
  3. 以为地址里流名可以省。 rtmp://host/app 缺流名,多数服务器直接拒。
自测三问(点开对答案) 1. RTMP 握手一共传多少字节? —— 4610:C0+C1 各 1 与 1536,S0+S1+S2 各 1 与 1536 两个,再加 C2 的 1536。 2. S2 里装的是什么? —— C1 的原样回抄,用来证明双方真的收到了对方的块。 3. 为什么握手要定长? —— 定长最好校验,少一个字节就说明连接有问题。

相关笔记