7.2 端到端完整实战


文档摘要

7.2 端到端完整实战 本节摘要:前面的零件都在这一节总装:一次完整可跑的 1v1 通话,从信令接入、采集、协商、连接到渲染与挂断,全代码走读;随后给出八个真实项目里最高频的翻车点清单。本节的目标不是再讲一遍原理,而是把原理变成一条你能亲手复制、运行、改坏、修好的完整链路。 读完本节你能 阅读完本节,你应当能够: 按「信令层、协商层、媒体层」三段结构组织通话代码,而不是把所有逻辑糊在一起; 完整写出主叫与被叫两侧的协商流程代码; 正确处理远端轨道的到达时机与渲染; 实现挂断与资源释放,避免设备占用残留; 对照八个翻车点自检代码,逐条说清病因与修法。

7.2 端到端完整实战

本节摘要:前面的零件都在这一节总装:一次完整可跑的 1v1 通话,从信令接入、采集、协商、连接到渲染与挂断,全代码走读;随后给出八个真实项目里最高频的翻车点清单。本节的目标不是再讲一遍原理,而是把原理变成一条你能亲手复制、运行、改坏、修好的完整链路。

读完本节你能

阅读完本节,你应当能够:

  1. 按「信令层、协商层、媒体层」三段结构组织通话代码,而不是把所有逻辑糊在一起;
  2. 完整写出主叫与被叫两侧的协商流程代码;
  3. 正确处理远端轨道的到达时机与渲染;
  4. 实现挂断与资源释放,避免设备占用残留;
  5. 对照八个翻车点自检代码,逐条说清病因与修法。

一、问题与直觉:demo 与产品的差距在哪里

教程代码与产品代码的差距,通常不在主流程——主流程就那几步——而在边角:谁先发起、中途谁挂了、再来一次时上一次的资源清干净没有。本节的实现刻意按「三段结构」组织:信令层只管收发消息(第 3 章的封装原样复用);协商层只管 offer-answer 与候选;媒体层只管采集与渲染。三层各自可测、可换,出问题时能按层定位而不是满文件找。

先看总装图,再逐段落码。

图:1v1 通话总装泳道

图:1v1 通话总装泳道

二、核心实现:三段代码总装

协商层主叫侧(媒体层采集代码见第 2 章,此处直接复用其产物):

