1.2 架构与三大接口 本节摘要:RTCPeerConnection 这个名字自带劝退气质——名字长、方法多、回调连环。但拆开看,WebRTC 的架构异常清晰:上层三个 JavaScript 接口分管「拿媒体、建连接、传数据」,下层浏览器内核把编解码、加密、打洞全部打包。本节给出全册通用的分层地形图,并点明信令通道为何置身架构之外。读完这一节,你就能给任何一段 WebRTC 代码定位它站在架构的哪一层。 学习目标 阅读完本节,你应当能够: 说出三大核心接口各自封装了什么能力、彼此如何传递对象; 画出从 JS API 到媒体引擎、传输层、加密层的浏览器内部分层; 解释信令为什么不在浏览器内核里、它和媒体通道是什么关系; 用不到三十行代码完成一次媒体采集并展示到页面。
本节摘要:RTCPeerConnection 这个名字自带劝退气质——名字长、方法多、回调连环。但拆开看,WebRTC 的架构异常清晰:上层三个 JavaScript 接口分管「拿媒体、建连接、传数据」,下层浏览器内核把编解码、加密、打洞全部打包。本节给出全册通用的分层地形图,并点明信令通道为何置身架构之外。读完这一节,你就能给任何一段 WebRTC 代码定位它站在架构的哪一层。
阅读完本节,你应当能够:
很多教程把 WebRTC 讲成「一个 API」,这是后续一切困惑的源头。真实的设计是三个接口、三种数据、三条通道:
三者关系一句话:轨道从采集接口流出,交给连接接口发送;数据通道是连接接口顺手开的另一条车道。 分三个接口而非一个,是因为三种数据的可靠性诉求完全不同——媒体可以容忍丢帧但不能忍受重传延迟,文件传输恰恰相反。接口分开,策略才能分开。

从上往下过一遍这张地形图。应用层只有三个接口和你的业务逻辑;内核层是浏览器厂商用 C++ 实现的引擎(Chrome 系基于开源的 libwebrtc),从设备捕获、音频前处理(回声消除、噪声抑制)、视频编解码,到 RTP 打包、拥塞控制、ICE 打洞、DTLS 加密,全部在这层完成;架构外那条虚线框是信令——它只是你自己的 WebSocket 服务,浏览器对它的存在一无所知。
为什么信令不能内建?回看 1.1 节的留白设计:内建信令等于替所有应用指定一个「中央登记处」,与点对点的哲学冲突。所以你会看到 RTCPeerConnection 的构造函数干干净净,不含任何「对方是谁」的参数——对方地址是后来通过信令通道拿到的候选地址,一个一个喂进去的。
信令与媒体的关系,用一个类比:信令是婚礼司仪,媒体是婚后生活。司仪只负责让两家交换信物(SDP)与联系方式(候选地址),办完事就退场;之后两家的日子(媒体数据)自己过,与司仪再无关系——甚至司仪中途倒闭,通话照常进行。
架构讲再多,不如让摄像头亮起来。下面这段代码是从零开始的第一步,也是第 2 章的预告:
// 最小采集示例:打开摄像头与麦克风并显示在页面上 async function startPreview() { try { // 约束对象:只带基础项,详细参数留给第 2 章 const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true, }); // 把媒体流挂到 video 标签,页面即可看到自己 document.querySelector('video').srcObject = stream; } catch (err) { // 权限拒绝、设备占用、无设备都会走到这里 if (err.name === 'NotAllowedError') { console.log('用户拒绝了摄像头授权'); } else { console.log('采集失败:', err.name); } } }
三个工程细节值得现在就记住。其一,getUserMedia 必须在 HTTPS 或 localhost 环境下才存在,用 http 打开页面会直接报 mediaDevices 为 undefined——大量「代码没错但不工作」的疑问源于此。其二,轨道对象是引用:同一个 track 可以同时给 video 预览和 RTCPeerConnection 发送,浏览器内部自动分流。其三,采集结果不是「一路视频」而是轨道集合,音视频各自独立成轨,这也为后面「只关麦不关摄像头」这类操作留下空间。
第二个小示例,展示三大接口如何咬合——把轨道塞进连接:
// 三大接口的咬合:轨道交给连接,数据通道由连接创建 const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.example.net' }], // 第 4 章展开 }); const stream = await navigator.mediaDevices.getUserMedia({ video: true }); stream.getTracks().forEach((track) => { pc.addTrack(track, stream); // 采集接口的产物 -> 连接接口 }); const dc = pc.createDataChannel('chat'); // 数据通道从连接接口长出
💡 关键直觉:把 RTCPeerConnection 想象成一台「加密传真机」,addTrack 是把文件放进进纸口,信令是你打电话问对方传真号,DTLS 握手是双方核对传真暗号。传真机本身从不管你的电话是怎么打的。