整个页面就是九个动作,前五步自己准备,第六步送出去,后三步接住。 answer 只做三处改动,但有两处不能动,动了就黑屏。
13.3.2 与 13.3.3 把信令与页面服务装好了,这一节把它们串起来。 下一章换成综合案例,把前面十一章全用上。
一、九个动作
不管页面写得多花,核心永远是这九步。左边是 API,右边是它干的事:
步骤 调用的 API 它干的事
1 getUserMedia 拿到音视频轨道 2 new RTCPeerConnection 建一个连接对象 3 addTrack 把轨道塞进去 4 createOffer 生成自己的 SDP 5 setLocalDescription 记下来,同时开始收候选 6 经信令发出去 对面拿到 offer 7 setRemoteDescription 认出对面的 SDP 8 createAnswer 生成回应 9 ontrack 轨道到了,接进 video
前五步是推的一方自己准备,第 6 步把 offer 送出去,后三步是收的一方接住。 中间那一步不在浏览器里,这也是为什么必须有信令服务器。
还有一个细节容易漏:setLocalDescription 之后候选是「陆续」出来的。
icegatheringstate 从 gathering 变成 complete 才收齐。
急着发出去只会发一个空候选表,剩下的要靠 onicecandidate 一条条补发。
这个「陆续补发」的做法叫 trickle,几乎所有现成方案都是这么做的。
二、拆一份 offer
沙箱里没有浏览器,抓不到真的 offer, 所以下面这份是按 Chrome 的真实格式誊抄的:字段一个不少、顺序一致、 PT 号也是 Chrome 真会给的那几个(111 给 opus、96 给 VP8、97 给 H264)。
媒体 PT 号 编码数 编码清单
audio 111 63 2 111=opus、63=red video 96 97 98 3 96=VP8、97=H264、98=rtx
注意 PT 号:111、63、96、97、98。这些号是发起方随手定的,不是标准号。 唯一固定的是 0 到 95 那一段(96 以上才留给动态分配)。
所以看 SDP 永远要顺着 a=rtpmap 查表,不能记「96 就是 VP8」。
这个习惯很重要:同一台机器上开两次浏览器,PT 号都可能不一样。
写代码时如果硬编码了某个号,换一台机器就跑不通。
三、answer 只改三处,但两处不能动
answer 不是照抄,SRS 在里面做了三处改动,还有一处故意不动:
行 offer answer
a=setup actpass active a=sendonly/recvonly sendonly recvonly a=group:BUNDLE BUNDLE 0 1 BUNDLE 0 1
sendonly 变 recvonly 最好理解:推的人说「我给」,收的人说「我要」。
setup 从 actpass 变 active 就有点绕:
actpass 的意思是「我主动被动都行,你定」;
SRS 回 active,等于接下这个活——DTLS 握手由服务器先发起。
如果两边都回 active,握手会僵住,谁也等不到对方。
这是自建信令时最常见的死锁,现象是连接一直停在 connecting。
BUNDLE 那行两边一样,都是 0 1。
它的意思是「音视频挤在同一个端口上走」,靠 mid 号区分。
如果不 BUNDLE,一路流就要一对端口,观众一多端口就不够用了。
再看编码那一栏:offer 给了 3 个视频编码,answer 只要 1 个。
被丢掉的是 96(VP8)与 98(rtx)。
98 的 apt=97 说明它是 97 的重传通道,丢掉它意味着这条路没有重传。
所以这套配置下一旦丢包就是花屏,不会补。
四、两条铁律:顺序不能换,号码不能改
第一,answer 的 m 段必须和 offer 一一对应、顺序相同。
这不是约定,是 SDP 规范——m 段是靠位置匹配的,mid 只是辅助。
把 answer 里 audio 和 video 两段对调,浏览器会认为「音频那条我要的是 H264」。
第二,每个编码的 PT 号由 offer 方定,answer 只能沿用,不能改号。 如果 SRS 把 video 的 97 改回 96,浏览器会去 96 的 rtpmap 里找, 而 answer 里 96 又没定义,结果就是「收到了包但不知道怎么解」。
这两条合起来解释了一类很难查的故障: SDP 语法完全合法、ICE 也连上了、DTLS 也通了,就是黑屏。 查这种问题不要看日志,直接把两边的 SDP 并排贴出来逐行对。 日志只会告诉你「连接成功」,它不会告诉你「协商错了」。
五、动手 Demo:创建基于 WebRTC 的网页视频会话——九个步骤与一次协商
先拆开一份双 m 段的 offer,抠出两条 m 段的编码表。 再和 answer 逐项对比,最后算一次会话总共交换多少字节。
def sdp_media(text):
"""把 SDP 拆成 (全局行, [每条 m 段的 (m 行, 其他行)])。"""
head, media, cur = [], [], None
for line in text.splitlines():
if line.startswith("m="):
cur = (line, [])
media.append(cur)
elif cur is None:
head.append(line)
else:
cur[1].append(line)
return head, media
def pt_table(lines):
"""从一条 m 段里抠出 PT 号与编码名的对照。"""
out = []
for line in lines:
if line.startswith("a=rtpmap:"):
num, rest = line[9:].split(" ", 1)
out.append((num, rest))
return out
head, media = sdp_media(OFFER)
print("全局", len(head), "行,m 段", len(media), "条")
for mline, body in media:
kind = mline.split(" ")[0][2:]
pts = pt_table(body)
print(kind, "PT", [a for a, _b in pts],
"编码", [b.split("/")[0] for _a, b in pts])
# 和 answer 比,看砍掉了哪些
o = dict((m.split(" ")[0][2:], pt_table(b)) for m, b in media)
a = dict((m.split(" ")[0][2:], pt_table(b)) for m, b in sdp_media(ANSWER)[1])
drop = [x for x in o["video"] if x[0] not in [y[0] for y in a["video"]]]
print("视频砍掉", drop)
实跑输出:
--------------------------------------------------------------------
创建基于 WebRTC 的网页视频会话——九个步骤与一次协商
--------------------------------------------------------------------
**① 整个页面就是九个动作**
**不管页面写得多花,核心永远是这九步。左边是 API,右边是它干的事:**
步骤 调用的 API 它干的事
-------------------------------------------------------------
1 getUserMedia 拿到音视频轨道
2 new RTCPeerConnection 建一个连接对象
3 addTrack 把轨道塞进去
4 createOffer 生成自己的 SDP
5 setLocalDescription 记下来,同时开始收候选
6 经信令发出去 对面拿到 offer
7 setRemoteDescription 认出对面的 SDP
8 createAnswer 生成回应
9 ontrack 轨道到了,接进 video
**前五步是推的一方自己准备,第 6 步把 offer 送出去,后三步是收的一方接住。**
**中间那一步(经信令发出去)不在浏览器里,这也是为什么必须有服务器。**
**注意 `setLocalDescription` 之后要等 `icegatheringstate` 变化才能拿到完整候选,**
**急着发出去只会发一个空候选表,这叫 trickle,后面那条候选得单独补发。**
**② 真拆一份浏览器的 offer**
**沙箱里没有浏览器,抓不到真的 offer,**
**所以下面这份是按 Chrome 的真实格式誊抄的:字段一个不少、顺序一致、**
**PT 号也是 Chrome 真会给的那几个(111 给 opus、96 给 VP8、97 给 H264)。**
**先把两条 m 段各自的编码表抠出来:**
**全局部分 6 行,m 段 2 条。**
媒体 PT 号 编码数 编码清单
-------------------------------------------------------------
audio 111 63 2 111=opus、63=red
video 96 97 98 3 96=VP8、97=H264、98=rtx
**注意 PT 号:111、63、96、97、98。这些号是发起方随手定的,不是标准号。**
**唯一固定的是 0 到 95 那一段(96 以上才留给动态分配)。**
**所以看 SDP 永远要顺着 `a=rtpmap` 查表,不能记「96 就是 VP8」。**
**③ 真 diff 一次 offer 与 answer**
**answer 不是照抄,SRS 在里面做了三处改动,还有一处故意不动。这里逐项比:**
项目 offer answer 变化
---------------------------------------------------
audio 编码数 2 1 砍掉 1 个
video 编码数 3 1 砍掉 2 个
**视频这边,offer 给了 3 个编码,answer 只要了 1 个。**
**被丢掉的是:96(VP8)、98(rtx)。**
**这里有个细节值得盯:被丢掉的 `98` 是 rtx,它的 `apt=97` 说明它是 97 的重传通道。**
**SRS 只要 97 不要 98,意味着这条路上一旦丢包就是花屏,没有重传。**
**这也是为什么 WebRTC 在弱网下画质掉得比 RTMP 快——不是它不行,是这套配置没开重传。**
**另外几行的对照,重点看哪一行动了、哪一行没动:**
行 offer answer
------------------------------------------------------
a=setup actpass active
a=sendonly/recvonly sendonly recvonly
a=group:BUNDLE BUNDLE 0 1 BUNDLE 0 1
**`sendonly` 变 `recvonly` 最好理解:推的人说「我给」,收的人说「我要」。**
**`setup` 从 `actpass` 变 `active` 就有点绕:**
**`actpass` 的意思是「我主动被动都行,你定」;**
**SRS 回 `active`,等于接下这个活——DTLS 握手由服务器先发起。**
**如果两边都回 `active`,握手会僵住,谁也等不到对方。**
**这是自建信令时最常见的死锁,现象是连接一直停在 connecting。**
**BUNDLE 那行两边一样,都是 `0 1`。**
**它的意思是「音视频挤在同一个端口上走」,靠 mid 号区分。**
**如果不 BUNDLE,一路流就要一对端口,观众一多端口就不够用了。**
**④ 两条铁律:顺序不能换,号码不能改**
**第一,answer 的 m 段必须和 offer 一一对应、顺序相同。**
**这不是约定,是 SDP 规范——m 段是靠位置匹配的,`mid` 只是辅助。**
**把 answer 里 audio 和 video 两段对调,浏览器会认为「音频那条我要的是 H264」。**
**第二,每个编码的 PT 号由 offer 方定,answer 只能沿用,不能改号。**
**如果 SRS 把 video 的 97 改回 96,浏览器会去 96 的 rtpmap 里找,**
**SRS 的 answer 里 96 又没定义,结果就是「收到了包但不知道怎么解」。**
**这两条合起来解释了一类很难查的故障:**
**SDP 语法完全合法、ICE 也连上了、DTLS 也通了,就是黑屏。**
**查这种问题不要看日志,直接把两边的 SDP 并排贴出来逐行对。**
**⑤ 一次会话到底交换了多少字节**
项目 字节 条数
--------------------------------------------
offer(含两条 m 段) 1324 1
answer 951 1
候选(trickle 逐条发) 354 3
合计 2629 5
**合计 2629 字节,五条消息。**
**其中 87% 是那两份 SDP——因为里面全是重复的 ice-ufrag、fingerprint。**
**而 ICE 自己跑一次连通性检查,一个包就 100 字节出头,要试几十上百个组合。**
**所以建立连接的耗时里,真正花在信令上的很少,大头在 ICE 试路。**
**这也解释了一个现象:**
**同一对浏览器,在同城机房 100 毫秒就出画,跨运营商要 3 秒。**
**差的不是 SDP 传输,是候选配对要试多少条路、每条路等多久超时。**
**想快就多给候选(TURN 兜底),想省钱就少给(只留 host 与 srflx)。**
**WebRTC 的体验从来不是「代码写得好不好」,是「路修得够不够」。**
offer 的全局部分 6 行,m 段 2 条,一条 audio 一条 video。 audio 有 2 个编码(111 是 opus、63 是 red),video 有 3 个(96/97/98)。 和 answer 一比,视频砍掉两个:96 的 VP8、98 的 rtx。 所以 SRS 只收 H264,而且不开重传。 一份 offer 1324 字节、一份 answer 951 字节,加三条候选一共 2629 字节。 其中 87% 是那两份 SDP,因为里面全是重复的 ice-ufrag 与 fingerprint。 而 ICE 自己跑一次连通性检查,一个包就 100 字节出头,要试几十上百个组合。 所以建连耗时的大头在 ICE 试路,不在信令。
30 秒实验
- 把 answer 里两条 m 段对调,猜会怎样——浏览器认为音频那条要的是 H264,协商失败,黑屏。
- 把 answer 里 video 的 97 改成 96,猜会怎样——96 在 answer 里没定义,收到包也解不了。
- 让双方都回
setup:active,猜连接停在哪——一直停在 connecting,DTLS 谁也不先发起,互相等。
常见误区
- 「PT 96 固定就是 VP8」——号码是发起方定的,必须查 rtpmap。
- 「answer 可以只回它要的那条 m 段」——必须一一对应,顺序也要一致。
- 「信令传得慢所以连接慢」——大头在 ICE 试路,信令才几 KB。
自测三问(点开对答案)
1. `setLocalDescription` 之后候选什么时候齐? —— **`icegatheringstate` 变成 complete**,之前只能靠 trickle 补发。 2. `setup` 从 actpass 变 active 意味着什么? —— **DTLS 握手由服务器先发起**,浏览器等它。 3. 为什么 rtx 被丢掉会影响弱网画质? —— **rtx 是重传通道**,没有它丢包就直接花屏。- ☐ (本章完,下一节 14.1 直播系统架构简介)