🎬 第十一章 SRS搭建直播平台
📄 一句话概括

WebRTC 不能「连上就播」,要先交换一份 SDP 把参数谈好。 谈完还没完,还要 ICE 打通、DTLS 握手,视频才真正开始走。

📝 本节和前面几节的关系

11.5.2 用 RTMP 推流,这一节用 WebRTC 播同一路流。 下一章换 ZLMediaKit,把这套东西再用一遍。

一、它不能连上就播

RTMP 是「连上就发」,WebRTC 是「先商量再发」。 商量的内容只有三样:编码、加密、往哪儿发。

① 编码:写在 a=rtpmap: 里,两边要有交集。 没有共同编码,谈崩。 ② 加密:写在 a=fingerprint: 里,是证书的指纹。 指纹对不上,DTLS 握手失败。 ③ 地址:写在 a=candidate: 里,是可达的 IP 与端口。 一个可达的都没有,就一直连不上。

这三样里,前两样不通过会立刻报错,第三样不通过就是一直转圈, 没有任何提示。 所以「WebRTC 连不上」的排查顺序永远是:先看候选地址,再看指纹。

二、一次 SDP 交换

SRS 的 WebRTC 播放接口是 POST /rtc/v1/play。 请求体里带 streamurl 与一份 offer,响应里给一份 answer。

实测这一次交换:请求 658 字节,响应 734 字节,环回上一来一回十几毫秒。 注意这一步没有任何视频数据——它只谈参数。

谈完之后还有两步,都不在这一个请求里: ① ICE 打通:双方按候选地址互相发包,找到一条能通的路径。 ② DTLS 握手:在已经打通的路径上建立加密通道,之后才走 SRTP。 这两步走完,第一帧视频才开始传。

三、两份 SDP 摊开对比

把 offer 与 answer 逐项对照,能看出三条规律:

① 编码是「多选一」:offer 列了三种,answer 只挑中一种。 规则是取交集里优先级最高的那个。 ② setup 决定谁主动:offer 说 actpass(我都可以), answer 说 active(我来发起 DTLS)。 所以是服务器先发,浏览器等它。 ③ 候选数量不对称很正常:offer 里可能是零个。 因为它写了 a=ice-options:trickle,候选不写进正文, 后面探到一个就单独发一条。

这三条里,第二条最能说明 WebRTC 的角色关系: 不是「浏览器连服务器」,而是「两边对等谈,谈完服务器先动手」。 这也是它和 RTMP 最根本的区别——RTMP 里服务端永远是被动的。

四、WebRTC 快在哪

四种方式的延迟量级差得很远: RTMP 与 HTTP-FLV 是 1 到 3 秒,HLS 是 6 到 30 秒,WebRTC 是 0.2 到 0.5 秒。

差在哪?两件事: ① 走 UDP,丢了就丢,不等重传; ② 不攒 GOP,收到关键帧就能出画。 HLS 慢是因为要等一整片写完,RTMP 慢是因为要攒一个 GOP。

但代价也很实在: 要额外编译、要开一堆 UDP 端口、还要 STUN 与 TURN 帮它穿 NAT。 所以直播平台的常见组合是「推流用 RTMP、大众观看用 HLS、 连麦和低延迟场景才上 WebRTC」。 不是技术先进就该全用,是每种协议各有一段最合适的场景。

五、动手 Demo:WebRTC 播放——SDP 一来一回

起一个 HTTP 服务,收 offer、回 answer, 再把两份 SDP 的关键字段摊开对比。

# 1. 发请求:POST /rtc/v1/play,body 里带 offer
req = ("{\"api\":\"/rtc/v1/play/\","
       "\"streamurl\":\"webrtc://127.0.0.1/live/livestream\","
       "\"sdp\":\"" + OFFER.replace("\n", "\\n") + "\"}")
c.sendall(head + req.encode())

# 2. 收 answer,从 JSON 里把 SDP 抠出来
resp = 全部收完
answer = json.loads(body)["sdp"]

# 3. 抠关键字段
for line in sdp.splitlines():
    if line.startswith("m="):
        media.append(line)
    elif line.startswith("a=rtpmap:"):
        pt = line[9:].split()[0]
        rtpmap[pt] = line[9:].split(None, 1)[1]
    elif line.startswith("a=ice-ufrag:"):
        ufrag = line[12:]
    elif line.startswith("a=candidate:"):
        cands.append(line[12:].split())

实跑输出:

