[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fz6eLlm_nq_EakN-0crdttJr9_TQtl30rhmz0ZJIeyeI":3},{"item":4,"related":40},{"id":5,"type":6,"title":7,"slug":8,"summary":9,"body":10,"coverUrl":11,"productScreenshots":12,"productLinks":13,"authorName":14,"authorUrl":15,"authorSubject":16,"category":17,"tags":22,"sourceLabel":31,"sourceName":32,"sourceUrl":32,"status":33,"seoTitle":32,"seoDescription":32,"canonicalUrl":32,"isFeatured":34,"sno":35,"sortOrder":36,"publishedAt":37,"updatedAt":38,"createdAt":39},"5b92fb4c-c87e-42db-b9fc-3172da6d6655","article","WebRTC：浏览器为什么不用安装软件就能视频通话","webrtc-browser-realtime-communication-explained","WebRTC 把摄像头、麦克风、网络穿透、加密传输和实时自适应组合进浏览器。本文从信令、SDP 与 ICE 讲起，拆解 STUN、TURN、DTLS-SRTP、数据通道和 SFU，并说明实时通信真正难在哪里。","你在浏览器里点开一个视频会议链接，不用安装插件，也不用先把视频上传到某个平台，摄像头和麦克风就能开始工作。WebRTC 就是这件事背后的浏览器能力集合：它提供建立实时音频、视频和数据交换的 API，并把底层连接所需的协议交给浏览器处理。[W3C WebRTC 规范](https:\u002F\u002Fwww.w3.org\u002FTR\u002Fwebrtc\u002F)\n\n![WebRTC 标志](https:\u002F\u002Fcommons.wikimedia.org\u002Fwiki\u002FSpecial:FilePath\u002FWebRTC%20Logo.svg)\n\n不过，“WebRTC 让两台设备直接连接”只是一个好记的开场白，不是完整架构。真实系统还需要信令服务器帮双方交换连接信息，需要 STUN 帮客户端发现自己的公网映射，必要时还要靠 TURN 中继媒体。理解这几层，才能知道为什么一个看似简单的“加入会议”按钮，背后会有这么多状态。\n\n## WebRTC 不是一个协议，而是一组协作部件\n\n可以把一次通话拆成四个问题：\n\n1. **谁想和谁通话？** 这是信令问题，通常由业务服务器通过 WebSocket、HTTP 或其他方式完成。\n2. **双方各自有什么能力？** 摄像头、麦克风、编码器、分辨率和数据通道都要协商。\n3. **网络上哪条路径能走通？** 这由 ICE 负责尝试候选地址。\n4. **建立后如何加密和传输？** 媒体通常走 SRTP，数据通道使用经过 DTLS 保护的传输机制。\n\n信令不属于 WebRTC 规范的核心 API。浏览器不会替你决定“把 Offer 发给谁”，它只生成和消费连接描述。业务系统可以把信令放在自己的服务器上，这也是为什么 WebRTC 仍然需要一个后端，即使音视频最终可能不经过这个后端。\n\n## 从点下按钮到听见声音，中间发生了什么\n\n### 第一步：浏览器申请媒体权限\n\n`getUserMedia` 会触发浏览器的权限模型。用户允许后，页面得到本地音视频轨道；拒绝、设备不存在、设备被其他程序占用，都可能在连接建立前失败。\n\n### 第二步：创建连接并生成 Offer\n\n页面创建 `RTCPeerConnection`，把本地音视频轨道加入连接，再调用 `createOffer`。生成的 SDP 可以理解为一份“我能提供什么、我希望收到什么、我支持哪些编码和传输参数”的描述。\n\n```js\nconst pc = new RTCPeerConnection({ iceServers })\nconst stream = await navigator.mediaDevices.getUserMedia({\n  audio: true,\n  video: true,\n})\n\nfor (const track of stream.getTracks()) {\n  pc.addTrack(track, stream)\n}\n\nconst offer = await pc.createOffer()\nawait pc.setLocalDescription(offer)\nsignalServer.send({ type: \"offer\", sdp: pc.localDescription })\n```\n\n### 第三步：对方返回 Answer\n\n对端收到 Offer 后设置为远端描述，再生成 Answer。双方通过信令交换这些描述，浏览器才知道协商的起点。这个过程不是传输媒体本身，而是在为传输建立共同语言。\n\n### 第四步：ICE 试路\n\n一台设备通常有多个地址：本机局域网地址、路由器映射出来的公网地址，以及 TURN 服务器提供的中继地址。ICE 会收集这些候选地址并进行连通性检查，优先尝试更直接的路径，失败后再回退。\n\n```mermaid\nflowchart TD\n    A[用户允许摄像头与麦克风] --> B[创建 RTCPeerConnection]\n    B --> C[生成 Offer \u002F Answer]\n    C --> D[信令服务器交换描述]\n    D --> E[收集本地与公网候选地址]\n    E --> F{ICE 连通性检查}\n    F -->|直连可用| G[建立点对点媒体路径]\n    F -->|直连失败| H[通过 TURN 中继]\n    G --> I[DTLS 握手与媒体加密]\n    H --> I\n    I --> J[音视频与数据通道运行]\n```\n\n## STUN 和 TURN 分别在解决什么\n\nSTUN 服务器像一个“公网回声器”：客户端向它发请求，服务器告诉客户端“从公网看，你的地址和端口是这个”。客户端可以把这个候选地址交给对方，尝试通过 NAT 映射建立连接。\n\nTURN 则更像中转站。如果双方的网络策略不允许直接打通，或者某些 NAT 映射无法被对端使用，媒体可以先发到 TURN，再由 TURN 转发给另一端。TURN 能提高成功率，但会增加服务器带宽和延迟成本，所以生产系统通常把它作为必要的回退路径，而不是默认让所有媒体都经过中继。\n\n## 媒体通道和数据通道不是一回事\n\n音视频强调持续播放和延迟控制，常见策略是丢掉太晚的帧，避免“为了完整而播放过时画面”。数据通道则可以选择可靠有序，或在某些实时状态场景中接受丢包。WebRTC 的数据通道适合聊天、协同控制、游戏状态和文件传输，但不同场景应该选择不同的可靠性与顺序策略。\n\n媒体质量也不是“协商完就固定”。浏览器会根据丢包、往返时间和可用带宽调整码率、分辨率和帧率。多路视频还可能使用 simulcast 或 SVC，让服务器或接收端选择更合适的层。工程上真正要观察的是 `getStats()` 中的丢包、抖动、RTT、编码时间和解码时间，而不是只看“连接成功”。\n\n## WebRTC 的边界\n\n- 它不替你解决房间、用户身份、录制、转码和消息存储。\n- 点对点不保证一定直连，TURN 中继是很多企业网络里的必要条件。\n- 浏览器权限、自动播放策略和设备切换会影响用户体验。\n- 多人会议如果让所有客户端互相连接，连接数会迅速增长，通常需要 SFU 或 MCU 之类的媒体服务器架构。\n- 端到端加密、录制和服务端转码之间存在真实的系统取舍，不能只用一个“安全”标签概括。\n\n## 一句话带走\n\nWebRTC 的魔法不是“浏览器突然学会了视频通话”，而是把权限、能力协商、NAT 穿透、加密和实时传输组合成了一套可编程能力。直连是理想路径，信令和 TURN 才是让这条理想路径在真实互联网里活下来的基础设施。\n\n## 延伸阅读\n\n- [W3C WebRTC 1.0 规范](https:\u002F\u002Fwww.w3.org\u002FTR\u002Fwebrtc\u002F)\n- [WebRTC 标志图](https:\u002F\u002Fcommons.wikimedia.org\u002Fwiki\u002FFile:WebRTC_Logo.svg)\n- [WebRTC 官方网站](https:\u002F\u002Fwebrtc.org\u002F)","\u002Fuploads\u002F2026-08-09\u002F5a91b562-9609-46a4-ba74-e3df25c719fe.jpg",[],[],"Foundit","https:\u002F\u002Ffoundit.cn","f39339b1-aaa6-4e86-b0c2-a6e6a21113b5",{"id":18,"name":19,"slug":20,"description":21},"6179d3b6-dc34-4483-9ded-3cd9f1b37a47","科普","abbreviation","介绍各领域新兴概念",[23,27],{"id":24,"name":25,"slug":26},"4c2bbea6-eab7-40a8-8447-1de478ff7749","分析","analyse",{"id":28,"name":29,"slug":30},"7c76bfc2-f80f-4ee0-a95d-27bd8708b434","技术","slug","资料来源",null,"published",false,66,0,"2026-08-09T00:00:00.000Z","2026-08-09T12:49:30.672Z","2026-08-09T12:13:26.523Z",[41,50,58],{"id":42,"type":6,"title":43,"slug":44,"summary":45,"coverUrl":46,"authorName":14,"sno":47,"publishedAt":48,"createdAt":49},"d6ff32b0-f499-453a-b1e3-c31a4a7a5b16","布隆过滤器：系统如何快速判断一个东西肯定不存在","bloom-filter-probabilistic-membership-explained","布隆过滤器用位数组和多个哈希函数，以极少内存快速筛掉肯定不存在的对象。本文讲清误报与漏报、误报率、删除难题，以及它在缓存、数据库和去重系统中的正确用法。","\u002Fuploads\u002F2026-08-06\u002F234915f6-2ef2-4820-8523-1a98544732f9.jpg",61,"2026-08-06T00:00:00.000Z","2026-08-06T05:38:25.381Z",{"id":51,"type":6,"title":52,"slug":53,"summary":54,"coverUrl":55,"authorName":14,"sno":56,"publishedAt":48,"createdAt":57},"23cd6a50-6716-4620-99f3-d5307511c8ee","CRDT：两个人同时改同一段文字，为什么不会互相覆盖","crdt-collaborative-editing-explained","CRDT 允许多个副本先本地修改，再通过可合并的数据结构最终收敛。本文用计数器和协同编辑讲清 CvRDT、CmRDT、并发插入、删除标记、元数据成本与业务语义冲突。","\u002Fuploads\u002F2026-08-06\u002F50641649-08d8-4cac-94f0-647eabe9fc5f.jpg",62,"2026-08-06T05:38:23.928Z",{"id":59,"type":6,"title":60,"slug":61,"summary":62,"coverUrl":63,"authorName":14,"sno":56,"publishedAt":48,"createdAt":64},"a04c10a4-3fe8-4537-81bd-354103c1078f","WebGPU：浏览器为什么能跑 3D、滤镜和部分 AI","webgpu-browser-gpu-computing-explained","WebGPU 让网页能更直接地使用 GPU 做渲染与通用计算。本文从 CPU 与 GPU 的分工讲起，拆解适配器、设备、缓冲区、WGSL 着色器和命令队列，并说明什么时候并行计算真的值得。","\u002Fuploads\u002F2026-08-06\u002F05f26f06-cda2-4ed3-b1f2-5f26c82c2a37.jpg","2026-08-06T05:38:22.834Z"]