class CallSession { constructor(signaling, stream) { this.signal = signaling; this.stream = stream; // 第 2 章 acquireMedia 的产物 this.pc = null; } async start() { this.pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.example.net:3478' }, { urls: 'turn:turn.example.net:3478', username: 'u', credential: 'p' }, ], }); this.wireEvents(); this.stream.getTracks().forEach((t) => this.pc.addTrack(t, this.stream)); // 主叫动作:生成并固化 offer,再经信令送出 await this.pc.setLocalDescription(await this.pc.createOffer()); this.signal.send({ type: 'offer', payload: this.pc.localDescription }); } async accept(remoteOffer) { this.pc = new RTCPeerConnection({ /* 同上 iceServers */ }); this.wireEvents(); // 被叫纪律:先设远端,再加轨道,后作答 await this.pc.setRemoteDescription(remoteOffer); this.stream.getTracks().forEach((t) => this.pc.addTrack(t, this.stream)); await this.pc.setLocalDescription(await this.pc.createAnswer()); this.signal.send({ type: 'answer', payload: this.pc.localDescription }); } wireEvents() { this.pc.onicecandidate = ({ candidate }) => { if (candidate) this.signal.send({ type: 'candidate', payload: candidate }); }; this.pc.ontrack = (event) => { remoteVideo.srcObject = event.streams[0]; // 对端画面在此交付 }; this.pc.onconnectionstatechange = () => { if (this.pc.connectionState === 'failed') this.onFailed(); }; } async onRemoteCandidate(c) { await this.pc.addIceCandidate(c); } async onRemoteAnswer(a) { await this.pc.setRemoteDescription(a); } hangup() { // 释放纪律:先停轨道释放设备,再关连接 this.stream?.getTracks().forEach((t) => t.stop()); this.pc?.close(); this.signal.send({ type: 'bye' }); } }

信令消息的分派只在四行之内(第 3 章的封装):

signal.on('offer', (m) => incomingCall(m.payload)); signal.on('answer', (m) => session.onRemoteAnswer(m.payload)); signal.on('candidate', (m) => session.onRemoteCandidate(m.payload)); signal.on('bye', () => session.hangup());

三、工程实践要点:八个高频翻车点

这八个点覆盖了真实项目里绝大多数「代码看着对、就是不通」:

翻车点 病因 修法
iceServers 忘配或配错 只有 host 候选,跨网必败 第 4 章部署清单对账
被叫先加轨道后设远端描述 answer 与轨道顺序错乱,方向异常 严格「先远端、再轨道、后作答」
只有一端 addTrack 对端无发送内容,黑屏 检查两端轨道结构
ontrack 挂在创建之前的 pc 上 监听丢失,画面永不渲染 先建 pc 再 wireEvents
数据通道 send 早于 open 抛异常,消息丢失 open 后再发,或挂待发队列
挂断只关连接不停轨道 摄像头指示灯常亮 轨道 stop 与连接 close 成对
信令乱序(answer 迟到) setRemoteDescription 抛错 协商代数校验(第 3 章案例)
弱网下重复点呼叫 多个连接并存互相干扰 会话状态机,禁止并发建立

⚠️ 常见坑:把这段代码直接复制两份跑在两个标签页里测试时,本机两个标签页会互相当成「同源同设备」,部分浏览器合并媒体处理,现象诡异。正经自测请用两台设备,或一台设备一个用浏览器一个用无痕窗口加虚拟摄像头。

完整案例:一次「跑得通但挂不掉」的联调

背景:团队按上述骨架联调,通话正常,但挂断后再次呼叫必失败,第二次通话双方都黑屏;重启页面又能恢复一次。

操作:按层排查。信令层消息序列正确;协商层日志显示第二次的 offer 正常送达;媒体层发现第二次采集复用了第一次的 stream 对象——而第一次挂断时轨道已被 stop,处于 ended 终态。把 ended 轨道 addTrack 进新连接,浏览器不报错但发不出任何内容。修复:挂断后置空会话持有的流引用;每次 start 前重新走 acquireMedia 采集新轨道;并在 acquireMedia 内部增加「旧轨道未释放先释放」的防御。

结果:连续多次呼叫与挂断循环测试通过;顺手在挂断流程加了「连接关闭后清空 video 标签的 srcObject」,消除了挂断瞬间最后一帧残留的产品瑕疵。

解读:这类 bug 的教育意义在于轨道的生命周期是「一次性的」:ended 是终态,不因重新使用而复活。资源管理的问题在 WebRTC 里会以「第二次不工作」的形态出现,而第一次永远完美——凡是「第二次才坏」的 bug,优先怀疑上次没清理干净。

变式:接入 SFU 时这份骨架的改动集中在两处:协商对端从「另一台浏览器」换成「SFU 服务器」(协议由服务器定义,通常是收发分离的两种 transceiver 配置);ontrack 会按流多次触发(每人一条),渲染层要按流标识动态管理 video 标签。骨架的分层结构在这里兑现价值——只动协商层与媒体层的接口实现,信令层原样复用。

本节要点回顾

  • 三段结构是防混乱的:信令、协商、媒体各层独立,出问题按层定位。
  • 两侧纪律不同:主叫「生成固化再发」,被叫「先远端、再轨道、后作答」,次序即正确性。
  • 资源释放成对出现:轨道 stop 释放设备,连接 close 摧毁状态,srcObject 清空收尾。
  • 翻车点清单是自检表:八条覆盖绝大多数「代码对但不通」,联调前过一遍。
  • 「第二次才坏」查清理:轨道是终态对象,上一次的残留是下一次的故障源。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U