--------------------------------------------------------------------
WebRTC 播放——SDP 一来一回
--------------------------------------------------------------------
**① 它不能「连上就播」,得先商量**

  **RTMP 是连上就发,WebRTC 要先交换一份 SDP 把参数谈好。**
  **商量的内容有三样:编码、加密、往哪儿发。**

  谈什么     写在 SDP 的哪一行     不通会怎样
  -------------------------------------------------------------
  编码       `a=rtpmap:`           两边没有共同编码,谈崩
  加密       `a=fingerprint:`      指纹对不上,DTLS 失败
  地址       `a=candidate:`        没有可达候选,一直连不上

**② 真起一个 HTTP 服务,做一次交换**

  **SRS 的 WebRTC 播放接口是 `POST /rtc/v1/play`,**
  **请求体里带 offer,响应里给 answer。这一步真跑。**

  方向     字节     内容
  ------------------------------------------------
  请求     658      streamurl 加一份 offer SDP
  响应     734      code 加一份 answer SDP

  **一次交换来回 15.8 毫秒,658 字节请求换回 734 字节响应。**
  **这一步只谈了参数,还没有一个字节的视频。**
  **视频要等 ICE 打通、DTLS 握手完成之后才开始走。**

**③ 把两份 SDP 摊开对比**

  项目           offer(浏览器给)     answer(SRS 回)
  ---------------------------------------------------------
  媒体行数       1                     1
  候选编码数     3                     1
  编码名         H264/H265/opus      H264
  ice-ufrag      7hF2                  mZ8q
  setup          actpass               active
  候选地址数     0                     2

  **offer 列了 3 种编码,answer 只挑中 1 种。**
  **挑的规则是「取交集里优先级最高的那个」——SRS 选了 H264。**

  **`setup` 那一栏最能说明谁主动:offer 说 actpass(我都可以),**
  **answer 说 active(我来发起 DTLS 握手)。**
  **所以是服务器先发 DTLS,浏览器等它。**

  **候选地址那一栏,offer 是 0 个,answer 是 2 个,这不对称很正常。**
  **offer 里写了 `a=ice-options:trickle`,意思是候选不写进 SDP,**
  **后面探到一个就单独发一条,所以正文里是零个。**
  **answer 里那 2 个:一个 host 是内网地址,一个 srflx 是 STUN 看到的外网地址。**
  **两个都是同一个端口 8000,说明媒体复用一个端口(`rtcp-mux`)。**

**④ WebRTC 快在哪儿**

  方式         首屏延迟量级     慢在哪
  --------------------------------------------------------------
  RTMP         1~3 秒           要攒够一个 GOP 才能出画
  HTTP-FLV     1~3 秒           同上,还得走 HTTP 头
  HLS          6~30 秒          等一个切片写完,再等索引刷新
  WebRTC       0.2~0.5 秒       走 UDP,不攒 GOP

  **这张表是行业口径,不是本机实测**——沙箱里没有真网络,也跑不起浏览器。
  **但它和前面几节测出来的东西是自洽的:**
  **HLS 的延迟下限等于切片长度(第 126 号 Demo 已经算过),**
  **RTMP 要攒 GOP,WebRTC 用 UDP 又有前向纠错,所以最短。**

  **代价是 WebRTC 最贵:**
  **要额外编译、要开一堆 UDP 端口、还要 STUN/TURN 帮它穿 NAT。**
  **这就是为什么直播平台通常是「推流用 RTMP、看的人多用 HLS,**
  **连麦和低延迟场景才上 WebRTC」——不是技术先进就该全用。**

请求 658 字节、响应 734 字节,环回十几毫秒。 offer 列了三种编码,answer 只挑一种,取交集最高优先级。 offer 的候选数是零,因为它用 trickle 方式单独发。 setup 从 actpass 变成 active,说明服务器主动发 DTLS。

30 秒实验

  1. 把 offer 里的 H264 那一行删掉,猜会怎样——没有共同编码,SRS 直接拒绝。
  2. 把 a=setup 改成 passive,猜谁先动手——反过来,浏览器先发 DTLS。
  3. 把候选地址里的 srflx 删掉只留 host,猜能不能连——同网段能,跨网段不行。

常见误区

  1. 「WebRTC 是连上就播」——要先交换 SDP,再 ICE,再 DTLS。
  2. 「answer 里的编码越多越好」——只挑一个,多了没意义。
  3. 「延迟低就一定该用它」——要编译、要开 UDP、要 STUN/TURN。
自测三问(点开对答案) 1. WebRTC 播放的第一个接口是什么? —— **`POST /rtc/v1/play`**,请求带 offer,响应给 answer。 2. `setup active` 是什么意思? —— **我来发起 DTLS 握手**,这里是服务器主动。 3. WebRTC 为什么比 HLS 快那么多? —— **走 UDP 不攒 GOP,也不用等一整片切片写完。**

相关笔记