想象你刚到一座陌生城市,约了一位刚认识的朋友见面喝咖啡。你不知道他住哪栋楼,也没存过他的手机号,连他到底在不在同一个城区都不确定。唯一能指望的,是你们恰好认识同一位“城市邮局的管理员”。于是你们先托管理员互传名片——名片上写着各种可联系地址:办公室座机、手机、小区快递柜、前台的转告电话。拿到名片后,你按顺序试拨:手机通了就直接聊,不通就试座机,再不行去快递柜留言,最不济请邮局的人帮忙转交。这套“传名片、试拨号、兜底中转”的流程,正是 WebRTC 里“信令服务器与 ICE 候选收集”在做的事:信令服务器是那座邮局,ICE 是那套试拨手册,NAT、STUN、TURN 则是城市里的收发室、电话局和公共中转站。读完这篇,你会从零理解一次音视频通话是怎样从“互不相识”走到“顺利连线”的。
信令与 ICE 决定了 WebRTC 的接通率和建连速度,是实时音视频项目中排查“打不通、卡爆音”的第一站。掌握它,你能独立设计信令协议、定位 NAT 穿透问题,并为 AI 音视频应用选型给出有理有据的工程判断。
可视化演示
展示WebRTC信令服务器架构:两个客户端通过信令服务器交换SDP Offer/Answer,收集host/srflx/relay三类ICE候选并进行连通性检查建立P2P连接。支持信令流程与候选类型两种模式。
本节配有交互动效(进入视口后自动加载,约 3KB)。
WebRTC信令交换与ICE候选收集 信令服务器中转SDP Offer/Answer协商 ICE收集host/srflx/relay候选并检查连通性 ICE=STUN+TURN+ConnCheck ICE框架实现NAT穿透
信令协议设计
邮局要帮忙送信,必须先有一套统一的“信封规范”:收件人写在哪、寄件人是谁、信纸怎么折,邮差才不会送错。WebRTC 里的信令协议正是这套规范。它不传输画面和声音,只负责在通话双方之间传递“自我介绍”和“约定事项”——双方各有什么能力、想用什么格式通话,都靠它来对齐。
信令的核心载体是 SDP(Session Description Protocol)会话描述。它像名片上的一句话:“我会说普通话和粤语,你选一个。”一方先发出 Offer,说明自己支持的音视频编码、码率、传输端口;另一方收到后回应 Answer,表示接受并补充自己的参数。这个一来一回的过程,就是 Offer/Answer 协商,也是所有通话开始前的“打招呼”。
信令通道最常用的载体是 WebSocket:双向、全双工、低延迟,天然适合持续交换小消息。一条信令消息通常是一个 JSON 对象,包含 type(消息类型:offer/answer/ice-candidate/bye 等)、from/to(发送方与接收方)、payload(SDP 或候选数据)、seq(序号,防止乱序)。光有格式还不够,还要有状态机:空闲 → 已发 Offer → 已收 Answer → 连接中 → 已连接,每一步都配超时和异常分支,防止“聊到一半失联”。
所以“邮票”和“信封”看起来琐碎,却决定了整封信能否准时送达。信令协议设计得好,就等于给后续的 ICE 环节铺了一条最顺的路——邮局这条线稳了,接下来的“找地址、试电话”才有意义。
- 三次往返Offer、Answer、最终确认各占一次 WebSocket 往返时延,这是信令协商的最小开销。
- ICE 并行开销候选收集与连通性检查可以和信令交换同时进行,公式取其最长路径的等待时间。
- 优化方向若网络 RTT 大或信令服务器处理慢,建连时间会明显变长,因此生产上优先选择 WebSocket 并就近部署节点。
候选类型分析
名片上的联系地址写得越全,找到对方的概率就越大。ICE 候选就是“名片上的地址”,一共三类。host 主机候选,像你办公桌上的座机——直接用本机网卡的 IP 和端口,最直接最快速,但只有在同一局域网内才能打通;srflx 反射候选,像收发室保安告诉你“外人拨这个号码能找到你”——你向 STUN 服务器问出自己在外网被看到的地址;relay 中继候选,则是城市公共快递柜的地址——谁都能用,但总归要绕一段路。
收集这三类地址的做法各不相同:主机候选直接枚举本机所有网卡(WiFi、4G、虚拟网卡),一条网络一个;反射候选需要向 STUN 服务器发送 Binding 请求,服务器会回答“从外面看,你的地址是 xxx:端口”;中继候选则是向 TURN 服务器申请一个转发地址,TURN 服务器把媒体包接力转给对端,代价是消耗服务器带宽。
既然地址有“快慢优劣”,就得排个优先级,让最可能成功的地址先被尝试。RFC 5245 给出经验权重:类型偏好 host=126、srflx=100、relay=0;同类型若有多条网卡,再用本地偏好细分;最后用组件号微调(RTP 是组件 1,RTCP 是组件 2)。三者相乘叠加,得出最终优先级。
于是每个人的名片都厚了起来,但每一行地址都不是随手写的——优先级就是试拨顺序。这也是工程师们常说“优先走 P2P、TURN 只做最后退路”的原因:能直连就不绕路,绕路也要选最短的那条。
- 类型偏好三类候选的基础权重,host 最高、relay 最低,决定排序的第一位。
- 本地偏好同类型多个候选时的二级细分权重,多网卡场景下用来区分“先试哪张卡”。
- 组件号修正组件号小的优先,保证 RTP 媒体流排在 RTCP 控制流之前。
连通性检查
两边都拿到名片后,接下来是“秘书”的工作:把两张名单交叉配对,然后按顺序试拨。假设 Alice 有 3 个候选、Bob 有 2 个,两两组合最多得到 6 个“候选对”(candidate pair)。每个候选对都是一种可能的通路,ICE 会为每个候选对计算优先级,然后从高到低逐个探测。
探测的方式是发送 STUN Binding 请求:向对端候选地址发一个“你在吗”的探测包,若在规定时间内收到响应,就说明这条链路真实可用——就像秘书拿起话筒拨号,听到“嘟——”才确认这个号码是真的。检查过程中有两个角色:controlling(控制方)和 controlled(被控制方)。控制方负责做最终决策,决定选哪条路;被控制方只配合回应。若双方都以为自己是控制方,就用一个随机 tie-breaker 数字决出胜负,保证只有唯一控制方。
候选对的优先级不是简单相加,而是“小者优先”:公式中权重最大的项是双方候选优先级里较小的那个。为什么要这样设计?因为整条链路的可用性取决于“最弱的一环”——Alice 的 host 地址再快,Bob 那边只有 relay 地址,那这趟也只能走中继。检查全部就绪后,控制方执行提名(nomination):激进模式下第一条连通的路直接当选;常规模式则等所有候选对检查完,再选最优的一条。
所以一次通话看起来是“秒连”,背后其实是秘书们拿着试拨清单,把每对地址都试了一遍。连通性检查就是这套“试拨电话”——通了记下,不通翻页,直到找到那条真正能走的路。
- 短板优先以双方优先级中的较小值为最大权重,体现“最弱一环决定链路可用性”。
- 长板次之较大值作为二级权重,在短板相同时,更优秀的那一方获得更高排序。
- 决胜位当 G 大于 D 时加 1,确保平局时控制方略占优势,避免排序歧义。
失败重试机制
电话会占线、会空号、会中途掉线,地址也一样。ICE 的试拨不是拨一次就完事,而是带超时、带重试、带降级预案的。
每个连通性检查都有一个 RTO(重传超时)期限:初始值取 500 毫秒和 2 倍 RTT 中的较大者。若超时未收到响应,检查方会重传,并按指数退避拉长等待时间,直到 30 秒上限。重传达到次数上限仍无响应,这个候选对就被标记为失败,转去尝试下一对。这就像电话拨不通时你不会干等,而是隔一会儿再拨一次,越等越久,最后放弃换下一个号码。
当 host 和 srflx 候选全部失败时,降级顺序就“倒过来走”——最终落回 relay 中继候选。TURN 中继虽然有额外延迟、还要占用服务器带宽,但它对对称 NAT 和严格防火墙几乎百分百有效,是真正的“最后一道保险”。即使所有候选都失败,前端还可以触发 ICE Restart:重新收集候选、重新发起 Offer/Answer,从头再来一遍,相当于“换个手机号再打一次”。
失败不可怕,可怕的是没有预案。重试与降级机制让通话建立过程像走迷宫:先试直线,被挡就绕道;实在绕不过去,就从邮局的公共通道走——无论哪条路,最终目标是让两边顺利接上头。
- 初始超时以 500 毫秒为下限、2 倍 RTT 为基准,避免在慢网络上过早放弃。
- 指数退避每次失败等待翻倍,避免大量重试同时涌向网络造成雪崩。
- 最大上限等待时间封顶 30 秒,超时后彻底放弃当前候选对。
生产部署路径
演示环境里一台电脑就能跑通信令服务器,但生产环境要考虑的是:邮局够不够用?高峰期会不会排队?中转站带宽扛不扛得住?这就是从概念验证(POC)走向生产部署时必须回答的问题。
部署前先做容量规划。信令服务器处理的是小消息,瓶颈在连接数和消息吞吐量;TURN 服务器的瓶颈则在带宽。经验估算:一路 720p 视频经 TURN 转发约需 1.5-2 Mbps,假设单机带宽 1 Gbps,理论并发约 500 路。信令节点虽不碰媒体,但要按“在线用户数 × 并发率”预留连接数,并留出 2-3 倍冗余应对突发。
拓扑上,信令节点要做水平扩展,前面挂负载均衡;TURN 服务器按地域就近部署,并配置多节点 failover。上线后必须盯好的监控指标包括:在线连接数、信令消息延迟、ICE 检查失败率、TURN 中继占用率、端到端建连耗时(p95)。这些指标能直接回答“用户为什么打不通电话”——是信令慢了、NAT 穿透失败,还是 TURN 满了。
从概念验证到生产,本质是把“能通”升级成“总能通”。邮局多了、线路备了、还能实时看见哪条街拥堵,通话体验才谈得上稳定——这就是 POC 与生产之间那条看不见的鸿沟。
- 单节点容量服务器总带宽除以每路通话平均码率,向下取整得到并发上限。
- 并发用户数注册用户总数乘以同时通话比例,得到峰值并发需求。
- 节点数量并发需求除以单节点容量,向上取整并加冗余。
工程实践建议
机制都懂了,接下来是“用得好”这门手艺。工程实践里,同样一套信令和 ICE,不同的选型与配置,体验可以差出数倍。
信令选型上,WebSocket 依然是主流,胜在生态成熟、全双工、低延迟;若对首包延迟和头部开销更敏感,可以评估 WebTransport。协议设计从第一天就要带版本号与能力协商字段,避免前后端迭代时互相不认识。安全方面:信令通道必须走 TLS 加密,加入房间要 token 鉴权,SDP 中不要暴露多余的内网 IP,防止信息泄露。
性能优化上,优先开启 Trickle ICE——候选边收集边发送,不用等全部就绪,通常能缩短 30%-50% 的建连时间;服务端如果是 SFU/MCU 星型转发架构,可用 ICE-lite 模式减少交互开销;对高频拨打对象可以做“预连接”,在后台提前建立 ICE 会话。排查问题时打开 chrome://webrtc-internals 看 stats:若发现大量通话依赖 relay 候选,说明 NAT 穿透配置待优化,应检查 STUN 服务器连通性与防火墙策略。
工程实践的金标准是“让用户无感”。复杂留给系统,流畅留给用户——所有优化落回同一个目标:无论网络多复杂,让通话像面对面聊天一样自然。
- 采集与编码麦克风/摄像头的采集时间与编码器打包时间,通常是固定开销。
- 网络与抖动P2P 直连时最小,走 TURN 中继时多一跳;抖动缓冲为平滑播放额外增加等待。
- 缓冲播放接收端抖动缓冲的等待时间,是延迟预算的最后一块拼图。
AI 应用场景
在线面试与互动课堂
阶段:实时音视频底层链路搭建
解决的问题:面试双方往往身处企业办公网,NAT 类型复杂、防火墙严格;若所有媒体都走服务器转发,带宽成本高且时延感人。信令服务器 + ICE 组合让大多数流量走 P2P 直连,TURN 只兜底极小比例。
具体案例:某在线面试平台使用 WebSocket 信令服务器交换 SDP 与 Trickle 候选,使 95% 的通话建立 P2P 直连;剩余 5% 对称 NAT 场景自动切换 TURN 中继。同时 AI 实时字幕通过信令通道带外传输字幕文本而非音视频,字幕延迟被压缩到 200 毫秒以内。
AI 数字人实时语音对话
阶段:端侧交互与云端大模型实时链路
解决的问题:数字人应用需要低延迟地把用户语音上行到云端大模型,再把生成的语音下行回传到端侧;用 WebSocket 传音频分片会有乱序与队头阻塞,需要 UDP 媒体通道。
具体案例:某 AI 陪伴应用让用户与数字人视频通话,接入层用信令服务器完成“用户 ↔ AI 节点”的候选配对,AI 节点侧使用 ICE-lite 简化交互;媒体走 P2P 后的往返延迟从 WebSocket 方案的 400 毫秒降到 120 毫秒,AI 回复的“思考延音”大幅缩短。
远程医疗会诊与网络诊断
阶段:会话质量监测与智能调优
解决的问题:医疗会诊对清晰度和抖动要求极高,传统方案在弱网下“一刀切降码率”会牺牲影像细节;需要根据 ICE 候选类型和丢包统计做精细化诊断与自适应。
具体案例:会诊平台利用 ICE 候选分布判断网络类型——当 srflx 反射候选检查长期失败时,系统自动切换到 TURN 中继并同步调低帧率;AI 辅助分析 stats 面板,生成“建议开通 UDP 端口 / 升级企业防火墙策略”的可执行报告,将一次会诊的建连失败率从 8% 降至 1% 以下。
Python 演示
"""信令服务器与 ICE 候选收集简化模拟"""import random class ICECandidate: """一个 ICE 候选地址(名片上的一个联系地址)""" def __init__(self, ip, port, cand_type, component_id=1, local_pref=None): self.ip = ip self.port = port self.cand_type = cand_type self.component_id = component_id self.local_pref = local_pref self.priority = self.calc_priority() def calc_priority(self): type_pref = {"host": 126, "srflx": 100, "relay": 0}[self.cand_type] if self.local_pref is None: self.local_pref = 65535 if self.cand_type == "host" else 32767 return (2 ** 24) * type_pref + (2 ** 8) * self.local_pref + (256 - self.component_id) def __repr__(self): return "%s(%s:%d)" % (self.cand_type, self.ip, self.port) class SignalingServer: """邮局: 只转发控制消息, 不碰媒体数据""" def __init__(self): self.log = [] def route(self, msg): self.log.append(msg) return msg class ICEAgent: """每个对端都有一个 ICE 秘书: 收集, 配对, 试拨""" def __init__(self, name, candidates): self.name = name self.candidates = sorted(candidates, key=lambda c: c.priority, reverse=True) self.role = "controlling" self.selected_pair = None def make_pairs(self, remote_candidates): pairs = [] for local in self.candidates: for remote in remote_candidates: g = local.priority d = remote.priority pair_prio = (2 ** 32) * min(g, d) + 2 * max(g, d) + (1 if g > d else 0) pairs.append((pair_prio, local, remote)) pairs.sort(reverse=True) return [(local, remote) for _, local, remote in pairs] def connectivity_check(self, pairs): for local, remote in pairs: if local.cand_type == "relay" or remote.cand_type == "relay": ok = True elif local.cand_type == "host" and remote.cand_type == "host": ok = True else: ok = random.random() > 0.3 if ok: self.selected_pair = (local, remote) return local, remote return None def main(): random.seed(42) alice = ICEAgent("Alice", [ ICECandidate("192.168.1.10", 50001, "host"), ICECandidate("203.0.113.5", 50002, "srflx"), ICECandidate("198.51.100.7", 3478, "relay"), ]) bob = ICEAgent("Bob", [ ICECandidate("10.0.0.2", 50011, "host"), ICECandidate("198.51.100.7", 3478, "relay"), ]) server = SignalingServer() # 阶段一: 交换 SDP 名片 server.route({"from": "Alice", "to": "Bob", "sdp": "offer"}) server.route({"from": "Bob", "to": "Alice", "sdp": "answer"}) # 阶段二: Trickle 收集候选, 全部经信令服务器转发 for cand in alice.candidates + bob.candidates: server.route({"from": "?", "to": "?", "candidate": str(cand)}) # 阶段三: 配对 + 连通性检查 pairs = alice.make_pairs(bob.candidates) print("候选配对数量:", len(pairs)) winner = alice.connectivity_check(pairs) if winner: print("连通成功:", winner[0], "<->", winner[1]) else: print("全部失败, 需要 TURN 中继兜底") if __name__ == "__main__": main()
class ICECandidate:定义“名片”模型:一个候选就是一条可被联系的通路,包含地址、端口和类型。type_pref = {"host": 126, "srflx": 100, "relay": 0}[self.cand_type]类型偏好直接映射为公式权重,host 最高、relay 最低。server.route({"from": "Alice", "to": "Bob", "sdp": "offer"})信令服务器只转发控制消息,媒体数据完全不经手。for cand in alice.candidates + bob.candidates:Trickle 思想:候选不等齐,先收集到就先发给对方。pairs.sort(reverse=True)按候选对优先级从高到低排序,决定试拨顺序。if local.cand_type == "relay" or remote.cand_type == "relay":中继候选永远可用,是最后的兜底通道。random.seed(42)固定随机种子,让“反射候选 30% 概率失败”的模拟结果可复现。
常见误区
误区我一直以为信令服务器就像视频会议的中转中心,所有画面和声音都要从它这儿过一遍,所以带宽要按视频码率来算。
正解信令服务器只交换“名片”——SDP 会话描述、ICE 候选、控制指令,媒体流建立后通常走 P2P 直连,根本不经过服务器。邮局帮你递名片,不会帮你们俩站在后面聊天。带宽需求上,信令关心的是连接数和消息吞吐量,而不是媒体码率。
为何常见:许多架构图把服务器画在两端中间,箭头指向又粗又密,让人误以为它在转发媒体,其实只有 TURN 中继才做转发。
误区ICE 不是用来穿透 NAT 的吗?那两边都有公网 IP,直接连不就行了,何必还要收集候选。
正解公网 IP 不等于一定能直连。中间可能隔着防火墙,只放行特定端口和协议;也可能运营商做了端口限制。ICE 是“多条腿走路”的组合方案,公网 IP 只是 host 候选之一,仍然要经过候选配对和连通性检查来验证这条路真的通。
为何常见:把 ICE 简单等同于 NAT 穿透,忽略了防火墙、端口过滤等同样影响连通性的因素。
误区只要集成了 WebRTC 和 ICE,任何网络环境下都能直连成功,TURN 只是可选项,装不装无所谓。
正解对称 NAT 和部分企业防火墙会直接让 STUN 反射地址失效——你在内网发出的端口和外部看到的映射端口不同,每次请求都不一样。这种情况下 ICE 会试完所有候选对后失败。TURN 中继是真正的“兜底保险”,虽然绕路、费带宽,但保证通话能建立。
为何常见:ICE 全称带 Connectivity Establishment,名字听上去像“必定建立”,容易让人忽略它只是尽力而为。
误区流程上应该是先把 host、srflx、relay 所有候选都收集齐,再发 Offer 给对方,不然后面新发现的地址就白收了。
正解Trickle ICE 允许边收集边通知——先发主机候选,STUN/TURN 的结果陆续通过信令通道发补充消息,对端实时把新候选加进候选池。这就像先把手机号发过去聊起来,座机号想起来再补发,不用等名片印好才出门。
为何常见:早期 RFC 5245 默认是全量收集模式,不少老教程仍按旧流程讲解,没跟上 Trickle 的演进。
误区信令服务器不就是个 WebSocket 中转站嘛,能收发消息、能把 A 的消息转给 B,就算完工了。
正解生产级信令远不止“转发”:要管理会话状态机(谁在哪个房间、Offer/Answer 是否乱序)、校验消息归属(ICE 候选必须关联到正确的会话和房间)、处理断线重连与心跳超时,还要做鉴权、版本兼容、消息节流。Demo 里的 echo 服务器掩盖了这些真实复杂度。
为何常见:教程 demo 往往只演示“收到消息就转发”,让人误以为状态管理和异常处理都是多余的。
面试题
Q1. WebRTC 中信令服务器的核心职责是什么?
- A.转发音视频媒体数据
- B.交换会话描述与候选信息← 正确答案
- C.负责音视频编解码处理
- D.存储全部通话记录数据
答案:B。信令服务器负责交换 SDP 会话描述和 ICE 候选等元数据,像邮局递名片;媒体数据走 P2P 直连,不经由信令服务器。选 A 是把信令服务器误当成媒体中转,C 是编解码器的工作,D 与信令职责无关。
Q2. 下列哪类 ICE 候选的优先级最高?
- A.host 主机候选← 正确答案
- B.srflx 反射候选
- C.relay 中继候选
- D.三类候选完全一致
答案:A。ICE 优先级按“越直接越优先”设计,host 候选走本机网卡直连,权重 126 最高;srflx 反射候选权重 100 次之;relay 中继候选权重 0 最低,只作兜底。D 明显错误,三者权重完全不同。
Q3. STUN 服务器的主要作用是什么?
- A.中继转发媒体流量
- B.发现公网映射地址← 正确答案
- C.管理会话的 SDP 状态
- D.对通话内容加密保护
答案:B。STUN 通过 Binding 请求让客户端知道自己在外网被映射到的 IP 和端口,即生成 srflx 反射候选,相当于问收发室“外人看到的我是什么地址”。A 是 TURN 的职责,C 是信令服务器的职责,D 是 DTLS-SRTP 的职责。
Q4. 面对对称 NAT 导致的直连失败,最稳妥的兜底方案是?
- A.重新收集主机候选
- B.关闭 ICE 穿透功能
- C.使用 TURN 中继候选← 正确答案
- D.改用 HTTP 长轮询
答案:C。对称 NAT 下 STUN 反射地址每次映射都不同,直连基本无解。TURN 中继候选让媒体经服务器接力转发,绕过 NAT 限制,是 RFC 推荐的最后兜底。重收主机候选解决不了 NAT 映射问题,关闭 ICE 会直接断连。
Q5. ICE 候选对优先级公式中起决定性作用的分量是?
- A.较小一方的优先级← 正确答案
- B.较大一方的优先级
- C.组件编号本身大小
- D.双方约定的随机值
答案:A。候选对优先级公式中,权重最大项是 min(G, D),即双方优先级中的较小值——整条链路的可用性由“最弱一环”决定。较大值只是二级权重,组件号是微调,tie-breaker 只用于角色判定,与优先级无关。
Q6. 关于 Trickle ICE,下列说法正确的是?
- A.候选边收集边发送← 正确答案
- B.缩短连接建立时间← 正确答案
- C.必须等候选全部齐
- D.对端动态加入候选池← 正确答案
答案:ABD。Trickle ICE 的核心是“不等齐就出发”:先发已收集的候选,后续候选通过信令补充,对端动态加入候选池,从而让建连时间缩短 30%-50%。C 说的是旧的全量收集模式,与 Trickle 相反。
Q7. 连通性检查主要依赖哪类协议报文?
- A.发 HTTP 请求报文
- B.发 STUN 探测报文← 正确答案
- C.发 DNS 查询报文
- D.发 ICMP 探测报文
答案:B。ICE 连通性检查复用 STUN Binding 请求——向对端候选发探测包并等待响应,本质上是“试拨电话”。HTTP 基于 TCP 不适用于 UDP 媒体路径探测,DNS 用于域名解析,ICMP 常被防火墙屏蔽且与媒体路径不一致。
Q8. 生产环境部署信令与 TURN 服务时需考虑?
- A.信令节点水平扩容← 正确答案
- B.TURN 带宽容量估算← 正确答案
- C.媒体传输免设超时
- D.ICE 失败率监控告警← 正确答案
答案:ABD。生产部署要按容量公式规划信令节点数量、按并发码率估算 TURN 带宽,并持续监控 ICE 失败率与 TURN 占用率。“媒体传输免设超时”违背重试机制设计,连通性检查必须有 RTO 与重传上限。
Q9. ICE 候选降级尝试的正确顺序是?
- A.中继反射主机依次尝试
- B.主机反射中继依次尝试← 正确答案
- C.反射中继主机依次尝试
- D.按随机排列一一尝试
答案:B。候选尝试顺序与优先级一致:先 host 主机候选(最直接),再 srflx 反射候选(NAT 映射),最后 relay 中继候选(兜底)。随机排列会破坏优先级设计,导致大量不必要的绕路。
Q10. WebRTC 中“控制方”的 tie-breaker 主要用于?
- A.决定谁先发送媒体流
- B.角色冲突时裁定控制方← 正确答案
- C.提高本端候选优先级
- D.决定中继服务器选址
答案:B。当双方都认为自己是控制方、角色冲突时,双方交换各自随机生成的 tie-breaker(64 位随机数),较大者胜出为唯一控制方,避免提名决策混乱。媒体发送顺序由协商决定,tie-breaker 不涉及优先级或 TURN 选址。