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

流媒体服务器只做一件事:收一路流,转给很多人。 搭起来至少要过四关——端口在听、握手能通、命令能认、媒体能收。

📝 本节和前面几节的关系

这是第八章唯一没有小节的一节,也是本章的收口。 8.1 到 8.4 讲的那些协议,最后都要落到一台服务器上。

一、三条路线

自己搭服务器,绕不开这三个选择。

方案 擅长 不擅长


SRS RTMP/HLS 一把抓,配置简单 高并发要调 ZLMediaKit 协议最全,HTTP-FLV 强 配置项多 Nginx-rtmp 跟网站混布,运维熟 版本老,功能少

三家做的是同一件事,差别只在「支持哪些协议」与「配置有多细」。 选型时先问自己一个问题:客户端是什么?

客户端是浏览器,就必须有 HTTP-FLV 或 HLS; 客户端是 VLC 或 ffplay,RTMP 与 RTSP 就够了; 客户端是手机 App,还要考虑低延迟与弱网重连。

二、端口是认协议的暗号

每种协议都有一个约定俗成的端口,看到端口就能猜到对面在跑什么。

端口 协议 干什么


1935 RTMP 推流与拉流的老家 554 RTSP 控制指令 8554 RTSP 不用 root 的替代端口 8080 HTTP 网页、HLS、HTTP-FLV 1936 RTMP over TLS 加密的 RTMP

554 是 RTSP 的标准端口,但它小于 1024, 非 root 用户绑不上,所以开发机上普遍用 8554。

「服务器起没起来」可以先看端口:端口在听,说明进程活着一半。 另一半是「能不能收流」,那得真推一路才知道。

三、最小服务器:RTMP 握手

要理解服务器在做什么,最好的办法是自己当一次服务器。 下面用一段代码顶替 SRS,只做 RTMP 握手。

阶段 方向 字节


C0 版本号 客户端→服务器 1 C1 随机数 客户端→服务器 1536 S0 加 S1 加 S2 服务器→客户端 3073 C2 回应 客户端→服务器 1536

服务器收到的 C0 是 0x03,C1 长度 1536 字节,加起来正好 1537。 回给客户端的 3073 字节里,前 1537 是 S0 与 S1,后 1536 是 S2。

S2 就是把 C1 原样抄回去——这是 RTMP 握手的核心: 双方各发一段随机数,再各自抄回来,谁也不验证内容, 只确认「来回通了」。所以握手快,但也不防中间人。

0x03 这个版本号是明文 RTMP,加密版是 0x06。 版本号不对,正经服务器会直接断开,连错误信息都不给。

四、握手之后还有 chunk

握手只证明链路通,业务一点没做。真正的力气在后面这层。

层 干什么 字节


握手 确认链路通 3073 chunk 分块 把消息切成小段 每段最多 128 命令消息 connect/createStream/publish 几十字节 媒体消息 FLV 标签 按帧变

chunk 的默认大小只有 128 字节,一条命令要切好几段才能发完。 连上之后双方可以协商一个更大的 chunk size,把开销压下去。

所以「服务器搭好了」这件事,最少要过四关: 端口在听、握手能通、命令能认、媒体能收。 前三关全过、第四关卡住,就是最常见的「推流失败」: 日志里连接成功、命令也认了,但一直没有画面。

五、动手 Demo:自己当一次服务器,跑完 RTMP 握手

下面这段起一个只做握手的最小服务器,再用一个客户端连上去。 服务器不认任何命令,握完就关——够用来验证「四关里的第二关」。

import socket, struct, threading, time

srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 0))
port = srv.getsockname()[1]
srv.listen(1)
got = {}

def serve():
    srv.settimeout(8.0)
    c, _a = srv.accept()
    c.settimeout(8.0)
    buf = b""
    while len(buf) < 1537:          # C0 一个字节 + C1 1536 个
        d = c.recv(4096)
        if not d:
            break
        buf += d
    got["c0"] = buf[0]
    got["c1"] = len(buf) - 1
    # S0 + S1 + S2:版本号 + 1536 个 0 + 把 C1 原样抄回去
    c.sendall(bytes([3]) + struct.pack(">II", 0, 0)
              + b"\x00" * 1528 + buf[1:1537])
    try:
        got["c2"] = len(c.recv(1536))
    except Exception:
        got["c2"] = 0
    c.close()

th = threading.Thread(target=serve)
th.daemon = True
th.start()
time.sleep(0.2)

cli = socket.create_connection(("127.0.0.1", port), 3.0)
c1 = struct.pack(">II", 0, 0) + b"\x11" * 1528
cli.sendall(bytes([3]) + c1)
s = b""
cli.settimeout(5.0)
while len(s) < 3073:
    d = cli.recv(4096)
    if not d:
        break
    s += d
cli.sendall(c1[:1536])
cli.close()
th.join(6.0)
srv.close()
print(u"C0=0x%02X   C1=%d 字节   S0S1S2=%d 字节"
      % (got["c0"], got["c1"], len(s)))

实跑输出:

--------------------------------------------------------------------
8.5 流媒体服务器的搭建
--------------------------------------------------------------------

**① 三条路线,各自适合什么**

  方案           擅长                           不擅长
  ----------------------------------------------------------------
  SRS            RTMP/HLS 一把抓,配置简单     高并发要调
  ZLMediaKit     协议最全,HTTP-FLV 强          配置项多
  Nginx-rtmp     跟网站混布,运维熟             版本老,功能少

**三家都做同一件事:收一路流,转给很多人。**
**差别在「支持哪些协议」与「配置有多细」。**

**② 端口是认协议的暗号**

  端口     协议              干什么
  ---------------------------------------------------
  1935     RTMP              推流与拉流的老家
  554      RTSP              控制指令
  8554     RTSP              不用 root 的替代端口
  8080     HTTP              网页、HLS、HTTP-FLV
  1936     RTMP over TLS     加密的 RTMP

**所以「服务器起没起来」可以用端口判断:端口在听,说明进程活着一半。**
**另一半是「能不能收流」,那得真推一路才知道。**

**③ 一个服务器最少要做什么:用 RTMP 握手演示**

  阶段               方向              字节
  ---------------------------------------------
  C0 版本号          客户端→服务器     1
  C1 随机数          客户端→服务器     1536
  S0 + S1 + S2     服务器→客户端     3073
  C2 回应            客户端→服务器     1536

**服务器收到 C0 是 `0x03`,C1 长度 1536 字节——对得上 1537 这个总数。**

**S2 就是「把 C1 原样抄回去」,这是 RTMP 握手的核心:**
**双方各发一段随机数,再各自抄回来,谁也不验证内容,只确认「来回通了」。**

**④ 握手之后还有一层:chunk**

  层             干什么                             字节
  ------------------------------------------------------------------
  握手           确认链路通                         3073
  chunk 分块     把消息切成小段                     每段最多 128
  命令消息       connect/createStream/publish     几十字节
  媒体消息       FLV 标签                           按帧变

**所以「服务器搭好了」这件事,最少要过四关:**
**端口在听、握手能通、命令能认、媒体能收。**
**前三关全过、第四关卡住,就是最常见的「推流失败」。**

C0 读出来是 0x03,C1 是 1536 字节,服务器回的 3073 也分毫不差。

这段代码故意不认 connect 命令——所以真正的客户端连上来会卡在第三关。 「握手能通、命令不认」正是调服务器时最典型的中间状态。

30 秒实验

  1. 把 C0 改成 0x06:看客户端会不会因为版本号不认识而断开。
  2. 把 S2 改成全 0:正经客户端会拒绝,「抄回去」这一步是有用的。
  3. 把 listen(1) 改成 listen(5):同时连三路,看第几路被拒。

常见误区

  1. 以为端口在听就是服务器好了。端口只说明进程活着,不说明能收流。
  2. 用 554 起 RTSP 却不用管理员权限。小于 1024 的端口绑不上。
  3. 把握手当成鉴权。S2 只是把 C1 抄回来,谁都能通过。
自测三问(点开对答案) 1. RTMP 握手一共几个字节? —— 客户端发 1537(C0 加 C1),服务器回 3073(S0 加 S1 加 S2),客户端再回 1536(C2)。 2. 为什么 RTSP 开发商普遍用 8554? —— 554 小于 1024,非 root 用户绑不上,8554 是常用的替代端口。 3. 「推流失败」最常见的样子是什么? —— 端口通、握手成、命令也认了,就是没有媒体数据——卡在第四关。

相关笔记