<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="rss.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>RainLib Blog</title>
        <link>https://rainlib.vercel.app/en/blog</link>
        <description>RainLib Blog</description>
        <lastBuildDate>Fri, 26 Jun 2026 08:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[WebRTC 全景实战 (15)：Capstone — 生产级视频会议系统]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference</guid>
            <pubDate>Fri, 26 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[整合 Ch0-14 构建 Client + Signaling + SFU + TURN 完整视频会议系统，含压测与 LiveKit Agents 扩展]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"站在巨人的肩膀上。" — WebRTC 标准化团队的共识（<a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious — 历史</a>）</p>
</blockquote>
<p>本系列最后一篇，整合 Ch0–Ch14 全部知识，构建可部署的<strong>生产级视频会议系统</strong>。Serge Lachapelle 在 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 访谈</a> 中回顾：从 Marratech 的瑞典企业网实验，到 Google Meet 的全球部署，WebRTC 的终极形态不是某个协议细节，而是<strong>一套可运维、可扩展、可观测的实时通信系统</strong>。</p>
<p>配套代码：<code>examples/webrtc-lab/</code>（全栈整合）</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>推荐先读</div><div class="admonitionContent_BuS1"><p>Capstone 以 LiveKit 为 SFU 参考实现。请先阅读本站 <a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍</a>，了解 Room/Participant/Track 模型、SDK 选型与 Agents 扩展路径。</p></div></div>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Capstone</strong></td><td style="text-align:left">毕业设计</td><td style="text-align:left">本系列综合实战项目</td></tr><tr><td style="text-align:left"><strong>Control Plane</strong></td><td style="text-align:left">控制面</td><td style="text-align:left">信令、鉴权、Room 管理</td></tr><tr><td style="text-align:left"><strong>Media Plane</strong></td><td style="text-align:left">媒体面</td><td style="text-align:left">SFU 媒体转发 + TURN 中继</td></tr><tr><td style="text-align:left"><strong>Access Token</strong></td><td style="text-align:left">访问令牌</td><td style="text-align:left">JWT 编码 Room 权限和 Identity</td></tr><tr><td style="text-align:left"><strong>Egress</strong></td><td style="text-align:left">出口</td><td style="text-align:left">LiveKit 录制/直播推流服务</td></tr><tr><td style="text-align:left"><strong>Ingress</strong></td><td style="text-align:left">入口</td><td style="text-align:left">LiveKit 外部媒体源接入</td></tr><tr><td style="text-align:left"><strong>Agent</strong></td><td style="text-align:left">智能体</td><td style="text-align:left">LiveKit AI 参与者（STT/LLM/TTS）</td></tr><tr><td style="text-align:left"><strong>Perfect Negotiation</strong></td><td style="text-align:left">完美协商</td><td style="text-align:left">避免 Offer/Answer glare 的协商模式</td></tr><tr><td style="text-align:left"><strong>Graceful Degradation</strong></td><td style="text-align:left">优雅降级</td><td style="text-align:left">网络差时自动降质而非断连</td></tr><tr><td style="text-align:left"><strong>Chaos Engineering</strong></td><td style="text-align:left">混沌工程</td><td style="text-align:left">故意注入故障验证系统韧性</td></tr><tr><td style="text-align:left"><strong>Waiting Room</strong></td><td style="text-align:left">等候室</td><td style="text-align:left">主持人批准后才允许加入</td></tr><tr><td style="text-align:left"><strong>Active Speaker</strong></td><td style="text-align:left">活跃说话者</td><td style="text-align:left">大会议 UI 聚焦当前发言人</td></tr><tr><td style="text-align:left"><strong>Webhook</strong></td><td style="text-align:left">回调</td><td style="text-align:left">Room 事件推送后端</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一系统架构">一、系统架构<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E4%B8%80%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84" class="hash-link" aria-label="Direct link to 一、系统架构" title="Direct link to 一、系统架构" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">组件</th><th style="text-align:left">技术选型</th><th style="text-align:left">对应章节</th></tr></thead><tbody><tr><td style="text-align:left">客户端</td><td style="text-align:left">React + <code>@livekit/components-react</code></td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">Ch1</a>, <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">Ch2</a></td></tr><tr><td style="text-align:left">信令</td><td style="text-align:left">Node.js + ws（或 LiveKit 内置）</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">Ch3</a></td></tr><tr><td style="text-align:left">SFU</td><td style="text-align:left">LiveKit Server（基于 Pion）</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">Ch12</a></td></tr><tr><td style="text-align:left">TURN</td><td style="text-align:left">coturn 集群</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">Ch5</a>, <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">Ch14</a></td></tr><tr><td style="text-align:left">认证</td><td style="text-align:left">JWT Access Token</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">Ch3</a>, <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">Ch7</a></td></tr><tr><td style="text-align:left">监控</td><td style="text-align:left">getStats → Prometheus</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">Ch8</a>, <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">Ch13</a></td></tr><tr><td style="text-align:left">拥塞控制</td><td style="text-align:left">GCC + Simulcast</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">Ch10</a>, <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Ch11</a></td></tr></tbody></table>
<p><a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍</a> 详细说明了 LiveKit 如何整合上述组件——从 Pion 底层到 Room SDK、Agents、Egress 的完整产品栈。Capstone 在此基础上给出可部署的端到端方案。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二功能清单">二、功能清单<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E4%BA%8C%E5%8A%9F%E8%83%BD%E6%B8%85%E5%8D%95" class="hash-link" aria-label="Direct link to 二、功能清单" title="Direct link to 二、功能清单" translate="no">​</a></h2>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三关键流程用户加入-room">三、关键流程：用户加入 Room<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E4%B8%89%E5%85%B3%E9%94%AE%E6%B5%81%E7%A8%8B%E7%94%A8%E6%88%B7%E5%8A%A0%E5%85%A5-room" class="hash-link" aria-label="Direct link to 三、关键流程：用户加入 Room" title="Direct link to 三、关键流程：用户加入 Room" translate="no">​</a></h2>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-后端-join-api-完整实现">3.1 后端 Join API 完整实现<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#31-%E5%90%8E%E7%AB%AF-join-api-%E5%AE%8C%E6%95%B4%E5%AE%9E%E7%8E%B0" class="hash-link" aria-label="Direct link to 3.1 后端 Join API 完整实现" title="Direct link to 3.1 后端 Join API 完整实现" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// examples/webrtc-lab/signaling/ 扩展为完整 API</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports">express</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"express"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports">crypto</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"crypto"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">AccessToken</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"livekit-server-sdk"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> app </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">express</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">use</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">express</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateTurnCredentials</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">secret</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> identity</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> ttl </span><span class="token parameter operator" style="color:#393A34">=</span><span class="token parameter"> </span><span class="token parameter number" style="color:#36acaa">86400</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> timestamp </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">Math</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">floor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> ttl</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> username </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">timestamp</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">:</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">identity</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> hmac </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> crypto</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createHmac</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"sha1"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> secret</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  hmac</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">update</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">username</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> credential </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> hmac</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">digest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"base64"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token string" style="color:#e3116c">"turn:turn.example.com:443?transport=tcp"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token string" style="color:#e3116c">"turns:turn.example.com:443?transport=tcp"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        username</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        credential</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"stun:stun.example.com:3478"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateLiveKitToken</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">roomId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> identity</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> grants </span><span class="token parameter operator" style="color:#393A34">=</span><span class="token parameter"> </span><span class="token parameter punctuation" style="color:#393A34">{</span><span class="token parameter punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> token </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">AccessToken</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">LIVEKIT_API_KEY</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">LIVEKIT_API_SECRET</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ttl</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"24h"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  token</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addGrant</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">roomJoin</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">room</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">canPublish</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">canSubscribe</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">canPublishData</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token spread operator" style="color:#393A34">...</span><span class="token plain">grants</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> token</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">toJwt</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">post</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/rooms/join"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">req</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> res</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">body</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">roomId </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">identity</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">status</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">400</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">error</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"roomId and identity required"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> token </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateLiveKitToken</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> turn </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateTurnCredentials</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">TURN_SECRET</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">layer</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"signaling"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">event</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"room_join"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">timestamp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    token</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">livekitUrl</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">LIVEKIT_URL</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ws://localhost:7880"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> turn</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceServers</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">listen</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">3000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"API server :3000"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四客户端完整实现">四、客户端完整实现<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E5%9B%9B%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%AE%8C%E6%95%B4%E5%AE%9E%E7%8E%B0" class="hash-link" aria-label="Direct link to 四、客户端完整实现" title="Direct link to 四、客户端完整实现" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-react-视频会议组件">4.1 React 视频会议组件<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#41-react-%E8%A7%86%E9%A2%91%E4%BC%9A%E8%AE%AE%E7%BB%84%E4%BB%B6" class="hash-link" aria-label="Direct link to 4.1 React 视频会议组件" title="Direct link to 4.1 React 视频会议组件" translate="no">​</a></h3>
<div class="custom-code-block" data-language="jsx"><div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// examples/webrtc-lab/client/ch15-capstone/App.jsx</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"></span><br></div><div class="token-line" style="color:#393A34"><span class="token imports">  </span><span class="token imports maybe-class-name">LiveKitRoom</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"></span><br></div><div class="token-line" style="color:#393A34"><span class="token imports">  </span><span class="token imports maybe-class-name">VideoConference</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"></span><br></div><div class="token-line" style="color:#393A34"><span class="token imports">  </span><span class="token imports maybe-class-name">RoomAudioRenderer</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"></span><br></div><div class="token-line" style="color:#393A34"><span class="token imports">  useRoomContext</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"></span><br></div><div class="token-line" style="color:#393A34"><span class="token imports"></span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@livekit/components-react"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@livekit/components-styles"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> useState</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> useEffect </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"react"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">RoomEvent</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">DisconnectReason</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"livekit-client"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:#d73a49">CapstoneMeeting</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">token</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> setToken</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">useState</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">livekitUrl</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> setLivekitUrl</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">useState</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> setRoomId</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">useState</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">""</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> setIdentity</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">useState</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">""</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">joinRoom</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">rId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> res </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fetch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/api/rooms/join"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">method</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"POST"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">headers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"Content-Type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"application/json"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">body</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">roomId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> rId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">identity</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> id </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> data </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">setToken</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">data</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">token</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">setLivekitUrl</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">data</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">livekitUrl</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">setRoomId</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">rId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">setIdentity</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">token</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#00009f">JoinForm</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">onJoin</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f">joinRoom</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#00009f"> </span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#00009f">LiveKitRoom</span><span class="token tag" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">      </span><span class="token tag attr-name" style="color:#00a4db">token</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f">token</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">      </span><span class="token tag attr-name" style="color:#00a4db">serverUrl</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f">livekitUrl</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">      </span><span class="token tag attr-name" style="color:#00a4db">connectOptions</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f"> </span><span class="token tag script language-javascript literal-property property" style="color:#36acaa">autoSubscribe</span><span class="token tag script language-javascript operator" style="color:#393A34">:</span><span class="token tag script language-javascript" style="color:#00009f"> </span><span class="token tag script language-javascript boolean" style="color:#36acaa">true</span><span class="token tag script language-javascript" style="color:#00009f"> </span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">      </span><span class="token tag attr-name" style="color:#00a4db">video</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript boolean" style="color:#36acaa">true</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">      </span><span class="token tag attr-name" style="color:#00a4db">audio</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript boolean" style="color:#36acaa">true</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">      </span><span class="token tag attr-name" style="color:#00a4db">onDisconnected</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript punctuation" style="color:#393A34">(</span><span class="token tag script language-javascript parameter" style="color:#00009f">reason</span><span class="token tag script language-javascript punctuation" style="color:#393A34">)</span><span class="token tag script language-javascript" style="color:#00009f"> </span><span class="token tag script language-javascript arrow operator" style="color:#393A34">=&gt;</span><span class="token tag script language-javascript" style="color:#00009f"> </span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag script language-javascript" style="color:#00009f">        </span><span class="token tag script language-javascript keyword control-flow" style="color:#00009f">if</span><span class="token tag script language-javascript" style="color:#00009f"> </span><span class="token tag script language-javascript punctuation" style="color:#393A34">(</span><span class="token tag script language-javascript" style="color:#00009f">reason </span><span class="token tag script language-javascript operator" style="color:#393A34">!==</span><span class="token tag script language-javascript" style="color:#00009f"> </span><span class="token tag script language-javascript maybe-class-name" style="color:#00009f">DisconnectReason</span><span class="token tag script language-javascript punctuation" style="color:#393A34">.</span><span class="token tag script language-javascript constant" style="color:#36acaa">CLIENT_INITIATED</span><span class="token tag script language-javascript punctuation" style="color:#393A34">)</span><span class="token tag script language-javascript" style="color:#00009f"> </span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag script language-javascript" style="color:#00009f">          </span><span class="token tag script language-javascript console class-name" style="color:#00009f">console</span><span class="token tag script language-javascript punctuation" style="color:#393A34">.</span><span class="token tag script language-javascript method function property-access" style="color:#d73a49">log</span><span class="token tag script language-javascript punctuation" style="color:#393A34">(</span><span class="token tag script language-javascript string" style="color:#e3116c">"[Layer2] disconnected:"</span><span class="token tag script language-javascript punctuation" style="color:#393A34">,</span><span class="token tag script language-javascript" style="color:#00009f"> reason</span><span class="token tag script language-javascript punctuation" style="color:#393A34">)</span><span class="token tag script language-javascript punctuation" style="color:#393A34">;</span><span class="token tag script language-javascript" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag script language-javascript" style="color:#00009f">        </span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag script language-javascript" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag script language-javascript" style="color:#00009f">        </span><span class="token tag script language-javascript function" style="color:#d73a49">setToken</span><span class="token tag script language-javascript punctuation" style="color:#393A34">(</span><span class="token tag script language-javascript keyword null nil" style="color:#00009f">null</span><span class="token tag script language-javascript punctuation" style="color:#393A34">)</span><span class="token tag script language-javascript punctuation" style="color:#393A34">;</span><span class="token tag script language-javascript" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag script language-javascript" style="color:#00009f">      </span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">    </span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#00009f">VideoConference</span><span class="token tag" style="color:#00009f"> </span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#00009f">RoomAudioRenderer</span><span class="token tag" style="color:#00009f"> </span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#00009f">MetricsReporter</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">roomId</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f">roomId</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">identity</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f">identity</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#00009f"> </span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#00009f">ReconnectHandler</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">roomId</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f">roomId</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">identity</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f">identity</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">livekitUrl</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f">livekitUrl</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#00009f"> </span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag class-name" style="color:#00009f">LiveKitRoom</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-指标上报组件">4.2 指标上报组件<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#42-%E6%8C%87%E6%A0%87%E4%B8%8A%E6%8A%A5%E7%BB%84%E4%BB%B6" class="hash-link" aria-label="Direct link to 4.2 指标上报组件" title="Direct link to 4.2 指标上报组件" translate="no">​</a></h3>
<div class="custom-code-block" data-language="jsx"><div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:#d73a49">MetricsReporter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter punctuation" style="color:#393A34">{</span><span class="token parameter"> roomId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> identity </span><span class="token parameter punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> room </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">useRoomContext</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">useEffect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> interval </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">setInterval</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">layer</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"media"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">room</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">name</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">participants</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">numParticipants</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">timestamp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">clearInterval</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">interval</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="43-屏幕共享">4.3 屏幕共享<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#43-%E5%B1%8F%E5%B9%95%E5%85%B1%E4%BA%AB" class="hash-link" aria-label="Direct link to 4.3 屏幕共享" title="Direct link to 4.3 屏幕共享" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">Track</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"livekit-client"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">shareScreen</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">room</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localParticipant</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setScreenShareEnabled</span><span class="token punctuation" style="color:#393A34">(</span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">simulcast</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">videoEncoding</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3_000_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxFramerate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">15</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">RoomEvent</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">TrackSubscribed</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">track</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> publication</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> participant</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">source</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token maybe-class-name">Track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Source</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">ScreenShare</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> el </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">attach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token dom variable" style="color:#36acaa">document</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getElementById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"screen-share"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">appendChild</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">el</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五快速部署">五、快速部署<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E4%BA%94%E5%BF%AB%E9%80%9F%E9%83%A8%E7%BD%B2" class="hash-link" aria-label="Direct link to 五、快速部署" title="Direct link to 五、快速部署" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-本地开发环境">5.1 本地开发环境<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#51-%E6%9C%AC%E5%9C%B0%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83" class="hash-link" aria-label="Direct link to 5.1 本地开发环境" title="Direct link to 5.1 本地开发环境" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># 1. LiveKit SFU</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">brew </span><span class="token function" style="color:#d73a49">install</span><span class="token plain"> livekit</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">livekit-server </span><span class="token parameter variable" style="color:#36acaa">--dev</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># → ws://localhost:7880</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 2. TURN（可选，本地开发通常不需要）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">cd</span><span class="token plain"> examples/webrtc-lab/docker/coturn</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">docker</span><span class="token plain"> compose up </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 3. 信令 + API</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">cd</span><span class="token plain"> examples/webrtc-lab/signaling</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">npm</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">install</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">npm</span><span class="token plain"> start</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># → ws://localhost:8080 + http://localhost:3000</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 4. 生成 Token（CLI 方式）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">brew </span><span class="token function" style="color:#d73a49">install</span><span class="token plain"> livekit-cli</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">lk token create </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --api-key devkey </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --api-secret secret </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">--join</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">--room</span><span class="token plain"> capstone </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">--identity</span><span class="token plain"> user1 </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --valid-for 24h</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 5. 客户端</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">npx serve examples/webrtc-lab/client/ch12-sfu-client</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-生产部署拓扑">5.2 生产部署拓扑<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#52-%E7%94%9F%E4%BA%A7%E9%83%A8%E7%BD%B2%E6%8B%93%E6%89%91" class="hash-link" aria-label="Direct link to 5.2 生产部署拓扑" title="Direct link to 5.2 生产部署拓扑" translate="no">​</a></h3>
<!-- -->
<table><thead><tr><th style="text-align:left">步骤</th><th style="text-align:left">命令/配置</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">LiveKit 配置</td><td style="text-align:left"><code>livekit.yaml</code> + Redis 地址</td><td style="text-align:left">分布式 Mesh</td></tr><tr><td style="text-align:left">TURN 部署</td><td style="text-align:left">coturn × N 区域</td><td style="text-align:left">GeoDNS 解析</td></tr><tr><td style="text-align:left">API 部署</td><td style="text-align:left">Docker/K8s</td><td style="text-align:left">Token + TURN 凭证</td></tr><tr><td style="text-align:left">监控</td><td style="text-align:left">Prometheus scrape</td><td style="text-align:left">LiveKit :6789 + coturn :9641</td></tr><tr><td style="text-align:left">证书</td><td style="text-align:left">Let's Encrypt</td><td style="text-align:left">WSS + TURNS 443</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="53-livekityaml-生产示例">5.3 livekit.yaml 生产示例<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#53-livekityaml-%E7%94%9F%E4%BA%A7%E7%A4%BA%E4%BE%8B" class="hash-link" aria-label="Direct link to 5.3 livekit.yaml 生产示例" title="Direct link to 5.3 livekit.yaml 生产示例" translate="no">​</a></h3>
<div class="custom-code-block" data-language="yaml"><div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">port</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">7880</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">rtc</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">tcp_port</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">7881</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">port_range_start</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">50000</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">port_range_end</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">60000</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">use_external_ip</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">redis</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">address</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> redis.example.com</span><span class="token punctuation" style="color:#393A34">:</span><span class="token number" style="color:#36acaa">6379</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">turn</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">enabled</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">domain</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> turn.example.com</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">tls_port</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">443</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">keys</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">APIKey</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> &lt;your</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">api</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">APISecret</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> &lt;your</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">api</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">secret</span><span class="token punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">webhook</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">urls</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> https</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">//api.example.com/webhook/livekit</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">logging</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">level</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> info</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">room</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">empty_timeout</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">300</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">max_participants</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六性能基线与-slo">六、性能基线与 SLO<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E5%85%AD%E6%80%A7%E8%83%BD%E5%9F%BA%E7%BA%BF%E4%B8%8E-slo" class="hash-link" aria-label="Direct link to 六、性能基线与 SLO" title="Direct link to 六、性能基线与 SLO" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">指标</th><th style="text-align:left">目标</th><th style="text-align:left">测量方法</th><th style="text-align:left">对应章节</th></tr></thead><tbody><tr><td style="text-align:left">首帧时间</td><td style="text-align:left">&lt; 2s</td><td style="text-align:left"><code>framesDecoded</code> 首次 &gt; 0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">Ch13</a></td></tr><tr><td style="text-align:left">端到端延迟</td><td style="text-align:left">&lt; 300ms</td><td style="text-align:left">RTT/2 + jitterBufferDelay</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">Ch8</a></td></tr><tr><td style="text-align:left">100 Room 并发</td><td style="text-align:left">无 ICE failed</td><td style="text-align:left">压测 + Prometheus</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">Ch5</a></td></tr><tr><td style="text-align:left">TURN 占比</td><td style="text-align:left">&lt; 25%</td><td style="text-align:left">candidateType=relay 统计</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">Ch14</a></td></tr><tr><td style="text-align:left">通话成功率</td><td style="text-align:left">&gt; 99%</td><td style="text-align:left">connectionState=connected 比例</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">Ch13</a></td></tr><tr><td style="text-align:left">Simulcast 切换</td><td style="text-align:left">&lt; 1s</td><td style="text-align:left">限速后 rid 变化时间</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Ch11</a></td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="61-压测脚本">6.1 压测脚本<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#61-%E5%8E%8B%E6%B5%8B%E8%84%9A%E6%9C%AC" class="hash-link" aria-label="Direct link to 6.1 压测脚本" title="Direct link to 6.1 压测脚本" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">Room</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"livekit-client"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">loadTest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">roomId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> count</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> results </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> i </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> count</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> i</span><span class="token operator" style="color:#393A34">++</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> identity </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">bot-</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">i</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> res </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fetch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/api/rooms/join"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">method</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"POST"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">headers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"Content-Type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"application/json"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">body</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> token</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> livekitUrl </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> room </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Room</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> start </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">try</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">connect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">livekitUrl</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> token</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      results</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">connectTime</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> start</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">state</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">err</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      results</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">connectTime</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">state</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"failed"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">error</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> err</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">message</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">table</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">results</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> success </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> results</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">state</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"connected"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">length</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> avgConnect </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> results</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">connectTime</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">reduce</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">s</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> s </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">connectTime</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> success</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">Success: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">success</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">count</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">, Avg connect: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">avgConnect</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">ms</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七断线重连与容错">七、断线重连与容错<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E4%B8%83%E6%96%AD%E7%BA%BF%E9%87%8D%E8%BF%9E%E4%B8%8E%E5%AE%B9%E9%94%99" class="hash-link" aria-label="Direct link to 七、断线重连与容错" title="Direct link to 七、断线重连与容错" translate="no">​</a></h2>
<!-- -->
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:#d73a49">ReconnectHandler</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter punctuation" style="color:#393A34">{</span><span class="token parameter"> roomId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> identity</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> livekitUrl </span><span class="token parameter punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> room </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">useRoomContext</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">useEffect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">RoomEvent</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Reconnecting</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">layer</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ice"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">event</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"reconnecting"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> roomId </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">RoomEvent</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Reconnected</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">layer</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ice"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">event</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"reconnected"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> roomId </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">RoomEvent</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Disconnected</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">reason</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">reason </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token maybe-class-name">DisconnectReason</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">CLIENT_INITIATED</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">try</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> res </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fetch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/api/rooms/join"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token literal-property property" style="color:#36acaa">method</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"POST"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token literal-property property" style="color:#36acaa">headers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"Content-Type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"application/json"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token literal-property property" style="color:#36acaa">body</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> token </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">connect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">livekitUrl</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> token</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">err</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Reconnect failed:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> err</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> livekitUrl</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八livekit-agents-扩展">八、LiveKit Agents 扩展<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E5%85%ABlivekit-agents-%E6%89%A9%E5%B1%95" class="hash-link" aria-label="Direct link to 八、LiveKit Agents 扩展" title="Direct link to 八、LiveKit Agents 扩展" translate="no">​</a></h2>
<p>Serge Lachapelle 在 Curious 访谈中提到对未来最兴奋的方向：<strong>云计算 + AI 算法</strong>（降噪、背景分离、实时翻译）。LiveKit Agents 让 AI 作为 Room 中的普通 Participant：</p>
<!-- -->
<table><thead><tr><th style="text-align:left">Agent 类型</th><th style="text-align:left">用途</th><th style="text-align:left">示例</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Voice Assistant</strong></td><td style="text-align:left">语音问答</td><td style="text-align:left">会议 AI 助手</td></tr><tr><td style="text-align:left"><strong>Translator</strong></td><td style="text-align:left">实时翻译</td><td style="text-align:left">多语言会议</td></tr><tr><td style="text-align:left"><strong>Transcriber</strong></td><td style="text-align:left">实时字幕</td><td style="text-align:left">无障碍/accessibility</td></tr><tr><td style="text-align:left"><strong>Moderator</strong></td><td style="text-align:left">内容审核</td><td style="text-align:left">违规检测</td></tr></tbody></table>
<div class="custom-code-block" data-language="python"><div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># LiveKit Agent 最小示例</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> livekit</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">agents </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> AutoSubscribe</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> JobContext</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> WorkerOptions</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> cli</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> livekit</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">agents</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">voice </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> Agent</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">entrypoint</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> JobContext</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">await</span><span class="token plain"> ctx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">connect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">auto_subscribe</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">AutoSubscribe</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">AUDIO_ONLY</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    agent </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> Agent</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">instructions</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"You are a helpful meeting assistant."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    agent</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">start</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ctx</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> __name__ </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"__main__"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    cli</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">run_app</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">WorkerOptions</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">entrypoint_fnc</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">entrypoint</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div></div>
<p>详见 <a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍</a> 与 <a href="https://docs.livekit.io/agents/" target="_blank" rel="noopener noreferrer" class="">Agents 文档</a>。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九egress-录制与-ingress-接入">九、Egress 录制与 Ingress 接入<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E4%B9%9Degress-%E5%BD%95%E5%88%B6%E4%B8%8E-ingress-%E6%8E%A5%E5%85%A5" class="hash-link" aria-label="Direct link to 九、Egress 录制与 Ingress 接入" title="Direct link to 九、Egress 录制与 Ingress 接入" translate="no">​</a></h2>
<!-- -->
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">lk egress start </span><span class="token parameter variable" style="color:#36acaa">--room</span><span class="token plain"> capstone </span><span class="token parameter variable" style="color:#36acaa">--layout</span><span class="token plain"> grid </span><span class="token parameter variable" style="color:#36acaa">--output</span><span class="token plain"> s3://bucket/recording.mp4</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">lk egress start </span><span class="token parameter variable" style="color:#36acaa">--room</span><span class="token plain"> capstone </span><span class="token parameter variable" style="color:#36acaa">--stream</span><span class="token plain"> rtmp://a.rtmp.youtube.com/live2/KEY</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十exampleswebrtc-lab-全栈整合">十、examples/webrtc-lab 全栈整合<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E5%8D%81exampleswebrtc-lab-%E5%85%A8%E6%A0%88%E6%95%B4%E5%90%88" class="hash-link" aria-label="Direct link to 十、examples/webrtc-lab 全栈整合" title="Direct link to 十、examples/webrtc-lab 全栈整合" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">模块</th><th style="text-align:left">路径</th><th style="text-align:left">章节</th><th style="text-align:left">状态</th></tr></thead><tbody><tr><td style="text-align:left">媒体设备</td><td style="text-align:left"><code>client/ch01-media-devices</code></td><td style="text-align:left">Ch1</td><td style="text-align:left">✅</td></tr><tr><td style="text-align:left">P2P 通话</td><td style="text-align:left"><code>client/ch02-p2p-basic</code></td><td style="text-align:left">Ch2</td><td style="text-align:left">✅</td></tr><tr><td style="text-align:left">信令服务器</td><td style="text-align:left"><code>signaling/server.js</code></td><td style="text-align:left">Ch3</td><td style="text-align:left">✅</td></tr><tr><td style="text-align:left">Data Channel</td><td style="text-align:left"><code>client/ch06-data-channel</code></td><td style="text-align:left">Ch6</td><td style="text-align:left">📋 规划中</td></tr><tr><td style="text-align:left">SFU 客户端</td><td style="text-align:left"><code>client/ch12-sfu-client</code></td><td style="text-align:left">Ch12</td><td style="text-align:left">📋 规划中</td></tr><tr><td style="text-align:left">TURN 部署</td><td style="text-align:left"><code>docker/coturn/</code></td><td style="text-align:left">Ch5, Ch14</td><td style="text-align:left">✅</td></tr><tr><td style="text-align:left">Capstone</td><td style="text-align:left">全栈整合</td><td style="text-align:left">Ch15</td><td style="text-align:left">📋 规划中</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十一安全清单">十一、安全清单<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E5%8D%81%E4%B8%80%E5%AE%89%E5%85%A8%E6%B8%85%E5%8D%95" class="hash-link" aria-label="Direct link to 十一、安全清单" title="Direct link to 十一、安全清单" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">检查项</th><th style="text-align:left">实现</th><th style="text-align:left">章节</th></tr></thead><tbody><tr><td style="text-align:left">Token 权限最小化</td><td style="text-align:left"><code>canPublish</code> / <code>canSubscribe</code> 按需</td><td style="text-align:left">Ch12</td></tr><tr><td style="text-align:left">TURN 短期凭证</td><td style="text-align:left"><code>use-auth-secret</code> + TTL</td><td style="text-align:left">Ch14</td></tr><tr><td style="text-align:left">传输加密</td><td style="text-align:left">WSS + DTLS/SRTP</td><td style="text-align:left">Ch7</td></tr><tr><td style="text-align:left">Secret 管理</td><td style="text-align:left">环境变量 / K8s Secret</td><td style="text-align:left">—</td></tr><tr><td style="text-align:left">监控告警</td><td style="text-align:left">ICE failed / relay 占比</td><td style="text-align:left">Ch13</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十二常见陷阱">十二、常见陷阱<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E5%8D%81%E4%BA%8C%E5%B8%B8%E8%A7%81%E9%99%B7%E9%98%B1" class="hash-link" aria-label="Direct link to 十二、常见陷阱" title="Direct link to 十二、常见陷阱" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:center">#</th><th style="text-align:left">陷阱</th><th style="text-align:left">现象</th><th style="text-align:left">修复</th></tr></thead><tbody><tr><td style="text-align:center">1</td><td style="text-align:left">Token 权限过大</td><td style="text-align:left">任意用户可 publish</td><td style="text-align:left">JWT grant 最小权限</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left">未配 TURN</td><td style="text-align:left">部分用户 ICE failed</td><td style="text-align:left">部署 coturn + REST 凭证</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left">单 SFU 节点</td><td style="text-align:left">跨区延迟高</td><td style="text-align:left">Redis Mesh 分布式</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left">无监控</td><td style="text-align:left">故障不可见</td><td style="text-align:left">Prometheus + 三层日志</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left">Simulcast 未开</td><td style="text-align:left">大会议带宽爆炸</td><td style="text-align:left">publishTrack simulcast<!-- -->:true</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left">硬编码 API Key</td><td style="text-align:left">安全风险</td><td style="text-align:left">环境变量 + Secret 管理</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left">忽略 Token 刷新</td><td style="text-align:left">长会议中断</td><td style="text-align:left">过期前自动 refresh</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left">信令与 API 混端口</td><td style="text-align:left">CORS/路由混乱</td><td style="text-align:left">API :3000 + 信令 :8080 分离</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left">无 emptyTimeout</td><td style="text-align:left">空 Room 占资源</td><td style="text-align:left">livekit.yaml 配置</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left">压测未模拟 TURN</td><td style="text-align:left">生产 relay 爆满</td><td style="text-align:left">压测含 relay 场景</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十三系列回顾">十三、系列回顾<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E5%8D%81%E4%B8%89%E7%B3%BB%E5%88%97%E5%9B%9E%E9%A1%BE" class="hash-link" aria-label="Direct link to 十三、系列回顾" title="Direct link to 十三、系列回顾" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">阶段</th><th style="text-align:left">章节</th><th style="text-align:left">核心能力</th></tr></thead><tbody><tr><td style="text-align:left"><strong>认知</strong></td><td style="text-align:left">Ch0–Ch2</td><td style="text-align:left">WebRTC 架构、浏览器 API、首个 P2P 通话</td></tr><tr><td style="text-align:left"><strong>连接</strong></td><td style="text-align:left">Ch3–Ch6</td><td style="text-align:left">信令、SDP、ICE/TURN、Data Channel</td></tr><tr><td style="text-align:left"><strong>媒体</strong></td><td style="text-align:left">Ch7–Ch9</td><td style="text-align:left">DTLS/SRTP、RTP/RTCP、Codec/Simulcast</td></tr><tr><td style="text-align:left"><strong>SFU</strong></td><td style="text-align:left">Ch10–Ch12</td><td style="text-align:left">GCC 拥塞控制、Simulcast/SVC、SFU 架构</td></tr><tr><td style="text-align:left"><strong>生产</strong></td><td style="text-align:left">Ch13–Ch15</td><td style="text-align:left">调试可观测、TURN 部署、Capstone 系统</td></tr></tbody></table>
<table><thead><tr><th style="text-align:left">能力域</th><th style="text-align:left">你现在应该能</th></tr></thead><tbody><tr><td style="text-align:left">浏览器 API</td><td style="text-align:left">独立实现 P2P + DataChannel + Simulcast</td></tr><tr><td style="text-align:left">协议栈</td><td style="text-align:left">读懂 SDP、抓包分析 DTLS/SRTP、理解 GCC</td></tr><tr><td style="text-align:left">信令与 SFU</td><td style="text-align:left">设计 Room 模型、部署 LiveKit、Token 鉴权</td></tr><tr><td style="text-align:left">生产运维</td><td style="text-align:left">部署 TURN 集群、监控 stats、SLO 告警</td></tr><tr><td style="text-align:left">权威资料</td><td style="text-align:left">查阅 RFC / Curious / webrtcH4cKS / LiveKit Docs</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十四marratech-到-meet-的完整弧线">十四、Marratech 到 Meet 的完整弧线<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E5%8D%81%E5%9B%9Bmarratech-%E5%88%B0-meet-%E7%9A%84%E5%AE%8C%E6%95%B4%E5%BC%A7%E7%BA%BF" class="hash-link" aria-label="Direct link to 十四、Marratech 到 Meet 的完整弧线" title="Direct link to 十四、Marratech 到 Meet 的完整弧线" translate="no">​</a></h2>
<!-- -->
<p>Serge Lachapelle 的 Curious 访谈是整个系列的历史锚点——从 Marratech 的多播幻想，到 packet shufflers 的现实，再到今天 AI Agent 作为 Room 参与者，WebRTC 的故事仍在续写。本 Capstone 站在 Meet/LiveKit 的肩膀上，把 Ch0–Ch14 的知识落成一套可部署系统。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十五实战-labcapstone-验收清单">十五、实战 Lab：Capstone 验收清单<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E5%8D%81%E4%BA%94%E5%AE%9E%E6%88%98-labcapstone-%E9%AA%8C%E6%94%B6%E6%B8%85%E5%8D%95" class="hash-link" aria-label="Direct link to 十五、实战 Lab：Capstone 验收清单" title="Direct link to 十五、实战 Lab：Capstone 验收清单" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:center">#</th><th style="text-align:left">验收项</th><th style="text-align:left">操作</th><th style="text-align:left">通过标准</th></tr></thead><tbody><tr><td style="text-align:center">1</td><td style="text-align:left">本地启动</td><td style="text-align:left"><code>livekit-server --dev</code> + 客户端</td><td style="text-align:left">3 人互见视频</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left">Token 鉴权</td><td style="text-align:left">API 生成 JWT</td><td style="text-align:left">无 Token 无法加入</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left">TURN fallback</td><td style="text-align:left">关 STUN 仅留 TURN</td><td style="text-align:left">ICE relay 成功</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left">Simulcast</td><td style="text-align:left">限速 300kbps</td><td style="text-align:left">自动降层</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left">断线重连</td><td style="text-align:left">断网 5s 恢复</td><td style="text-align:left">自动 reconnect</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left">指标上报</td><td style="text-align:left">MetricsReporter 运行</td><td style="text-align:left">5s 间隔日志</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left">屏幕共享</td><td style="text-align:left">publishScreenShare</td><td style="text-align:left">其他参与者可见</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left">50 人压测</td><td style="text-align:left">loadTest(50)</td><td style="text-align:left">成功率 &gt; 99%</td></tr></tbody></table>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token shebang important">#!/bin/bash</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">set</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-e</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">echo</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"=== Capstone WebRTC 视频会议系统 ==="</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">livekit-server </span><span class="token parameter variable" style="color:#36acaa">--dev</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">sleep</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">cd</span><span class="token plain"> examples/webrtc-lab/docker/coturn </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">docker</span><span class="token plain"> compose up </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">cd</span><span class="token plain"> -</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">cd</span><span class="token plain"> examples/webrtc-lab/signaling </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">npm</span><span class="token plain"> start </span><span class="token operator" style="color:#393A34">&amp;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">sleep</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token assign-left variable" style="color:#36acaa">TOKEN</span><span class="token operator" style="color:#393A34">=</span><span class="token variable" style="color:#36acaa">$(</span><span class="token variable" style="color:#36acaa">lk token create --api-key devkey --api-secret secret </span><span class="token variable punctuation" style="color:#393A34">\</span><span class="token variable" style="color:#36acaa"></span><br></div><div class="token-line" style="color:#393A34"><span class="token variable" style="color:#36acaa">  </span><span class="token variable parameter variable" style="color:#36acaa">--join</span><span class="token variable" style="color:#36acaa"> </span><span class="token variable parameter variable" style="color:#36acaa">--room</span><span class="token variable" style="color:#36acaa"> capstone </span><span class="token variable parameter variable" style="color:#36acaa">--identity</span><span class="token variable" style="color:#36acaa"> tester --valid-for 1h </span><span class="token variable operator file-descriptor important" style="color:#393A34">2</span><span class="token variable operator" style="color:#393A34">&gt;</span><span class="token variable" style="color:#36acaa">/dev/null</span><span class="token variable" style="color:#36acaa">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">echo</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"LiveKit: ws://localhost:7880"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">echo</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Token: </span><span class="token string variable" style="color:#36acaa">$TOKEN</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">echo</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"打开 examples/webrtc-lab/client/ch12-sfu-client 加入 room=capstone"</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十六推荐阅读">十六、推荐阅读<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#%E5%8D%81%E5%85%AD%E6%8E%A8%E8%8D%90%E9%98%85%E8%AF%BB" class="hash-link" aria-label="Direct link to 十六、推荐阅读" title="Direct link to 十六、推荐阅读" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://webrtcforthecurious.com/zh/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious 全书</a>（CC0，vendor-neutral 圣经）</li>
<li class=""><a href="https://webrtchacks.com/" target="_blank" rel="noopener noreferrer" class="">webrtcH4cKS</a>（工业界深度实验）</li>
<li class=""><a href="https://blogwebrtc.org/" target="_blank" rel="noopener noreferrer" class="">Advancing WebRTC (Fippo)</a>（API 设计决策）</li>
<li class=""><a href="https://github.com/livekit-examples/meet" target="_blank" rel="noopener noreferrer" class="">LiveKit Meet 开源参考</a></li>
<li class=""><a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍 — 本站推荐</a></li>
<li class=""><a href="https://github.com/pion/example-webrtc-applications" target="_blank" rel="noopener noreferrer" class="">Pion WebRTC Examples</a></li>
</ul>
<hr>
<blockquote>
<p><strong>系列导航 — WebRTC 全景实战（全 16 章）</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<p>🎉 <strong>恭喜完成 WebRTC 全景实战全系列！</strong></p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://webrtcforthecurious.com/zh/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 全书</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史（Serge Lachapelle / Marratech）</a></li>
<li class=""><a href="https://meet.livekit.io/" target="_blank" rel="noopener noreferrer" class="">LiveKit Meet</a></li>
<li class=""><a href="https://docs.livekit.io/" target="_blank" rel="noopener noreferrer" class="">LiveKit Docs</a></li>
<li class=""><a href="https://docs.livekit.io/agents/" target="_blank" rel="noopener noreferrer" class="">LiveKit Agents</a></li>
<li class=""><a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍 — 本站推荐</a></li>
<li class=""><a href="https://github.com/livekit-examples/meet" target="_blank" rel="noopener noreferrer" class="">LiveKit Meet 开源仓库</a></li>
<li class=""><a href="https://github.com/pion/webrtc" target="_blank" rel="noopener noreferrer" class="">Pion WebRTC</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>SFU</category>
            <category>实时通信</category>
            <category>Production</category>
            <category>教程</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (14)：TURN 集群部署与多区域扩展]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production</guid>
            <pubDate>Thu, 25 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[coturn 生产配置、TURN REST API 短期凭证、带宽成本模型与多区域拓扑]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"TURN 是 WebRTC 基础设施中带宽成本最高的组件——relay 流量约占 10–30%。" — 生产运维共识</p>
</blockquote>
<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">Ch5 ICE/STUN/TURN</a> 讲了 TURN 原理。本章落地<strong>生产级 coturn 集群</strong>——从单机 Docker 到多区域 GeoDNS 部署，覆盖凭证安全、带宽成本建模与容量规划。</p>
<p>Serge Lachapelle 在 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 历史访谈</a> 中回忆：Marratech 时代企业网内 ICE 几乎总是成功，但扩展到公网后 <strong>TURN relay 成为必需品</strong>——Today Meet/LiveKit 全球部署中，TURN 集群的运维复杂度不亚于 SFU 本身。</p>
<p>配套 Lab：<code>examples/webrtc-lab/docker/coturn/docker-compose.yml</code></p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>TURN</strong></td><td style="text-align:left">Traversal Using Relays around NAT</td><td style="text-align:left">NAT 穿透失败时的中继服务器</td></tr><tr><td style="text-align:left"><strong>STUN</strong></td><td style="text-align:left">Session Traversal Utilities for NAT</td><td style="text-align:left">获取公网 IP 的轻量服务</td></tr><tr><td style="text-align:left"><strong>coturn</strong></td><td style="text-align:left">—</td><td style="text-align:left">最流行的开源 TURN/STUN 服务器</td></tr><tr><td style="text-align:left"><strong>Allocate</strong></td><td style="text-align:left">分配</td><td style="text-align:left">TURN 客户端请求 relay 地址的操作</td></tr><tr><td style="text-align:left"><strong>Relay Address</strong></td><td style="text-align:left">中继地址</td><td style="text-align:left">TURN 分配给客户端的流量转发地址</td></tr><tr><td style="text-align:left"><strong>TURN REST API</strong></td><td style="text-align:left">—</td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8656" target="_blank" rel="noopener noreferrer" class="">RFC 8656</a> 定义的短期凭证机制</td></tr><tr><td style="text-align:left"><strong>lt-cred-mech</strong></td><td style="text-align:left">Long-term Credential</td><td style="text-align:left">coturn 长期凭证模式（开发用）</td></tr><tr><td style="text-align:left"><strong>use-auth-secret</strong></td><td style="text-align:left">—</td><td style="text-align:left">coturn 短期 HMAC 凭证模式（生产用）</td></tr><tr><td style="text-align:left"><strong>GeoDNS</strong></td><td style="text-align:left">地理 DNS</td><td style="text-align:left">根据客户端位置解析到最近 TURN 节点</td></tr><tr><td style="text-align:left"><strong>Anycast</strong></td><td style="text-align:left">任播</td><td style="text-align:left">同一 IP 多区域广播（高级部署）</td></tr><tr><td style="text-align:left"><strong>Relay Ratio</strong></td><td style="text-align:left">中继占比</td><td style="text-align:left">走 TURN relay 的连接占总连接的比例</td></tr><tr><td style="text-align:left"><strong>ICE-lite</strong></td><td style="text-align:left">—</td><td style="text-align:left">简化 ICE 实现，服务端不主动连通性检查</td></tr><tr><td style="text-align:left"><strong>ChannelBind</strong></td><td style="text-align:left">通道绑定</td><td style="text-align:left">TURN 优化模式，减少协议开销</td></tr><tr><td style="text-align:left"><strong>Permission</strong></td><td style="text-align:left">权限</td><td style="text-align:left">TURN 允许 relay 的目标 IP 白名单</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一生产拓扑">一、生产拓扑<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#%E4%B8%80%E7%94%9F%E4%BA%A7%E6%8B%93%E6%89%91" class="hash-link" aria-label="Direct link to 一、生产拓扑" title="Direct link to 一、生产拓扑" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">组件</th><th style="text-align:left">部署策略</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left"><strong>SFU</strong></td><td style="text-align:left">区域亲和 + Redis Mesh</td><td style="text-align:left">同 Room 参与者尽量同区域</td></tr><tr><td style="text-align:left"><strong>TURN</strong></td><td style="text-align:left">每区域独立集群</td><td style="text-align:left">客户端连接最近 TURN</td></tr><tr><td style="text-align:left"><strong>STUN</strong></td><td style="text-align:left">可与 TURN 共用 coturn</td><td style="text-align:left">轻量，可全局单点</td></tr><tr><td style="text-align:left"><strong>DNS</strong></td><td style="text-align:left">GeoDNS 或 Anycast</td><td style="text-align:left">客户端无感知选最近节点</td></tr></tbody></table>
<p>Marratech 在瑞典企业网时代几乎不需要 TURN；Google 收购后在 Meet 全球部署中，TURN 成为<strong>带宽成本最高的基础设施组件</strong>之一——relay 流量是 SFU 转发的 2 倍（入站 + 出站各计一次）。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二coturn-生产配置要点">二、coturn 生产配置要点<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#%E4%BA%8Ccoturn-%E7%94%9F%E4%BA%A7%E9%85%8D%E7%BD%AE%E8%A6%81%E7%82%B9" class="hash-link" aria-label="Direct link to 二、coturn 生产配置要点" title="Direct link to 二、coturn 生产配置要点" translate="no">​</a></h2>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-开发环境lab-用">2.1 开发环境（Lab 用）<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#21-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83lab-%E7%94%A8" class="hash-link" aria-label="Direct link to 2.1 开发环境（Lab 用）" title="Direct link to 2.1 开发环境（Lab 用）" translate="no">​</a></h3>
<p><code>examples/webrtc-lab/docker/coturn/docker-compose.yml</code>：</p>
<div class="custom-code-block" data-language="yaml"><div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">services</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">coturn</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">image</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> coturn/coturn</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">latest</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">ports</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"3478:3478/udp"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"3478:3478/tcp"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"49152-49200:49152-49200/udp"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">command</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">&gt;</span><span class="token scalar string" style="color:#e3116c"></span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">      -n --log-file=stdout</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">      --lt-cred-mech</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">      --user=test:test123</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">      --realm=webrtc.lab</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">      --min-port=49152</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">      --max-port=49200</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="22-生产环境配置">2.2 生产环境配置<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#22-%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE" class="hash-link" aria-label="Direct link to 2.2 生产环境配置" title="Direct link to 2.2 生产环境配置" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># /etc/turnserver.conf 核心配置</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">listening-port</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">3478</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tls-listening-port</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">443</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">listening-ip</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">0.0</span><span class="token plain">.0.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">relay-ip</span><span class="token operator" style="color:#393A34">=</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">SERVER_PUBLIC_IP</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">external-ip</span><span class="token operator" style="color:#393A34">=</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">SERVER_PUBLIC_IP</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">use-auth-secret</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">static-auth-secret</span><span class="token operator" style="color:#393A34">=</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">YOUR_HMAC_SECRET</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token assign-left variable" style="color:#36acaa">realm</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">turn.example.com</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">no-multicast-peers</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">no-cli</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">stale-nonce</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">600</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">max-bps</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">3000000</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">user-quota</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">10</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">total-quota</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">5000</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">min-port</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">49152</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">max-port</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">65535</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">log-file</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">/var/log/turnserver.log</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">verbose</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">prometheus</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">配置项</th><th style="text-align:left">开发</th><th style="text-align:left">生产</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">认证</td><td style="text-align:left"><code>--lt-cred-mech --user=test:test123</code></td><td style="text-align:left"><code>use-auth-secret</code></td><td style="text-align:left">生产禁止明文密码</td></tr><tr><td style="text-align:left">TLS</td><td style="text-align:left">可选</td><td style="text-align:left"><strong>必须 443</strong></td><td style="text-align:left">穿透企业防火墙</td></tr><tr><td style="text-align:left"><code>--max-bps</code></td><td style="text-align:left">不限</td><td style="text-align:left">3M–5M</td><td style="text-align:left">防止单用户占满带宽</td></tr><tr><td style="text-align:left">端口范围</td><td style="text-align:left">49152-49200</td><td style="text-align:left">49152-65535</td><td style="text-align:left">每个 relay 会话占用 1 端口</td></tr><tr><td style="text-align:left"><code>--no-multicast-peers</code></td><td style="text-align:left">—</td><td style="text-align:left">必须</td><td style="text-align:left">WebRTC 不需要多播</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>开放 49152-65535 UDP 是必须的</div><div class="admonitionContent_BuS1"><p>TURN relay 为每个会话分配一个 UDP 端口。安全组/防火墙必须开放此范围，否则 Allocate 成功但 media relay 失败——这是最常见的生产部署错误。</p></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="23-nat-后的-coturn-部署">2.3 NAT 后的 coturn 部署<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#23-nat-%E5%90%8E%E7%9A%84-coturn-%E9%83%A8%E7%BD%B2" class="hash-link" aria-label="Direct link to 2.3 NAT 后的 coturn 部署" title="Direct link to 2.3 NAT 后的 coturn 部署" translate="no">​</a></h3>
<!-- -->
<p>云服务器部署时务必设置 <code>--external-ip=&lt;公网IP&gt;/&lt;内网IP&gt;</code>，否则 relay 地址可能是内网 IP，客户端无法连接。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三turn-rest-api-短期凭证">三、TURN REST API 短期凭证<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#%E4%B8%89turn-rest-api-%E7%9F%AD%E6%9C%9F%E5%87%AD%E8%AF%81" class="hash-link" aria-label="Direct link to 三、TURN REST API 短期凭证" title="Direct link to 三、TURN REST API 短期凭证" translate="no">​</a></h2>
<p><a href="https://datatracker.ietf.org/doc/html/rfc8656" target="_blank" rel="noopener noreferrer" class="">RFC 8656</a> 定义 REST API 短期凭证机制：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-凭证生成nodejs">3.1 凭证生成（Node.js）<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#31-%E5%87%AD%E8%AF%81%E7%94%9F%E6%88%90nodejs" class="hash-link" aria-label="Direct link to 3.1 凭证生成（Node.js）" title="Direct link to 3.1 凭证生成（Node.js）" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// examples/webrtc-lab/signaling/ 扩展</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports">crypto</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"crypto"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateTurnCredentials</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">secret</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> identity</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> ttlSeconds </span><span class="token parameter operator" style="color:#393A34">=</span><span class="token parameter"> </span><span class="token parameter number" style="color:#36acaa">86400</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> timestamp </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">Math</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">floor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> ttlSeconds</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> username </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">timestamp</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">:</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">identity</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> hmac </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> crypto</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createHmac</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"sha1"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> secret</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  hmac</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">update</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">username</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> credential </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> hmac</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">digest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"base64"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    username</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    credential</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">ttl</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> ttlSeconds</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token string" style="color:#e3116c">"turn:turn.example.com:443?transport=tcp"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token string" style="color:#e3116c">"turn:turn.example.com:443?transport=udp"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token string" style="color:#e3116c">"turns:turn.example.com:443?transport=tcp"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        username</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        credential</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"stun:stun.example.com:3478"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">post</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/rooms/join"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">req</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> res</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">body</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> turnCreds </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateTurnCredentials</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">TURN_SECRET</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> turnCreds</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceServers</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-凭证时效与刷新">3.2 凭证时效与刷新<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#32-%E5%87%AD%E8%AF%81%E6%97%B6%E6%95%88%E4%B8%8E%E5%88%B7%E6%96%B0" class="hash-link" aria-label="Direct link to 3.2 凭证时效与刷新" title="Direct link to 3.2 凭证时效与刷新" translate="no">​</a></h3>
<!-- -->
<table><thead><tr><th style="text-align:left">参数</th><th style="text-align:left">推荐值</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">TTL</td><td style="text-align:left">24h（86400s）</td><td style="text-align:left">长会议足够，短于攻击窗口</td></tr><tr><td style="text-align:left">刷新阈值</td><td style="text-align:left">剩余 10min</td><td style="text-align:left">客户端主动刷新</td></tr><tr><td style="text-align:left">Secret 轮换</td><td style="text-align:left">每 90 天</td><td style="text-align:left">双 Secret 过渡期</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四带宽成本模型">四、带宽成本模型<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#%E5%9B%9B%E5%B8%A6%E5%AE%BD%E6%88%90%E6%9C%AC%E6%A8%A1%E5%9E%8B" class="hash-link" aria-label="Direct link to 四、带宽成本模型" title="Direct link to 四、带宽成本模型" translate="no">​</a></h2>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-turn-带宽计算公式">4.1 TURN 带宽计算公式<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#41-turn-%E5%B8%A6%E5%AE%BD%E8%AE%A1%E7%AE%97%E5%85%AC%E5%BC%8F" class="hash-link" aria-label="Direct link to 4.1 TURN 带宽计算公式" title="Direct link to 4.1 TURN 带宽计算公式" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">单用户 TURN 消耗 = 媒体码率 × 2（入站 relay + 出站 relay）</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">1000 用户 TURN 总消耗 = 1000 × relay_ratio × 码率 × 2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">示例：</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  1000 用户，25% relay，720p 1.5Mbps</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  = 1000 × 0.25 × 1.5M × 2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  = 750 Mbps TURN 带宽</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">场景</th><th style="text-align:left">码率</th><th style="text-align:left">relay 占比</th><th style="text-align:left">1000 用户 TURN 带宽</th></tr></thead><tbody><tr><td style="text-align:left">音频 only</td><td style="text-align:left">50 kbps</td><td style="text-align:left">20%</td><td style="text-align:left">20 Mbps</td></tr><tr><td style="text-align:left">360p 视频</td><td style="text-align:left">500 kbps</td><td style="text-align:left">25%</td><td style="text-align:left">250 Mbps</td></tr><tr><td style="text-align:left">720p 视频</td><td style="text-align:left">1.5 Mbps</td><td style="text-align:left">25%</td><td style="text-align:left">750 Mbps</td></tr><tr><td style="text-align:left">1080p 视频</td><td style="text-align:left">3 Mbps</td><td style="text-align:left">30%</td><td style="text-align:left">1.8 Gbps</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-单节点容量规划">4.2 单节点容量规划<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#42-%E5%8D%95%E8%8A%82%E7%82%B9%E5%AE%B9%E9%87%8F%E8%A7%84%E5%88%92" class="hash-link" aria-label="Direct link to 4.2 单节点容量规划" title="Direct link to 4.2 单节点容量规划" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">实例规格</th><th style="text-align:left">网卡带宽</th><th style="text-align:left">720p relay 会话数（估算）</th></tr></thead><tbody><tr><td style="text-align:left">4 vCPU / 8GB</td><td style="text-align:left">1 Gbps</td><td style="text-align:left">~300 并发 relay</td></tr><tr><td style="text-align:left">8 vCPU / 16GB</td><td style="text-align:left">5 Gbps</td><td style="text-align:left">~1500 并发 relay</td></tr><tr><td style="text-align:left">16 vCPU / 32GB</td><td style="text-align:left">10 Gbps</td><td style="text-align:left">~3000 并发 relay</td></tr></tbody></table>
<p>估算假设：每路 relay 720p ≈ 1.5Mbps × 2 = 3Mbps TURN 消耗。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="43-成本优化策略">4.3 成本优化策略<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#43-%E6%88%90%E6%9C%AC%E4%BC%98%E5%8C%96%E7%AD%96%E7%95%A5" class="hash-link" aria-label="Direct link to 4.3 成本优化策略" title="Direct link to 4.3 成本优化策略" translate="no">​</a></h3>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五多区域部署策略">五、多区域部署策略<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#%E4%BA%94%E5%A4%9A%E5%8C%BA%E5%9F%9F%E9%83%A8%E7%BD%B2%E7%AD%96%E7%95%A5" class="hash-link" aria-label="Direct link to 五、多区域部署策略" title="Direct link to 五、多区域部署策略" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-geodns">5.1 GeoDNS<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#51-geodns" class="hash-link" aria-label="Direct link to 5.1 GeoDNS" title="Direct link to 5.1 GeoDNS" translate="no">​</a></h3>
<!-- -->
<table><thead><tr><th style="text-align:left">策略</th><th style="text-align:left">复杂度</th><th style="text-align:left">效果</th><th style="text-align:left">适用</th></tr></thead><tbody><tr><td style="text-align:left"><strong>GeoDNS</strong></td><td style="text-align:left">低</td><td style="text-align:left">按地理位置解析</td><td style="text-align:left">大多数场景</td></tr><tr><td style="text-align:left"><strong>客户端测速</strong></td><td style="text-align:left">中</td><td style="text-align:left">并行 STUN 多个区域，选 RTT 最低</td><td style="text-align:left">对延迟敏感</td></tr><tr><td style="text-align:left"><strong>Anycast</strong></td><td style="text-align:left">高</td><td style="text-align:left">同一 IP 多区域广播</td><td style="text-align:left">大规模全球部署</td></tr><tr><td style="text-align:left"><strong>SFU 内置 TURN</strong></td><td style="text-align:left">低</td><td style="text-align:left">LiveKit 自动管理</td><td style="text-align:left">LiveKit Cloud/自托管</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-客户端并行测速选-turn">5.2 客户端并行测速选 TURN<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#52-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%B9%B6%E8%A1%8C%E6%B5%8B%E9%80%9F%E9%80%89-turn" class="hash-link" aria-label="Direct link to 5.2 客户端并行测速选 TURN" title="Direct link to 5.2 客户端并行测速选 TURN" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">selectBestTurn</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">turnServers</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> results </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token known-class-name class-name">Promise</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">all</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    turnServers</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">server</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> start </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">performance</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">try</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">server</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createDataChannel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"probe"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> offer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Promise</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">resolve</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onicecandidate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"relay"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">resolve</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">resolve</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token function" style="color:#d73a49">setTimeout</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">resolve</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">close</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> server</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rtt</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">performance</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> start </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> server</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rtt</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">Infinity</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> results</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">sort</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">a</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> a</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">rtt</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">rtt</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">server</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="53-livekit-内置-turn">5.3 LiveKit 内置 TURN<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#53-livekit-%E5%86%85%E7%BD%AE-turn" class="hash-link" aria-label="Direct link to 5.3 LiveKit 内置 TURN" title="Direct link to 5.3 LiveKit 内置 TURN" translate="no">​</a></h3>
<p>LiveKit Cloud 自动管理 TURN 凭证和区域选择。自托管配置：</p>
<div class="custom-code-block" data-language="yaml"><div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># livekit.yaml</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">turn</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">enabled</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">domain</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> turn.example.com</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">tls_port</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">443</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">udp_port</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">443</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">external_tls</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><br></div></code></pre></div></div></div>
<p>详见 <a href="https://docs.livekit.io/home/self-hosting/deployment/" target="_blank" rel="noopener noreferrer" class="">LiveKit TURN 文档</a> 与 <a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍</a>。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六高可用与扩容">六、高可用与扩容<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#%E5%85%AD%E9%AB%98%E5%8F%AF%E7%94%A8%E4%B8%8E%E6%89%A9%E5%AE%B9" class="hash-link" aria-label="Direct link to 六、高可用与扩容" title="Direct link to 六、高可用与扩容" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">指标</th><th style="text-align:left">告警阈值</th><th style="text-align:left">扩容动作</th></tr></thead><tbody><tr><td style="text-align:left">活跃会话数</td><td style="text-align:left">&gt; 单节点 80% 容量</td><td style="text-align:left">加 coturn 实例</td></tr><tr><td style="text-align:left">出站带宽</td><td style="text-align:left">&gt; 节点网卡 70%</td><td style="text-align:left">加节点或升级带宽</td></tr><tr><td style="text-align:left">Allocate 失败率</td><td style="text-align:left">&gt; 1%</td><td style="text-align:left">查端口范围/Secret</td></tr><tr><td style="text-align:left">凭证验证失败</td><td style="text-align:left">&gt; 0.1%</td><td style="text-align:left">查 Secret 同步/时钟偏移</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="61-coturn-prometheus-指标">6.1 coturn Prometheus 指标<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#61-coturn-prometheus-%E6%8C%87%E6%A0%87" class="hash-link" aria-label="Direct link to 6.1 coturn Prometheus 指标" title="Direct link to 6.1 coturn Prometheus 指标" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># turnserver.conf</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">prometheus</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 默认 :9641/metrics</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 关键指标</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># turn_total_allocations</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># turn_total_traffic_bytes</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># turn_total_traffic_packets</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="62-coturn-集群限制">6.2 coturn 集群限制<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#62-coturn-%E9%9B%86%E7%BE%A4%E9%99%90%E5%88%B6" class="hash-link" aria-label="Direct link to 6.2 coturn 集群限制" title="Direct link to 6.2 coturn 集群限制" translate="no">​</a></h3>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>coturn 无原生集群状态同步</div><div class="admonitionContent_BuS1"><p>coturn 实例之间<strong>不共享</strong> relay 会话状态。LB 应使用 <strong>session affinity（源 IP 粘性）</strong>，或让每个客户端独立 Allocate。Secret 必须在所有实例间同步。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七安全加固">七、安全加固<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#%E4%B8%83%E5%AE%89%E5%85%A8%E5%8A%A0%E5%9B%BA" class="hash-link" aria-label="Direct link to 七、安全加固" title="Direct link to 七、安全加固" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">措施</th><th style="text-align:left">配置</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">短期凭证</td><td style="text-align:left"><code>use-auth-secret</code></td><td style="text-align:left">禁止长期密码</td></tr><tr><td style="text-align:left">带宽配额</td><td style="text-align:left"><code>--max-bps=3000000</code></td><td style="text-align:left">单用户上限</td></tr><tr><td style="text-align:left">会话配额</td><td style="text-align:left"><code>--user-quota=10</code></td><td style="text-align:left">防滥用</td></tr><tr><td style="text-align:left">TLS 必须</td><td style="text-align:left"><code>--tls-listening-port=443</code></td><td style="text-align:left">加密 + 防火墙穿透</td></tr><tr><td style="text-align:left">禁用 CLI</td><td style="text-align:left"><code>--no-cli</code></td><td style="text-align:left">防远程管理攻击</td></tr><tr><td style="text-align:left">日志审计</td><td style="text-align:left"><code>--verbose</code> + 集中采集</td><td style="text-align:left">追踪异常 Allocate</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八防火墙与企业网络">八、防火墙与企业网络<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#%E5%85%AB%E9%98%B2%E7%81%AB%E5%A2%99%E4%B8%8E%E4%BC%81%E4%B8%9A%E7%BD%91%E7%BB%9C" class="hash-link" aria-label="Direct link to 八、防火墙与企业网络" title="Direct link to 八、防火墙与企业网络" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">端口</th><th style="text-align:left">协议</th><th style="text-align:left">用途</th><th style="text-align:left">必须</th></tr></thead><tbody><tr><td style="text-align:left">3478</td><td style="text-align:left">UDP/TCP</td><td style="text-align:left">STUN/TURN</td><td style="text-align:left">是</td></tr><tr><td style="text-align:left">443</td><td style="text-align:left">TCP/TLS</td><td style="text-align:left">TURNS 穿透防火墙</td><td style="text-align:left">生产必须</td></tr><tr><td style="text-align:left">49152-65535</td><td style="text-align:left">UDP</td><td style="text-align:left">TURN relay 媒体</td><td style="text-align:left">是（UDP relay）</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九常见陷阱">九、常见陷阱<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#%E4%B9%9D%E5%B8%B8%E8%A7%81%E9%99%B7%E9%98%B1" class="hash-link" aria-label="Direct link to 九、常见陷阱" title="Direct link to 九、常见陷阱" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:center">#</th><th style="text-align:left">陷阱</th><th style="text-align:left">现象</th><th style="text-align:left">修复</th></tr></thead><tbody><tr><td style="text-align:center">1</td><td style="text-align:left">未开放 UDP 49152-65535</td><td style="text-align:left">Allocate 成功但无 media</td><td style="text-align:left">安全组开放端口范围</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><code>external-ip</code> 未配置</td><td style="text-align:left">relay 地址是内网 IP</td><td style="text-align:left">设置 <code>--external-ip</code></td></tr><tr><td style="text-align:center">3</td><td style="text-align:left">长期凭证泄露</td><td style="text-align:left">带宽被滥用</td><td style="text-align:left">切换 REST API 短期凭证</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left">单 TURN 节点</td><td style="text-align:left">跨区 RTT 高</td><td style="text-align:left">多区域 GeoDNS</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left">忽略 relay 占比</td><td style="text-align:left">成本失控</td><td style="text-align:left">Prometheus 监控 + 告警</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left">STUN/TURN 混用同一域名</td><td style="text-align:left">DNS 解析混乱</td><td style="text-align:left">STUN 和 TURN 分开域名</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left">时钟偏移</td><td style="text-align:left">凭证验证失败</td><td style="text-align:left">NTP 同步所有节点</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left">LB 无 session affinity</td><td style="text-align:left">relay 会话中断</td><td style="text-align:left">源 IP 粘性或客户端重 Allocate</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left">仅 TCP TURN 无 UDP</td><td style="text-align:left">媒体延迟高</td><td style="text-align:left">优先 UDP relay + TCP fallback</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left">Secret 不一致</td><td style="text-align:left">部分节点验证失败</td><td style="text-align:left">配置管理同步 Secret</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十实战-lab">十、实战 Lab<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#%E5%8D%81%E5%AE%9E%E6%88%98-lab" class="hash-link" aria-label="Direct link to 十、实战 Lab" title="Direct link to 十、实战 Lab" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-1docker-coturn-启动与验证">Lab 1：Docker coturn 启动与验证<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#lab-1docker-coturn-%E5%90%AF%E5%8A%A8%E4%B8%8E%E9%AA%8C%E8%AF%81" class="hash-link" aria-label="Direct link to Lab 1：Docker coturn 启动与验证" title="Direct link to Lab 1：Docker coturn 启动与验证" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token builtin class-name">cd</span><span class="token plain"> examples/webrtc-lab/docker/coturn</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">docker</span><span class="token plain"> compose up </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">turnutils_stunclient localhost</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">turnutils_uclient </span><span class="token parameter variable" style="color:#36acaa">-T</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-u</span><span class="token plain"> </span><span class="token builtin class-name">test</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-w</span><span class="token plain"> test123 localhost</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-2浏览器-turn-验证">Lab 2：浏览器 TURN 验证<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#lab-2%E6%B5%8F%E8%A7%88%E5%99%A8-turn-%E9%AA%8C%E8%AF%81" class="hash-link" aria-label="Direct link to Lab 2：浏览器 TURN 验证" title="Direct link to Lab 2：浏览器 TURN 验证" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"turn:localhost:3478"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">username</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"test"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">credential</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"test123"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createDataChannel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"test"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">then</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">o</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">o</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onicecandidate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">address</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-3rest-api-凭证">Lab 3：REST API 凭证<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#lab-3rest-api-%E5%87%AD%E8%AF%81" class="hash-link" aria-label="Direct link to Lab 3：REST API 凭证" title="Direct link to Lab 3：REST API 凭证" translate="no">​</a></h3>
<ol>
<li class="">部署带 <code>use-auth-secret</code> 的 coturn</li>
<li class="">用 <code>generateTurnCredentials()</code> 生成凭证</li>
<li class="">浏览器连接，确认 relay 成功</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-4带宽限制验证">Lab 4：带宽限制验证<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#lab-4%E5%B8%A6%E5%AE%BD%E9%99%90%E5%88%B6%E9%AA%8C%E8%AF%81" class="hash-link" aria-label="Direct link to Lab 4：带宽限制验证" title="Direct link to Lab 4：带宽限制验证" translate="no">​</a></h3>
<p>配置 <code>--max-bps=500000</code>，720p 通话观察画质限制在 ~500kbps。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-5relay-占比统计">Lab 5：relay 占比统计<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#lab-5relay-%E5%8D%A0%E6%AF%94%E7%BB%9F%E8%AE%A1" class="hash-link" aria-label="Direct link to Lab 5：relay 占比统计" title="Direct link to Lab 5：relay 占比统计" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate-pair"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">state</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"succeeded"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">nominated</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Local:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localCandidateType</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Remote:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">remoteCandidateType</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-6tls-443-穿透">Lab 6：TLS 443 穿透<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#lab-6tls-443-%E7%A9%BF%E9%80%8F" class="hash-link" aria-label="Direct link to Lab 6：TLS 443 穿透" title="Direct link to Lab 6：TLS 443 穿透" translate="no">​</a></h3>
<p>配置 <code>tls-listening-port=443</code> + 证书，验证 <code>turns:</code> 连接成功。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-7跨区-turn-延迟对比">Lab 7：跨区 TURN 延迟对比<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#lab-7%E8%B7%A8%E5%8C%BA-turn-%E5%BB%B6%E8%BF%9F%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to Lab 7：跨区 TURN 延迟对比" title="Direct link to Lab 7：跨区 TURN 延迟对比" translate="no">​</a></h3>
<ol>
<li class="">部署两个区域 coturn（或模拟 DNS 解析）</li>
<li class="">用 <code>selectBestTurn()</code> 测 RTT</li>
<li class="">对比选优前后 ICE 连接时间</li>
</ol>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十一本章小结">十一、本章小结<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#%E5%8D%81%E4%B8%80%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="Direct link to 十一、本章小结" title="Direct link to 十一、本章小结" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">概念</th><th style="text-align:left">要点</th></tr></thead><tbody><tr><td style="text-align:left">coturn</td><td style="text-align:left">最流行的 TURN 服务器，生产必配 TLS 443</td></tr><tr><td style="text-align:left">REST API</td><td style="text-align:left">短期 HMAC 凭证，禁止长期密码</td></tr><tr><td style="text-align:left">带宽成本</td><td style="text-align:left">relay = 码率 × 2 × relay 占比</td></tr><tr><td style="text-align:left">多区域</td><td style="text-align:left">GeoDNS + 区域 TURN 集群</td></tr><tr><td style="text-align:left">监控</td><td style="text-align:left">relay 占比 + 带宽 + Allocate 失败率</td></tr><tr><td style="text-align:left">安全</td><td style="text-align:left">max-bps + user-quota + use-auth-secret</td></tr><tr><td style="text-align:left">历史</td><td style="text-align:left">Marratech 企业网 → 公网 TURN 必需品</td></tr></tbody></table>
<p><strong>下一篇（Ch15）</strong>：<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 视频会议系统</a></p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8656" target="_blank" rel="noopener noreferrer" class="">RFC 8656 — TURN REST API</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8489" target="_blank" rel="noopener noreferrer" class="">RFC 8489 — STUN</a></li>
<li class=""><a href="https://github.com/coturn/coturn/wiki" target="_blank" rel="noopener noreferrer" class="">coturn wiki</a></li>
<li class=""><a href="https://hub.docker.com/r/coturn/coturn" target="_blank" rel="noopener noreferrer" class="">coturn Docker</a></li>
<li class=""><a href="https://docs.livekit.io/home/self-hosting/distributed/" target="_blank" rel="noopener noreferrer" class="">LiveKit Distributed Deployment</a></li>
<li class=""><a href="https://docs.livekit.io/home/self-hosting/deployment/" target="_blank" rel="noopener noreferrer" class="">LiveKit TURN Configuration</a></li>
<li class=""><a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍 — 本站推荐</a></li>
<li class=""><a href="https://webrtcforthecurious.com/docs/06-connecting/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — ICE</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史（Serge Lachapelle 访谈）</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>STUN/TURN</category>
            <category>实时通信</category>
            <category>Production</category>
            <category>Deep Dive</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (13)：调试工具链与可观测性]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-debugging-observability</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-debugging-observability</guid>
            <pubDate>Wed, 24 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[chrome://webrtc-internals 读法、getStats 指标化、常见问题诊断树与三层日志规范]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"理解协议意图，才能高效 Debug。" — <a href="https://webrtcforthecurious.com/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious</a></p>
</blockquote>
<p>WebRTC 的调试难度在于：<strong>媒体走 UDP 直连，信令走 WebSocket，加密层覆盖全链路</strong>——传统 HTTP 调试工具几乎无用。Serge Lachapelle 在 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 历史访谈</a> 中提到，Marratech 时代最大的工程挑战不是编解码，而是<strong>在不可控的公网环境中定位连接失败</strong>——这个问题今天依然有效。</p>
<p>本章汇总生产排障工具链，回顾 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">Ch5 ICE</a>、<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">Ch7 DTLS</a>、<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">Ch8 RTCP</a>、<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">Ch10 GCC</a> 所有失败模式，建立系统化的可观测性体系。</p>
<p>配套 Lab：<code>examples/webrtc-lab/</code> 全模块 + 故障注入脚本。</p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>webrtc-internals</strong></td><td style="text-align:left">—</td><td style="text-align:left">Chrome 内置 WebRTC 调试页面，暴露所有 PC 内部状态</td></tr><tr><td style="text-align:left"><strong>getStats</strong></td><td style="text-align:left">—</td><td style="text-align:left">W3C API，获取 RTCPeerConnection 的实时统计指标</td></tr><tr><td style="text-align:left"><strong>Candidate Pair</strong></td><td style="text-align:left">候选对</td><td style="text-align:left">ICE 连通性检查成功后选中的本地+远程地址组合</td></tr><tr><td style="text-align:left"><strong>Nominated</strong></td><td style="text-align:left">提名</td><td style="text-align:left">ICE 最终选定的 candidate pair</td></tr><tr><td style="text-align:left"><strong>Jitter Buffer</strong></td><td style="text-align:left">抖动缓冲</td><td style="text-align:left">接收端平滑包到达时间差异的缓冲队列</td></tr><tr><td style="text-align:left"><strong>PLI</strong></td><td style="text-align:left">Picture Loss Indication</td><td style="text-align:left">关键帧请求 RTCP 反馈</td></tr><tr><td style="text-align:left"><strong>FIR</strong></td><td style="text-align:left">Full Intra Request</td><td style="text-align:left">强制 I 帧请求</td></tr><tr><td style="text-align:left"><strong>Observability</strong></td><td style="text-align:left">可观测性</td><td style="text-align:left">通过 Metrics + Logs + Traces 理解系统行为</td></tr><tr><td style="text-align:left"><strong>SLO</strong></td><td style="text-align:left">Service Level Objective</td><td style="text-align:left">服务质量目标，如首帧 &lt; 2s</td></tr><tr><td style="text-align:left"><strong>OpenTelemetry</strong></td><td style="text-align:left">—</td><td style="text-align:left">分布式追踪标准，串联三层日志</td></tr><tr><td style="text-align:left"><strong>rtcstats</strong></td><td style="text-align:left">—</td><td style="text-align:left">标准化 WebRTC 统计 JSON 格式</td></tr><tr><td style="text-align:left"><strong>SSLKEYLOGFILE</strong></td><td style="text-align:left">—</td><td style="text-align:left">导出 DTLS 密钥供 Wireshark 解密</td></tr><tr><td style="text-align:left"><strong>Runbook</strong></td><td style="text-align:left">运维手册</td><td style="text-align:left">告警触发后的标准排查步骤</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一调试工具全景">一、调试工具全景<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#%E4%B8%80%E8%B0%83%E8%AF%95%E5%B7%A5%E5%85%B7%E5%85%A8%E6%99%AF" class="hash-link" aria-label="Direct link to 一、调试工具全景" title="Direct link to 一、调试工具全景" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">工具</th><th style="text-align:left">适用场景</th><th style="text-align:left">平台</th></tr></thead><tbody><tr><td style="text-align:left"><code>chrome://webrtc-internals</code></td><td style="text-align:left">浏览器端全链路状态</td><td style="text-align:left">Chrome/Edge</td></tr><tr><td style="text-align:left"><code>about:webrtc</code></td><td style="text-align:left">Firefox 等效页面</td><td style="text-align:left">Firefox</td></tr><tr><td style="text-align:left">Wireshark + SSLKEYLOGFILE</td><td style="text-align:left">抓包分析 DTLS/SRTP</td><td style="text-align:left">全平台</td></tr><tr><td style="text-align:left"><code>tc netem</code></td><td style="text-align:left">注入网络故障</td><td style="text-align:left">Linux/macOS</td></tr><tr><td style="text-align:left">LiveKit Dashboard</td><td style="text-align:left">SFU Room/Participant 状态</td><td style="text-align:left">LiveKit 部署</td></tr><tr><td style="text-align:left">Prometheus + Grafana</td><td style="text-align:left">生产指标监控</td><td style="text-align:left">全栈</td></tr></tbody></table>
<p>Marratech 工程师在 2000 年代靠 tcpdump 和自研日志定位问题；今天 Chrome webrtc-internals + getStats + Prometheus 提供了更系统的可观测性栈，但<strong>分层排查思路</strong>不变：信令 → ICE → DTLS → 媒体。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二chromewebrtc-internals-深度读法">二、chrome://webrtc-internals 深度读法<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#%E4%BA%8Cchromewebrtc-internals-%E6%B7%B1%E5%BA%A6%E8%AF%BB%E6%B3%95" class="hash-link" aria-label="Direct link to 二、chrome://webrtc-internals 深度读法" title="Direct link to 二、chrome://webrtc-internals 深度读法" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-页面结构">2.1 页面结构<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#21-%E9%A1%B5%E9%9D%A2%E7%BB%93%E6%9E%84" class="hash-link" aria-label="Direct link to 2.1 页面结构" title="Direct link to 2.1 页面结构" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="22-关键字段与告警阈值">2.2 关键字段与告警阈值<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#22-%E5%85%B3%E9%94%AE%E5%AD%97%E6%AE%B5%E4%B8%8E%E5%91%8A%E8%AD%A6%E9%98%88%E5%80%BC" class="hash-link" aria-label="Direct link to 2.2 关键字段与告警阈值" title="Direct link to 2.2 关键字段与告警阈值" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">位置</th><th style="text-align:left">含义</th><th style="text-align:left">告警阈值</th></tr></thead><tbody><tr><td style="text-align:left"><code>packetsLost / packetsReceived</code></td><td style="text-align:left">inbound-rtp</td><td style="text-align:left">丢包率</td><td style="text-align:left">&gt; 2% 画质下降</td></tr><tr><td style="text-align:left"><code>jitter</code></td><td style="text-align:left">inbound-rtp</td><td style="text-align:left">到达时间抖动</td><td style="text-align:left">持续 &gt; 30ms</td></tr><tr><td style="text-align:left"><code>jitterBufferDelay</code></td><td style="text-align:left">inbound-rtp</td><td style="text-align:left">抖动缓冲延迟</td><td style="text-align:left">&gt; 200ms 交互延迟明显</td></tr><tr><td style="text-align:left"><code>currentRoundTripTime</code></td><td style="text-align:left">candidate-pair</td><td style="text-align:left">RTT</td><td style="text-align:left">&gt; 300ms</td></tr><tr><td style="text-align:left"><code>availableOutgoingBitrate</code></td><td style="text-align:left">candidate-pair</td><td style="text-align:left">估计可用带宽</td><td style="text-align:left">远低于预期码率</td></tr><tr><td style="text-align:left"><code>targetBitrate</code></td><td style="text-align:left">outbound-rtp</td><td style="text-align:left">GCC 目标码率</td><td style="text-align:left">持续低于 maxBitrate 30%</td></tr><tr><td style="text-align:left"><code>qualityLimitationReason</code></td><td style="text-align:left">outbound-rtp</td><td style="text-align:left">降质原因</td><td style="text-align:left"><code>bandwidth</code> / <code>cpu</code></td></tr><tr><td style="text-align:left"><code>framesDecoded / framesDropped</code></td><td style="text-align:left">inbound-rtp</td><td style="text-align:left">解码/丢弃帧</td><td style="text-align:left">dropped/decoded &gt; 5%</td></tr><tr><td style="text-align:left"><code>iceConnectionState</code></td><td style="text-align:left">顶层</td><td style="text-align:left">ICE 状态</td><td style="text-align:left"><code>failed</code> / <code>disconnected</code></td></tr><tr><td style="text-align:left"><code>connectionState</code></td><td style="text-align:left">顶层</td><td style="text-align:left">整体连接状态</td><td style="text-align:left"><code>failed</code></td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="23-ice-candidate-pair-分析">2.3 ICE Candidate Pair 分析<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#23-ice-candidate-pair-%E5%88%86%E6%9E%90" class="hash-link" aria-label="Direct link to 2.3 ICE Candidate Pair 分析" title="Direct link to 2.3 ICE Candidate Pair 分析" translate="no">​</a></h3>
<!-- -->
<p>在 webrtc-internals 的 ICE 标签中，找到 <code>state=succeeded</code> 且 <code>nominated=true</code> 的行：</p>
<table><thead><tr><th style="text-align:left">组合</th><th style="text-align:left">含义</th><th style="text-align:left">性能</th></tr></thead><tbody><tr><td style="text-align:left">host ↔ host</td><td style="text-align:left">同局域网直连</td><td style="text-align:left">最优</td></tr><tr><td style="text-align:left">srflx ↔ srflx</td><td style="text-align:left">公网 NAT 穿透</td><td style="text-align:left">良好</td></tr><tr><td style="text-align:left">relay ↔ relay</td><td style="text-align:left">TURN 中继</td><td style="text-align:left">额外延迟 + 服务器带宽</td></tr><tr><td style="text-align:left">host ↔ relay</td><td style="text-align:left">混合</td><td style="text-align:left">一端在 NAT 后</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>TURN 占比监控</div><div class="admonitionContent_BuS1"><p>生产环境应统计 <code>candidateType=relay</code> 的连接占比。超过 25% 说明 NAT 穿透率低，需优化 STUN 部署或检查防火墙策略（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">Ch14</a>）。</p></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="24-events-标签状态机追踪">2.4 Events 标签：状态机追踪<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#24-events-%E6%A0%87%E7%AD%BE%E7%8A%B6%E6%80%81%E6%9C%BA%E8%BF%BD%E8%B8%AA" class="hash-link" aria-label="Direct link to 2.4 Events 标签：状态机追踪" title="Direct link to 2.4 Events 标签：状态机追踪" translate="no">​</a></h3>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三getstats-api-完整指南">三、getStats API 完整指南<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#%E4%B8%89getstats-api-%E5%AE%8C%E6%95%B4%E6%8C%87%E5%8D%97" class="hash-link" aria-label="Direct link to 三、getStats API 完整指南" title="Direct link to 三、getStats API 完整指南" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-统计报告类型">3.1 统计报告类型<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#31-%E7%BB%9F%E8%AE%A1%E6%8A%A5%E5%91%8A%E7%B1%BB%E5%9E%8B" class="hash-link" aria-label="Direct link to 3.1 统计报告类型" title="Direct link to 3.1 统计报告类型" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-生产级采集器">3.2 生产级采集器<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#32-%E7%94%9F%E4%BA%A7%E7%BA%A7%E9%87%87%E9%9B%86%E5%99%A8" class="hash-link" aria-label="Direct link to 3.2 生产级采集器" title="Direct link to 3.2 生产级采集器" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// examples/webrtc-lab/client/ch02-p2p-basic 扩展</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">WebRTCMetricsCollector</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">constructor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> intervalMs </span><span class="token parameter operator" style="color:#393A34">=</span><span class="token parameter"> </span><span class="token parameter number" style="color:#36acaa">5000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">intervalMs</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> intervalMs</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">timer</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onMetrics</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sessionId</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> crypto</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">randomUUID</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">start</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">timer</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">setInterval</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">collect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">intervalMs</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">stop</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">clearInterval</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">timer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">collect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> snapshot </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">sessionId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sessionId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">timestamp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">connectionState</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">connectionState</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">iceConnectionState</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceConnectionState</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">outbound</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">inbound</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">candidatePair</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">switch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"outbound-rtp"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          snapshot</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">outbound</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">kind</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">rid</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">ssrc</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ssrc</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">bytesSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">packetsSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">packetsSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">framesEncoded</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">framesEncoded</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">targetBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">targetBitrate</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">qualityLimitationReason</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">qualityLimitationReason</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">frameWidth</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">frameWidth</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">frameHeight</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">frameHeight</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"inbound-rtp"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          snapshot</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">inbound</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">kind</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">ssrc</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ssrc</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">bytesReceived</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesReceived</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">packetsReceived</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">packetsReceived</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">packetsLost</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">packetsLost</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">jitter</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">jitter</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">framesDecoded</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">framesDecoded</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">framesDropped</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">framesDropped</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">frameWidth</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">frameWidth</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token literal-property property" style="color:#36acaa">frameHeight</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">frameHeight</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate-pair"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">state</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"succeeded"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">nominated</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            snapshot</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidatePair</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">              </span><span class="token literal-property property" style="color:#36acaa">localCandidateType</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localCandidateType</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">              </span><span class="token literal-property property" style="color:#36acaa">remoteCandidateType</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">remoteCandidateType</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">              </span><span class="token literal-property property" style="color:#36acaa">currentRoundTripTime</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">currentRoundTripTime</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">              </span><span class="token literal-property property" style="color:#36acaa">availableOutgoingBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">availableOutgoingBitrate</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">              </span><span class="token literal-property property" style="color:#36acaa">bytesSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">              </span><span class="token literal-property property" style="color:#36acaa">bytesReceived</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesReceived</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onMetrics</span><span class="token operator" style="color:#393A34">?.</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">snapshot</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> snapshot</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> collector </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">WebRTCMetricsCollector</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">collector</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onMetrics</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">m</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"[Metrics]"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">m</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">collector</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">start</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="33-首帧时间测量">3.3 首帧时间测量<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#33-%E9%A6%96%E5%B8%A7%E6%97%B6%E9%97%B4%E6%B5%8B%E9%87%8F" class="hash-link" aria-label="Direct link to 3.3 首帧时间测量" title="Direct link to 3.3 首帧时间测量" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> firstFrameTime </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> joinTime </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getReceivers</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">receiver</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receiver</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">track</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">kind </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> checkFirstFrame </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">setInterval</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">receiver</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"inbound-rtp"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">framesDecoded</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">firstFrameTime</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          firstFrameTime </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"First frame:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> firstFrameTime </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> joinTime</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ms"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token function" style="color:#d73a49">clearInterval</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">checkFirstFrame</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四常见问题诊断树">四、常见问题诊断树<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#%E5%9B%9B%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E8%AF%8A%E6%96%AD%E6%A0%91" class="hash-link" aria-label="Direct link to 四、常见问题诊断树" title="Direct link to 四、常见问题诊断树" translate="no">​</a></h2>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-分层诊断速查">4.1 分层诊断速查<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#41-%E5%88%86%E5%B1%82%E8%AF%8A%E6%96%AD%E9%80%9F%E6%9F%A5" class="hash-link" aria-label="Direct link to 4.1 分层诊断速查" title="Direct link to 4.1 分层诊断速查" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">层级</th><th style="text-align:left">关键状态</th><th style="text-align:left">工具</th><th style="text-align:left">常见根因</th></tr></thead><tbody><tr><td style="text-align:left">信令</td><td style="text-align:left">WebSocket 连接</td><td style="text-align:left">信令日志</td><td style="text-align:left">WSS 证书、Room ID 错误</td></tr><tr><td style="text-align:left">ICE</td><td style="text-align:left"><code>iceConnectionState</code></td><td style="text-align:left">webrtc-internals ICE 标签</td><td style="text-align:left">无 TURN、防火墙</td></tr><tr><td style="text-align:left">DTLS</td><td style="text-align:left"><code>connectionState</code></td><td style="text-align:left">webrtc-internals Events</td><td style="text-align:left">SDP fingerprint 不匹配</td></tr><tr><td style="text-align:left">SRTP</td><td style="text-align:left"><code>framesDecoded</code></td><td style="text-align:left">getStats inbound-rtp</td><td style="text-align:left">Codec 协商失败</td></tr><tr><td style="text-align:left">GCC</td><td style="text-align:left"><code>targetBitrate</code></td><td style="text-align:left">webrtc-internals Graphs</td><td style="text-align:left">带宽不足</td></tr><tr><td style="text-align:left">SFU</td><td style="text-align:left">Track 订阅</td><td style="text-align:left">LiveKit Dashboard</td><td style="text-align:left">未 publish/subscribe</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-sfu-特有问题">4.2 SFU 特有问题<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#42-sfu-%E7%89%B9%E6%9C%89%E9%97%AE%E9%A2%98" class="hash-link" aria-label="Direct link to 4.2 SFU 特有问题" title="Direct link to 4.2 SFU 特有问题" translate="no">​</a></h3>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五三层日志规范">五、三层日志规范<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#%E4%BA%94%E4%B8%89%E5%B1%82%E6%97%A5%E5%BF%97%E8%A7%84%E8%8C%83" class="hash-link" aria-label="Direct link to 五、三层日志规范" title="Direct link to 五、三层日志规范" translate="no">​</a></h2>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-日志格式规范">5.1 日志格式规范<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#51-%E6%97%A5%E5%BF%97%E6%A0%BC%E5%BC%8F%E8%A7%84%E8%8C%83" class="hash-link" aria-label="Direct link to 5.1 日志格式规范" title="Direct link to 5.1 日志格式规范" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// Layer 1: Signaling — examples/webrtc-lab/signaling/server.js 扩展</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">layer</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"signaling"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">event</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"offer_sent"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">roomId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"meeting-123"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">peerId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"p1"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">targetPeerId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"p2"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">sdpType</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"offer"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">timestamp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Layer 2: ICE/DTLS</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">oniceconnectionstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">layer</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ice"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">event</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"state_change"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">iceConnectionState</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceConnectionState</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">connectionState</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">connectionState</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">timestamp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Layer 3: Media (每 5s 采样)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">collector</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onMetrics</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">m</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">layer</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"media"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">roomId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"meeting-123"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token spread operator" style="color:#393A34">...</span><span class="token plain">m </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-生产采样策略">5.2 生产采样策略<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#52-%E7%94%9F%E4%BA%A7%E9%87%87%E6%A0%B7%E7%AD%96%E7%95%A5" class="hash-link" aria-label="Direct link to 5.2 生产采样策略" title="Direct link to 5.2 生产采样策略" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">层级</th><th style="text-align:left">采样频率</th><th style="text-align:left">触发全量 dump</th></tr></thead><tbody><tr><td style="text-align:left">Signaling</td><td style="text-align:left">事件驱动</td><td style="text-align:left">连接失败时</td></tr><tr><td style="text-align:left">ICE/DTLS</td><td style="text-align:left">状态变化时</td><td style="text-align:left"><code>failed</code> / <code>disconnected</code></td></tr><tr><td style="text-align:left">Media</td><td style="text-align:left">每 5s</td><td style="text-align:left">丢包率 &gt; 5% 或 qualityLimitation</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="53-opentelemetry-关联">5.3 OpenTelemetry 关联<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#53-opentelemetry-%E5%85%B3%E8%81%94" class="hash-link" aria-label="Direct link to 5.3 OpenTelemetry 关联" title="Direct link to 5.3 OpenTelemetry 关联" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 伪代码：用 Trace ID 串联三层</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> span </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> tracer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">startSpan</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"webrtc.session"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">span</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setAttribute</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"room.id"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">span</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setAttribute</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"peer.id"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> peerId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">oniceconnectionstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  span</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addEvent</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ice.state"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">state</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceConnectionState</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">collector</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onMetrics</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">m</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  span</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setAttribute</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"media.target_bitrate"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> m</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">outbound</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">targetBitrate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>关联 ID 是关键</div><div class="admonitionContent_BuS1"><p>每条日志必须携带 <code>roomId</code> + <code>peerId</code> + <code>sessionId</code>，否则无法跨层关联。生产环境建议使用 OpenTelemetry Trace 串联三层。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六prometheus-指标化">六、Prometheus 指标化<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#%E5%85%ADprometheus-%E6%8C%87%E6%A0%87%E5%8C%96" class="hash-link" aria-label="Direct link to 六、Prometheus 指标化" title="Direct link to 六、Prometheus 指标化" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="61-客户端指标上报">6.1 客户端指标上报<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#61-%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%8C%87%E6%A0%87%E4%B8%8A%E6%8A%A5" class="hash-link" aria-label="Direct link to 6.1 客户端指标上报" title="Direct link to 6.1 客户端指标上报" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">reportMetrics</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> roomId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> identity</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> payload </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">timestamp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">metrics</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"inbound-rtp"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      payload</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">metrics</span><span class="token punctuation" style="color:#393A34">[</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">inbound_</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">r</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">kind</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">_packets_lost</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">packetsLost</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      payload</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">metrics</span><span class="token punctuation" style="color:#393A34">[</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">inbound_</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">r</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">kind</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">_jitter</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">jitter</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      payload</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">metrics</span><span class="token punctuation" style="color:#393A34">[</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">inbound_</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">r</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">kind</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">_fps</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">framesPerSecond</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"outbound-rtp"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      payload</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">metrics</span><span class="token punctuation" style="color:#393A34">[</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">outbound_</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">r</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">kind</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">_target_bitrate</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">targetBitrate</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      payload</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">metrics</span><span class="token punctuation" style="color:#393A34">[</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">outbound_</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">r</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">kind</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">_quality_limitation</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">qualityLimitationReason</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate-pair"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">state</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"succeeded"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">nominated</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      payload</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">metrics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">rtt</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">currentRoundTripTime</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      payload</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">metrics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">available_outgoing_bitrate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">availableOutgoingBitrate</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      payload</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">metrics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate_type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localCandidateType</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fetch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/api/metrics"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">method</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"POST"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">headers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"Content-Type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"application/json"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">body</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">payload</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="62-推荐-prometheus-指标">6.2 推荐 Prometheus 指标<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#62-%E6%8E%A8%E8%8D%90-prometheus-%E6%8C%87%E6%A0%87" class="hash-link" aria-label="Direct link to 6.2 推荐 Prometheus 指标" title="Direct link to 6.2 推荐 Prometheus 指标" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">指标名</th><th style="text-align:left">类型</th><th style="text-align:left">标签</th><th style="text-align:left">告警规则</th></tr></thead><tbody><tr><td style="text-align:left"><code>webrtc_ice_state</code></td><td style="text-align:left">Gauge</td><td style="text-align:left">room, identity, state</td><td style="text-align:left">state=failed</td></tr><tr><td style="text-align:left"><code>webrtc_rtt_seconds</code></td><td style="text-align:left">Gauge</td><td style="text-align:left">room, identity</td><td style="text-align:left">&gt; 0.3</td></tr><tr><td style="text-align:left"><code>webrtc_packets_lost_total</code></td><td style="text-align:left">Counter</td><td style="text-align:left">room, identity, kind</td><td style="text-align:left">rate &gt; 0.02</td></tr><tr><td style="text-align:left"><code>webrtc_target_bitrate_bps</code></td><td style="text-align:left">Gauge</td><td style="text-align:left">room, identity, kind</td><td style="text-align:left">&lt; 100000</td></tr><tr><td style="text-align:left"><code>webrtc_turn_relay_ratio</code></td><td style="text-align:left">Gauge</td><td style="text-align:left">region</td><td style="text-align:left">&gt; 0.25</td></tr><tr><td style="text-align:left"><code>webrtc_first_frame_seconds</code></td><td style="text-align:left">Histogram</td><td style="text-align:left">room</td><td style="text-align:left">p99 &gt; 2</td></tr><tr><td style="text-align:left"><code>webrtc_connection_success_total</code></td><td style="text-align:left">Counter</td><td style="text-align:left">room</td><td style="text-align:left">rate failed/success &gt; 0.01</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="63-livekit-服务端指标">6.3 LiveKit 服务端指标<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#63-livekit-%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8C%87%E6%A0%87" class="hash-link" aria-label="Direct link to 6.3 LiveKit 服务端指标" title="Direct link to 6.3 LiveKit 服务端指标" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> localhost:6789/metrics </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">grep</span><span class="token plain"> livekit</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># livekit_room_total</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># livekit_participant_total</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># livekit_track_published_total</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># livekit_packet_bytes_total</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七wireshark-抓包分析">七、Wireshark 抓包分析<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#%E4%B8%83wireshark-%E6%8A%93%E5%8C%85%E5%88%86%E6%9E%90" class="hash-link" aria-label="Direct link to 七、Wireshark 抓包分析" title="Direct link to 七、Wireshark 抓包分析" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="71-dtls-解密配置">7.1 DTLS 解密配置<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#71-dtls-%E8%A7%A3%E5%AF%86%E9%85%8D%E7%BD%AE" class="hash-link" aria-label="Direct link to 7.1 DTLS 解密配置" title="Direct link to 7.1 DTLS 解密配置" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token builtin class-name">export</span><span class="token plain"> </span><span class="token assign-left variable" style="color:#36acaa">SSLKEYLOGFILE</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">/tmp/sslkeys.log</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">/Applications/Google</span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"> Chrome.app/Contents/MacOS/Google</span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"> Chrome </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --ssl-key-log-file</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">/tmp/sslkeys.log</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Wireshark → Preferences → Protocols → TLS</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># → (Pre)-Master-Secret log filename: /tmp/sslkeys.log</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 过滤器: dtls &amp;&amp; ip.addr == x.x.x.x</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="72-关键-wireshark-过滤器">7.2 关键 Wireshark 过滤器<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#72-%E5%85%B3%E9%94%AE-wireshark-%E8%BF%87%E6%BB%A4%E5%99%A8" class="hash-link" aria-label="Direct link to 7.2 关键 Wireshark 过滤器" title="Direct link to 7.2 关键 Wireshark 过滤器" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">过滤器</th><th style="text-align:left">用途</th></tr></thead><tbody><tr><td style="text-align:left"><code>stun</code></td><td style="text-align:left">ICE 连通性检查</td></tr><tr><td style="text-align:left"><code>dtls</code></td><td style="text-align:left">DTLS 握手</td></tr><tr><td style="text-align:left"><code>rtcp</code></td><td style="text-align:left">RTCP 反馈（TWCC/RR/NACK）</td></tr><tr><td style="text-align:left"><code>rtp</code></td><td style="text-align:left">RTP 媒体包（需 DTLS 解密后）</td></tr><tr><td style="text-align:left"><code>turn.channeldata</code></td><td style="text-align:left">TURN 中继流量</td></tr><tr><td style="text-align:left"><code>stun.type == 0x0001</code></td><td style="text-align:left">STUN Binding Request</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="73-抓包诊断流程">7.3 抓包诊断流程<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#73-%E6%8A%93%E5%8C%85%E8%AF%8A%E6%96%AD%E6%B5%81%E7%A8%8B" class="hash-link" aria-label="Direct link to 7.3 抓包诊断流程" title="Direct link to 7.3 抓包诊断流程" translate="no">​</a></h3>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八常见陷阱">八、常见陷阱<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#%E5%85%AB%E5%B8%B8%E8%A7%81%E9%99%B7%E9%98%B1" class="hash-link" aria-label="Direct link to 八、常见陷阱" title="Direct link to 八、常见陷阱" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:center">#</th><th style="text-align:left">陷阱</th><th style="text-align:left">现象</th><th style="text-align:left">修复</th></tr></thead><tbody><tr><td style="text-align:center">1</td><td style="text-align:left">只看 <code>iceConnectionState</code></td><td style="text-align:left">DTLS 失败误判为 ICE 问题</td><td style="text-align:left">同时看 <code>connectionState</code></td></tr><tr><td style="text-align:center">2</td><td style="text-align:left">getStats 不区分 rid</td><td style="text-align:left">Simulcast 层混淆</td><td style="text-align:left">按 <code>rid</code> 分组统计</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left">日志无关联 ID</td><td style="text-align:left">无法跨层排查</td><td style="text-align:left">统一 roomId/peerId/sessionId</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left">5s 采样错过短故障</td><td style="text-align:left">间歇性卡顿无数据</td><td style="text-align:left">事件驱动 + 异常触发高频采样</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left">忽略 <code>remote-inbound-rtp</code></td><td style="text-align:left">看不到远端视角丢包</td><td style="text-align:left">同时采集 outbound + remote-inbound</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left">Wireshark 未解密 DTLS</td><td style="text-align:left">只能看到 UDP 包</td><td style="text-align:left">配置 SSLKEYLOGFILE</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left">生产未监控 TURN 占比</td><td style="text-align:left">relay 成本失控</td><td style="text-align:left">统计 candidateType=relay 比例</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left">移动端无 webrtc-internals</td><td style="text-align:left">无法浏览器调试</td><td style="text-align:left">用 getStats 上报 + 远程日志</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left">SFU 问题只看客户端</td><td style="text-align:left">层选择/订阅问题漏查</td><td style="text-align:left">LiveKit Dashboard + Webhook</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left">告警无 Runbook</td><td style="text-align:left">值班无从下手</td><td style="text-align:left">每个告警绑定诊断树</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九实战-lab制造-7-种故障">九、实战 Lab：制造 7 种故障<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#%E4%B9%9D%E5%AE%9E%E6%88%98-lab%E5%88%B6%E9%80%A0-7-%E7%A7%8D%E6%95%85%E9%9A%9C" class="hash-link" aria-label="Direct link to 九、实战 Lab：制造 7 种故障" title="Direct link to 九、实战 Lab：制造 7 种故障" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:center">#</th><th style="text-align:left">故障</th><th style="text-align:left">制造方法</th><th style="text-align:left">预期现象</th><th style="text-align:left">诊断工具</th></tr></thead><tbody><tr><td style="text-align:center">1</td><td style="text-align:left">ICE failed</td><td style="text-align:left">去掉 TURN + 对称 NAT</td><td style="text-align:left"><code>iceConnectionState=failed</code></td><td style="text-align:left">webrtc-internals ICE 标签</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left">DTLS failed</td><td style="text-align:left">改 SDP fingerprint</td><td style="text-align:left"><code>connectionState=failed</code></td><td style="text-align:left">webrtc-internals Events</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left">高丢包</td><td style="text-align:left"><code>sudo tc qdisc add dev en0 root netem loss 10%</code></td><td style="text-align:left"><code>packetsLost</code> 飙升</td><td style="text-align:left">getStats + Graphs</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left">带宽不足</td><td style="text-align:left">DevTools 限 300kbps</td><td style="text-align:left"><code>qualityLimitationReason=bandwidth</code></td><td style="text-align:left">getStats outbound-rtp</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left">单向视频</td><td style="text-align:left">Callee 不 addTrack</td><td style="text-align:left">远端 <code>ontrack</code> 不触发</td><td style="text-align:left">信令日志 + SDP 分析</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left">TURN 过载</td><td style="text-align:left">100 用户全走 relay</td><td style="text-align:left">TURN 带宽打满</td><td style="text-align:left">coturn 日志 + Prometheus</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left">Simulcast 层不切换</td><td style="text-align:left">SFU 未启用 LayerSelector</td><td style="text-align:left">低带宽仍收 h 层</td><td style="text-align:left">LiveKit Dashboard</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-详细步骤">Lab 详细步骤<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#lab-%E8%AF%A6%E7%BB%86%E6%AD%A5%E9%AA%A4" class="hash-link" aria-label="Direct link to Lab 详细步骤" title="Direct link to Lab 详细步骤" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># 环境准备</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">cd</span><span class="token plain"> examples/webrtc-lab/signaling </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">npm</span><span class="token plain"> start</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">npx serve examples/webrtc-lab/client/ch02-p2p-basic</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Lab 3: 丢包注入</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">sudo</span><span class="token plain"> tc qdisc </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> dev en0 root netem loss </span><span class="token number" style="color:#36acaa">5</span><span class="token plain">% delay 50ms</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">sudo</span><span class="token plain"> tc qdisc del dev en0 root</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Lab 4: TURN 故障</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">cd</span><span class="token plain"> examples/webrtc-lab/docker/coturn</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">docker</span><span class="token plain"> compose up </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 修改 iceServers 指向 coturn → 停止容器 → 观察 ICE failed</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">docker</span><span class="token plain"> compose down</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-8导出-webrtc-internals-dump">Lab 8：导出 webrtc-internals dump<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#lab-8%E5%AF%BC%E5%87%BA-webrtc-internals-dump" class="hash-link" aria-label="Direct link to Lab 8：导出 webrtc-internals dump" title="Direct link to Lab 8：导出 webrtc-internals dump" translate="no">​</a></h3>
<ol>
<li class=""><code>chrome://webrtc-internals</code> → 选择 PC → 点击 <strong>Create Dump</strong></li>
<li class="">保存 JSON，搜索 <code>iceConnectionState</code> 和 <code>candidate-pair</code></li>
<li class="">与 getStats 输出交叉验证</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-9三层日志关联演练">Lab 9：三层日志关联演练<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#lab-9%E4%B8%89%E5%B1%82%E6%97%A5%E5%BF%97%E5%85%B3%E8%81%94%E6%BC%94%E7%BB%83" class="hash-link" aria-label="Direct link to Lab 9：三层日志关联演练" title="Direct link to Lab 9：三层日志关联演练" translate="no">​</a></h3>
<ol>
<li class="">开启 Layer 1/2/3 日志</li>
<li class="">制造 ICE failed（关 TURN）</li>
<li class="">用 <code>sessionId</code> 串联三层日志，写出完整故障时间线</li>
</ol>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十slo-与告警设计">十、SLO 与告警设计<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#%E5%8D%81slo-%E4%B8%8E%E5%91%8A%E8%AD%A6%E8%AE%BE%E8%AE%A1" class="hash-link" aria-label="Direct link to 十、SLO 与告警设计" title="Direct link to 十、SLO 与告警设计" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">SLO</th><th style="text-align:left">测量方法</th><th style="text-align:left">数据源</th></tr></thead><tbody><tr><td style="text-align:left">首帧时间</td><td style="text-align:left"><code>framesDecoded</code> 首次 &gt; 0 的时间差</td><td style="text-align:left">getStats</td></tr><tr><td style="text-align:left">端到端延迟</td><td style="text-align:left">RTT/2 + jitterBufferDelay</td><td style="text-align:left">getStats candidate-pair + inbound-rtp</td></tr><tr><td style="text-align:left">通话成功率</td><td style="text-align:left"><code>connectionState=connected</code> 比例</td><td style="text-align:left">客户端上报</td></tr><tr><td style="text-align:left">TURN 占比</td><td style="text-align:left"><code>candidateType=relay</code> 比例</td><td style="text-align:left">getStats</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十一本章小结">十一、本章小结<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#%E5%8D%81%E4%B8%80%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="Direct link to 十一、本章小结" title="Direct link to 十一、本章小结" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">概念</th><th style="text-align:left">要点</th></tr></thead><tbody><tr><td style="text-align:left">webrtc-internals</td><td style="text-align:left">浏览器端全链路调试入口</td></tr><tr><td style="text-align:left">getStats</td><td style="text-align:left">生产指标采集的核心 API</td></tr><tr><td style="text-align:left">诊断树</td><td style="text-align:left">信令 → ICE → DTLS → 媒体 分层排查</td></tr><tr><td style="text-align:left">三层日志</td><td style="text-align:left">Signaling / ICE-DTLS / Media 关联</td></tr><tr><td style="text-align:left">Prometheus</td><td style="text-align:left">客户端 + 服务端指标化</td></tr><tr><td style="text-align:left">Wireshark</td><td style="text-align:left">DTLS 解密后分析 RTP/RTCP</td></tr><tr><td style="text-align:left">SLO</td><td style="text-align:left">首帧/延迟/成功率/TURN 占比</td></tr></tbody></table>
<p><strong>下一篇（Ch14）</strong>：<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 生产部署</a></p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://webrtcforthecurious.com/docs/09-debugging/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — Debugging</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史（Serge Lachapelle 访谈）</a></li>
<li class=""><a href="https://webrtchacks.com/" target="_blank" rel="noopener noreferrer" class="">webrtcH4cKS</a></li>
<li class=""><a href="https://webrtc.github.io/webrtc-org/native-code/native-apis/debugging/" target="_blank" rel="noopener noreferrer" class="">Chrome webrtc-internals 指南</a></li>
<li class=""><a href="https://docs.livekit.io/home/self-hosting/monitoring/" target="_blank" rel="noopener noreferrer" class="">LiveKit Observability</a></li>
<li class=""><a href="https://webrtchacks.com/wireshark-janus/" target="_blank" rel="noopener noreferrer" class="">Wireshark WebRTC 解密指南</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>实时通信</category>
            <category>Debugging</category>
            <category>Monitoring</category>
            <category>Deep Dive</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (12)：SFU/MCU/Mesh 架构与 Pion 实战]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture</guid>
            <pubDate>Tue, 23 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[Mesh vs MCU vs SFU 架构对比、Forwarder 核心逻辑、Pion/LiveKit 选型与三人会议实战]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"我们早期押注多播，但公网教会了我们：你需要的是 packet shufflers，不是 multicast routers。" — Serge Lachapelle，<a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 历史访谈</a></p>
</blockquote>
<p>本系列从 P2P（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">Ch2</a>）走来，现在进入<strong>多人会议</strong>的架构选型。Marratech 是瑞典最早的 Web 视频会议公司之一，2009 年被 Google 收购——Serge Lachapelle 随之加入 Google，成为 WebRTC 标准化的核心推动者。他在 Curious 访谈中回忆：Marratech 早期押注 IP 多播，后来行业转向 <strong>packet shufflers</strong>——即今天的 SFU（Selective Forwarding Unit）。</p>
<p>配套 Lab：<code>examples/webrtc-lab/client/ch12-sfu-client</code>（LiveKit 客户端）+ <code>examples/webrtc-lab/signaling/</code>。</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>推荐先读</div><div class="admonitionContent_BuS1"><p>本章涉及 LiveKit 作为 SFU 参考实现。建议先阅读本站 <a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍</a>，了解 Room/Participant/Track 模型与 SDK 选型。</p></div></div>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Mesh</strong></td><td style="text-align:left">网状拓扑</td><td style="text-align:left">每个参与者与其他所有人建立 P2P 连接</td></tr><tr><td style="text-align:left"><strong>MCU</strong></td><td style="text-align:left">Multipoint Control Unit</td><td style="text-align:left">多点控制单元，服务端混流/转码后广播</td></tr><tr><td style="text-align:left"><strong>SFU</strong></td><td style="text-align:left">Selective Forwarding Unit</td><td style="text-align:left">选择性转发单元，服务端按订阅转发 RTP 包</td></tr><tr><td style="text-align:left"><strong>Forwarder</strong></td><td style="text-align:left">转发器</td><td style="text-align:left">SFU 核心模块，接收 RTP 并按需转发给订阅者</td></tr><tr><td style="text-align:left"><strong>Room</strong></td><td style="text-align:left">房间</td><td style="text-align:left">逻辑会话容器，包含多个 Participant</td></tr><tr><td style="text-align:left"><strong>Participant</strong></td><td style="text-align:left">参与者</td><td style="text-align:left">Room 内的一个连接实体（人、Agent、录制器）</td></tr><tr><td style="text-align:left"><strong>Track</strong></td><td style="text-align:left">轨道</td><td style="text-align:left">单条媒体流（音频/视频/屏幕共享）</td></tr><tr><td style="text-align:left"><strong>Publication</strong></td><td style="text-align:left">发布</td><td style="text-align:left">Track 的发布状态与元数据</td></tr><tr><td style="text-align:left"><strong>Subscription</strong></td><td style="text-align:left">订阅</td><td style="text-align:left">订阅者与远程 Track 的绑定关系</td></tr><tr><td style="text-align:left"><strong>Pion</strong></td><td style="text-align:left">—</td><td style="text-align:left">Go 语言 WebRTC 库，Curious 作者 Sean-Der 主导</td></tr><tr><td style="text-align:left"><strong>StreamTracker</strong></td><td style="text-align:left">流追踪器</td><td style="text-align:left">SFU 内部跟踪每个 Track 各层可用性的组件</td></tr><tr><td style="text-align:left"><strong>LayerSelector</strong></td><td style="text-align:left">层选择器</td><td style="text-align:left">根据带宽为订阅者选择 Simulcast/SVC 层</td></tr><tr><td style="text-align:left"><strong>Control Plane</strong></td><td style="text-align:left">控制面</td><td style="text-align:left">信令、Room 管理、Token 鉴权</td></tr><tr><td style="text-align:left"><strong>Media Plane</strong></td><td style="text-align:left">媒体面</td><td style="text-align:left">SRTP 媒体流转发，高带宽低延迟</td></tr><tr><td style="text-align:left"><strong>Webhook</strong></td><td style="text-align:left">回调</td><td style="text-align:left">Room 事件通知后端（join/leave/publish）</td></tr><tr><td style="text-align:left"><strong>Node</strong></td><td style="text-align:left">节点</td><td style="text-align:left">分布式 SFU 集群中的单台物理/虚拟服务器</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一三种架构对比">一、三种架构对比<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#%E4%B8%80%E4%B8%89%E7%A7%8D%E6%9E%B6%E6%9E%84%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to 一、三种架构对比" title="Direct link to 一、三种架构对比" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">架构</th><th style="text-align:left">延迟</th><th style="text-align:left">服务端 CPU</th><th style="text-align:left">客户端 CPU</th><th style="text-align:left">客户端带宽</th><th style="text-align:left">规模</th><th style="text-align:left">典型产品</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Mesh</strong></td><td style="text-align:left">最低</td><td style="text-align:left">无</td><td style="text-align:left">O(N²) 解码</td><td style="text-align:left">O(N) 上下行</td><td style="text-align:left">≤4 人</td><td style="text-align:left">1v1 通话</td></tr><tr><td style="text-align:left"><strong>MCU</strong></td><td style="text-align:left">较高（混流）</td><td style="text-align:left">极高（转码）</td><td style="text-align:left">低（1 路）</td><td style="text-align:left">低（1 路）</td><td style="text-align:left">传统会议</td><td style="text-align:left">早期 Zoom</td></tr><tr><td style="text-align:left"><strong>SFU</strong></td><td style="text-align:left">低</td><td style="text-align:left">低（转发）</td><td style="text-align:left">O(N) 解码</td><td style="text-align:left">O(N) 下行</td><td style="text-align:left">10–10k+</td><td style="text-align:left">Meet/Teams/LiveKit</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="11-连接数增长对比">1.1 连接数增长对比<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#11-%E8%BF%9E%E6%8E%A5%E6%95%B0%E5%A2%9E%E9%95%BF%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to 1.1 连接数增长对比" title="Direct link to 1.1 连接数增长对比" translate="no">​</a></h3>
<!-- -->
<p>Mesh 在 N=6 时需要 15 条 P2P 连接，N=10 时需要 45 条——客户端和网络都无法承受。SFU 将连接数降为 O(N)。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="12-何时仍用-mesh">1.2 何时仍用 Mesh？<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#12-%E4%BD%95%E6%97%B6%E4%BB%8D%E7%94%A8-mesh" class="hash-link" aria-label="Direct link to 1.2 何时仍用 Mesh？" title="Direct link to 1.2 何时仍用 Mesh？" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">场景</th><th style="text-align:left">原因</th></tr></thead><tbody><tr><td style="text-align:left">1v1 通话</td><td style="text-align:left">无 SFU 开销，延迟最低</td></tr><tr><td style="text-align:left">2–3 人小群</td><td style="text-align:left">连接数可控</td></tr><tr><td style="text-align:left">端到端加密 E2EE</td><td style="text-align:left">无服务端转发（SFU 需特殊 E2EE 方案）</td></tr><tr><td style="text-align:left">超低延迟游戏</td><td style="text-align:left">避免 SFU 额外一跳</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二历史脉络marratech--meet--livekit">二、历史脉络：Marratech → Meet → LiveKit<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#%E4%BA%8C%E5%8E%86%E5%8F%B2%E8%84%89%E7%BB%9Cmarratech--meet--livekit" class="hash-link" aria-label="Direct link to 二、历史脉络：Marratech → Meet → LiveKit" title="Direct link to 二、历史脉络：Marratech → Meet → LiveKit" translate="no">​</a></h2>
<!-- -->
<p>Serge Lachapelle 在 Curious 访谈中强调：WebRTC 标准化的成功在于<strong>站在巨人肩膀上</strong>——RTP、SRTP、ICE 等协议早已成熟，WebRTC 做的是把它们整合成浏览器 API。SFU 架构同理：不是发明新的媒体协议，而是<strong>在正确的位置做 RTP 包调度</strong>。</p>
<p>Marratech 被收购后，其核心工程师将 packet shuffler 经验带入 Google Meet。Sean-Der（Pion 作者）则在 Curious 一书中系统化了 SFU 的实现细节——LiveKit 正是 Pion 之上构建的生产级 SFU。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三sfu-核心数据模型">三、SFU 核心数据模型<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#%E4%B8%89sfu-%E6%A0%B8%E5%BF%83%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B" class="hash-link" aria-label="Direct link to 三、SFU 核心数据模型" title="Direct link to 三、SFU 核心数据模型" translate="no">​</a></h2>
<!-- -->
<p>LiveKit 的 <strong>Room / Participant / Track</strong> 模型是 SFU 领域的标准抽象：</p>
<table><thead><tr><th style="text-align:left">概念</th><th style="text-align:left">说明</th><th style="text-align:left">示例</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Room</strong></td><td style="text-align:left">会话容器，有唯一 ID</td><td style="text-align:left"><code>meeting-123</code></td></tr><tr><td style="text-align:left"><strong>Participant</strong></td><td style="text-align:left">Room 内的连接实体</td><td style="text-align:left"><code>alice</code>, <code>bob</code></td></tr><tr><td style="text-align:left"><strong>Track</strong></td><td style="text-align:left">单条媒体流</td><td style="text-align:left"><code>TR_abc123</code> (video), <code>TR_def456</code> (audio)</td></tr><tr><td style="text-align:left"><strong>Publication</strong></td><td style="text-align:left">Track 的发布状态</td><td style="text-align:left"><code>muted</code>, <code>simulcast</code>, <code>source</code></td></tr><tr><td style="text-align:left"><strong>Subscription</strong></td><td style="text-align:left">订阅关系</td><td style="text-align:left">P2 订阅 P1 的 video Track</td></tr></tbody></table>
<p>详见 <a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍</a>——该文详细对比了 LiveKit 与 mediasoup/Janus 的选型，以及 SDK 集成路径。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-track-生命周期">3.1 Track 生命周期<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#31-track-%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F" class="hash-link" aria-label="Direct link to 3.1 Track 生命周期" title="Direct link to 3.1 Track 生命周期" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-jwt-access-token-结构">3.2 JWT Access Token 结构<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#32-jwt-access-token-%E7%BB%93%E6%9E%84" class="hash-link" aria-label="Direct link to 3.2 JWT Access Token 结构" title="Direct link to 3.2 JWT Access Token 结构" translate="no">​</a></h3>
<!-- -->
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">AccessToken</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"livekit-server-sdk"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> token </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">AccessToken</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">apiKey</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> apiSecret</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">identity</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"alice"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">token</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addGrant</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">roomJoin</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">room</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"meeting-123"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">canPublish</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">canSubscribe</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">canPublishData</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> jwt </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> token</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">toJwt</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四sfu-forwarder-内部逻辑">四、SFU Forwarder 内部逻辑<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#%E5%9B%9Bsfu-forwarder-%E5%86%85%E9%83%A8%E9%80%BB%E8%BE%91" class="hash-link" aria-label="Direct link to 四、SFU Forwarder 内部逻辑" title="Direct link to 四、SFU Forwarder 内部逻辑" translate="no">​</a></h2>
<!-- -->
<p>Pion（Go）和 LiveKit（Go + Pion）都实现了这套 Forwarder + StreamTracker 管线。关键设计原则：</p>
<ol>
<li class=""><strong>不转码</strong>：只修改 RTP 头，payload 原样转发</li>
<li class=""><strong>per-subscriber 状态</strong>：每个订阅者独立的 SSRC/seq/timestamp 空间</li>
<li class=""><strong>per-subscriber BWE</strong>：每个订阅者独立的 TWCC 和 LayerSelector</li>
<li class=""><strong>Simulcast 层选择</strong>：根据 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Ch11</a> 的 rid/DD 选层</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-rtp-头重写">4.1 RTP 头重写<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#41-rtp-%E5%A4%B4%E9%87%8D%E5%86%99" class="hash-link" aria-label="Direct link to 4.1 RTP 头重写" title="Direct link to 4.1 RTP 头重写" translate="no">​</a></h3>
<!-- -->
<p>SFU 必须为每个订阅者维护独立的 RTP 序列号空间，否则订阅者的 jitter buffer 和 TWCC 会混乱。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-streamtracker-职责">4.2 StreamTracker 职责<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#42-streamtracker-%E8%81%8C%E8%B4%A3" class="hash-link" aria-label="Direct link to 4.2 StreamTracker 职责" title="Direct link to 4.2 StreamTracker 职责" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">检测项</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:left">各 rid 层是否有 Keyframe</td><td style="text-align:left">层切换前提</td></tr><tr><td style="text-align:left">各层最后活跃时间</td><td style="text-align:left">Dynacast 停发依据</td></tr><tr><td style="text-align:left">MID/SSRC 映射变化</td><td style="text-align:left">Re-negotiation 处理</td></tr><tr><td style="text-align:left">DD 空间/时间层可用性</td><td style="text-align:left">SVC 选层输入</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五开源-sfu-选型">五、开源 SFU 选型<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#%E4%BA%94%E5%BC%80%E6%BA%90-sfu-%E9%80%89%E5%9E%8B" class="hash-link" aria-label="Direct link to 五、开源 SFU 选型" title="Direct link to 五、开源 SFU 选型" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">项目</th><th style="text-align:left">语言</th><th style="text-align:left">定位</th><th style="text-align:left">特点</th></tr></thead><tbody><tr><td style="text-align:left"><a href="https://github.com/pion/webrtc" target="_blank" rel="noopener noreferrer" class="">Pion</a></td><td style="text-align:left">Go</td><td style="text-align:left">库</td><td style="text-align:left">WebRTC for the Curious 作者 Sean-Der 主导，最大灵活度</td></tr><tr><td style="text-align:left"><a href="https://mediasoup.org/" target="_blank" rel="noopener noreferrer" class="">mediasoup</a></td><td style="text-align:left">C++/Node</td><td style="text-align:left">框架</td><td style="text-align:left">灵活，需自建信令/Room/鉴权</td></tr><tr><td style="text-align:left"><a href="https://janus.conf.meetecho.com/" target="_blank" rel="noopener noreferrer" class="">Janus</a></td><td style="text-align:left">C</td><td style="text-align:left">框架</td><td style="text-align:left">插件化，SIP 集成强</td></tr><tr><td style="text-align:left"><a href="https://github.com/livekit/livekit" target="_blank" rel="noopener noreferrer" class="">LiveKit</a></td><td style="text-align:left">Go</td><td style="text-align:left">产品</td><td style="text-align:left">完整栈：SFU + SDK + Agents + Egress</td></tr><tr><td style="text-align:left"><a href="https://jitsi.org/jitsi-videobridge/" target="_blank" rel="noopener noreferrer" class="">Jitsi Videobridge</a></td><td style="text-align:left">Java</td><td style="text-align:left">产品</td><td style="text-align:left">Jitsi Meet 后端，成熟但 Java 栈</td></tr></tbody></table>
<p><a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍</a> 从控制面/媒体面分离、SDK 生态、Agents 扩展等维度做了更完整的选型分析——本章聚焦 SFU 媒体面原理，LiveKit 作为贯穿 Ch12–Ch15 的参考实现。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-选型决策树">5.1 选型决策树<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#51-%E9%80%89%E5%9E%8B%E5%86%B3%E7%AD%96%E6%A0%91" class="hash-link" aria-label="Direct link to 5.1 选型决策树" title="Direct link to 5.1 选型决策树" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-mediasoup-vs-livekit-模型对比">5.2 mediasoup vs LiveKit 模型对比<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#52-mediasoup-vs-livekit-%E6%A8%A1%E5%9E%8B%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to 5.2 mediasoup vs LiveKit 模型对比" title="Direct link to 5.2 mediasoup vs LiveKit 模型对比" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">概念</th><th style="text-align:left">mediasoup</th><th style="text-align:left">LiveKit</th></tr></thead><tbody><tr><td style="text-align:left">路由单元</td><td style="text-align:left">Router</td><td style="text-align:left">Room</td></tr><tr><td style="text-align:left">传输通道</td><td style="text-align:left">WebRtcTransport</td><td style="text-align:left">Participant PC</td></tr><tr><td style="text-align:left">媒体流</td><td style="text-align:left">Producer / Consumer</td><td style="text-align:left">Track Publication / Subscription</td></tr><tr><td style="text-align:left">信令</td><td style="text-align:left">自建</td><td style="text-align:left">内置 WebSocket</td></tr><tr><td style="text-align:left">鉴权</td><td style="text-align:left">自建</td><td style="text-align:left">JWT Token</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六pion-最小-sfu-概念代码">六、Pion 最小 SFU 概念代码<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#%E5%85%ADpion-%E6%9C%80%E5%B0%8F-sfu-%E6%A6%82%E5%BF%B5%E4%BB%A3%E7%A0%81" class="hash-link" aria-label="Direct link to 六、Pion 最小 SFU 概念代码" title="Direct link to 六、Pion 最小 SFU 概念代码" translate="no">​</a></h2>
<div class="custom-code-block" data-language="go"><div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 概念示意 — 非完整可运行代码</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 完整示例见 github.com/pion/example-webrtc-applications</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">package</span><span class="token plain"> main</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"github.com/pion/webrtc/v4"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">func</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">main</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    api </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> webrtc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">NewAPI</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    publisherPC</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">_</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> api</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">NewPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">webrtc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Configuration</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    subscribers </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">make</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">map</span><span class="token punctuation" style="color:#393A34">[</span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">]</span><span class="token operator" style="color:#393A34">*</span><span class="token plain">webrtc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">PeerConnection</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    publisherPC</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">OnTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">func</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">track </span><span class="token operator" style="color:#393A34">*</span><span class="token plain">webrtc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">TrackRemote</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> receiver </span><span class="token operator" style="color:#393A34">*</span><span class="token plain">webrtc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">RTPReceiver</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">_</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> subPC </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">range</span><span class="token plain"> subscribers </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            localTrack</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">_</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> webrtc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">NewTrackLocalStaticRTP</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Codec</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">RTPCodecCapability</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">ID</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">StreamID</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            subPC</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">AddTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">localTrack</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">go</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">func</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">t </span><span class="token operator" style="color:#393A34">*</span><span class="token plain">webrtc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">TrackRemote</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> lt </span><span class="token operator" style="color:#393A34">*</span><span class="token plain">webrtc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">TrackLocalStaticRTP</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                buf </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">make</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token builtin">byte</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1500</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                    n</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">_</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> t</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Read</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">buf</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> err </span><span class="token operator" style="color:#393A34">!=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">nil</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                    lt</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Write</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">buf</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">n</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">track</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> localTrack</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>生产 SFU 远不止这些</div><div class="admonitionContent_BuS1"><p>上述代码缺少 Simulcast 层选择、TWCC、SRTP 密钥管理、Room 状态、断线重连等。这就是为什么大多数团队选择 LiveKit 而非从零用 Pion 构建——详见 <a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍</a>。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七实战livekit-三人会议">七、实战：LiveKit 三人会议<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#%E4%B8%83%E5%AE%9E%E6%88%98livekit-%E4%B8%89%E4%BA%BA%E4%BC%9A%E8%AE%AE" class="hash-link" aria-label="Direct link to 七、实战：LiveKit 三人会议" title="Direct link to 七、实战：LiveKit 三人会议" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="71-启动-livekit-server">7.1 启动 LiveKit Server<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#71-%E5%90%AF%E5%8A%A8-livekit-server" class="hash-link" aria-label="Direct link to 7.1 启动 LiveKit Server" title="Direct link to 7.1 启动 LiveKit Server" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">brew </span><span class="token function" style="color:#d73a49">install</span><span class="token plain"> livekit</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">livekit-server </span><span class="token parameter variable" style="color:#36acaa">--dev</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 输出: LiveKit server started on :7880</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="72-生成-access-token">7.2 生成 Access Token<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#72-%E7%94%9F%E6%88%90-access-token" class="hash-link" aria-label="Direct link to 7.2 生成 Access Token" title="Direct link to 7.2 生成 Access Token" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">brew </span><span class="token function" style="color:#d73a49">install</span><span class="token plain"> livekit-cli</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">lk token create </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --api-key devkey </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --api-secret secret </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">--join</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">--room</span><span class="token plain"> meeting </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">--identity</span><span class="token plain"> user1 </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  --valid-for 24h</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="73-客户端连接">7.3 客户端连接<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#73-%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%BF%9E%E6%8E%A5" class="hash-link" aria-label="Direct link to 7.3 客户端连接" title="Direct link to 7.3 客户端连接" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// examples/webrtc-lab/client/ch12-sfu-client</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">Room</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">RoomEvent</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">Track</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"livekit-client"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> room </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Room</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">RoomEvent</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">TrackSubscribed</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">track</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> publication</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> participant</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token maybe-class-name">Track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Kind</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Video</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> element </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">attach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token dom variable" style="color:#36acaa">document</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getElementById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"remote-videos"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">appendChild</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">element</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">RoomEvent</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">ParticipantConnected</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">p</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"[Layer1] joined:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> p</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">identity</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">connect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ws://localhost:7880"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> token</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localParticipant</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">enableCameraAndMicrophone</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Participants:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">numParticipants</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<p>React 组件方式：</p>
<div class="custom-code-block" data-language="jsx"><div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">LiveKitRoom</span><span class="token imports punctuation" style="color:#393A34">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">VideoConference</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"@livekit/components-react"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:#d73a49">Meeting</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#00009f">LiveKitRoom</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">token</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#00009f">token</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">serverUrl</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">ws://localhost:7880</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#00009f">VideoConference</span><span class="token tag" style="color:#00009f"> </span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token plain-text"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag class-name" style="color:#00009f">LiveKitRoom</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="74-验证三人会议">7.4 验证三人会议<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#74-%E9%AA%8C%E8%AF%81%E4%B8%89%E4%BA%BA%E4%BC%9A%E8%AE%AE" class="hash-link" aria-label="Direct link to 7.4 验证三人会议" title="Direct link to 7.4 验证三人会议" translate="no">​</a></h3>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八分布式-sfu-mesh">八、分布式 SFU Mesh<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#%E5%85%AB%E5%88%86%E5%B8%83%E5%BC%8F-sfu-mesh" class="hash-link" aria-label="Direct link to 八、分布式 SFU Mesh" title="Direct link to 八、分布式 SFU Mesh" translate="no">​</a></h2>
<!-- -->
<p>LiveKit 默认启用<strong>分布式 Mesh</strong>——单个 Room 可跨多台物理服务器：</p>
<table><thead><tr><th style="text-align:left">组件</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Redis</strong></td><td style="text-align:left">Room 路由表、Node 注册、Participant 位置</td></tr><tr><td style="text-align:left"><strong>Node Selector</strong></td><td style="text-align:left">新 Participant 分配到最优 Node</td></tr><tr><td style="text-align:left"><strong>Relay</strong></td><td style="text-align:left">跨 Node 的 RTP 转发（当发布者和订阅者在不同 Node）</td></tr></tbody></table>
<p>跨 Node Relay 会增加一跳延迟——生产环境应通过 <strong>GeoDNS + 区域亲和性</strong> 让同一 Room 的参与者尽量在同一 Node（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">Ch14</a> 详述）。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="81-webhook-事件">8.1 Webhook 事件<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#81-webhook-%E4%BA%8B%E4%BB%B6" class="hash-link" aria-label="Direct link to 8.1 Webhook 事件" title="Direct link to 8.1 Webhook 事件" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 后端接收 LiveKit Webhook</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">app</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">post</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/webhook/livekit"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">req</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> res</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> event </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> req</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">body</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">switch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"participant_joined"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">layer</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"signaling"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">identity</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">participant</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">identity</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"track_published"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">layer</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"media"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">track</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">track</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"room_finished"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// 清理资源</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  res</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">sendStatus</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">200</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九sfu-vs-信令的关系">九、SFU vs 信令的关系<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#%E4%B9%9Dsfu-vs-%E4%BF%A1%E4%BB%A4%E7%9A%84%E5%85%B3%E7%B3%BB" class="hash-link" aria-label="Direct link to 九、SFU vs 信令的关系" title="Direct link to 九、SFU vs 信令的关系" translate="no">​</a></h2>
<!-- -->
<p>关键认知：</p>
<ol>
<li class=""><strong>信令不承载媒体</strong>——SDP/ICE 走 WebSocket，SRTP 走 UDP 直连 SFU</li>
<li class=""><strong>LiveKit 内置信令</strong>——<code>room.connect()</code> 同时完成信令 + 媒体协商</li>
<li class=""><strong>自研信令 + SFU</strong>——mediasoup/Pion 场景需自行实现 Room 管理（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">Ch3</a>）</li>
</ol>
<p><code>examples/webrtc-lab/signaling/server.js</code> 实现了最小 P2P 信令；SFU 场景下信令由 LiveKit 接管，但 Join API 仍可在同一 Node 服务中提供 Token。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十常见陷阱">十、常见陷阱<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#%E5%8D%81%E5%B8%B8%E8%A7%81%E9%99%B7%E9%98%B1" class="hash-link" aria-label="Direct link to 十、常见陷阱" title="Direct link to 十、常见陷阱" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:center">#</th><th style="text-align:left">陷阱</th><th style="text-align:left">现象</th><th style="text-align:left">修复</th></tr></thead><tbody><tr><td style="text-align:center">1</td><td style="text-align:left">Mesh 用于大会议</td><td style="text-align:left">客户端 CPU/带宽爆炸</td><td style="text-align:left">N&gt;4 切换 SFU</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left">SFU 做转码</td><td style="text-align:left">延迟高、CPU 爆</td><td style="text-align:left">SFU 只转发，混流用 MCU/Egress</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left">忽略 per-subscriber SSRC</td><td style="text-align:left">订阅者 jitter buffer 乱</td><td style="text-align:left">Forwarder 重写 SSRC/seq</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left">单 Node 部署</td><td style="text-align:left">跨区延迟高</td><td style="text-align:left">分布式 Mesh + GeoDNS</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left">Token 无过期</td><td style="text-align:left">安全风险</td><td style="text-align:left">JWT 短期 Token + 刷新</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left">未启用 Simulcast</td><td style="text-align:left">所有订阅者同质量</td><td style="text-align:left">发布端开启 Simulcast</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left">信令与媒体混淆</td><td style="text-align:left">带宽打满信令服务器</td><td style="text-align:left">媒体必须直连 SFU</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left">跨 Node Relay 未监控</td><td style="text-align:left">延迟飙升</td><td style="text-align:left">Prometheus 监控 relay 延迟</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left">Webhook 未验签</td><td style="text-align:left">伪造 join 事件</td><td style="text-align:left">验证 LiveKit Webhook 签名</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left">Room 无空房间清理</td><td style="text-align:left">内存泄漏</td><td style="text-align:left"><code>emptyTimeout</code> 配置</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十一实战-lab">十一、实战 Lab<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#%E5%8D%81%E4%B8%80%E5%AE%9E%E6%88%98-lab" class="hash-link" aria-label="Direct link to 十一、实战 Lab" title="Direct link to 十一、实战 Lab" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-1livekit-三人会议">Lab 1：LiveKit 三人会议<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#lab-1livekit-%E4%B8%89%E4%BA%BA%E4%BC%9A%E8%AE%AE" class="hash-link" aria-label="Direct link to Lab 1：LiveKit 三人会议" title="Direct link to Lab 1：LiveKit 三人会议" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">livekit-server </span><span class="token parameter variable" style="color:#36acaa">--dev</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">lk token create --api-key devkey --api-secret secret </span><span class="token punctuation" style="color:#393A34">\</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token parameter variable" style="color:#36acaa">--join</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">--room</span><span class="token plain"> lab </span><span class="token parameter variable" style="color:#36acaa">--identity</span><span class="token plain"> user1 --valid-for 1h</span><br></div></code></pre></div></div></div>
<ol>
<li class="">3 个浏览器 Tab 加入 <code>room=lab</code></li>
<li class="">验证互相看到视频</li>
<li class=""><code>chrome://webrtc-internals</code> 确认仅 1 条 PeerConnection 到 SFU</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-2pion-示例运行">Lab 2：Pion 示例运行<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#lab-2pion-%E7%A4%BA%E4%BE%8B%E8%BF%90%E8%A1%8C" class="hash-link" aria-label="Direct link to Lab 2：Pion 示例运行" title="Direct link to Lab 2：Pion 示例运行" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">git</span><span class="token plain"> clone https://github.com/pion/example-webrtc-applications</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">cd</span><span class="token plain"> example-webrtc-applications/sfu-ws</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">go run main.go</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 浏览器打开 http://localhost:8080</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-3simulcast-层观察">Lab 3：Simulcast 层观察<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#lab-3simulcast-%E5%B1%82%E8%A7%82%E5%AF%9F" class="hash-link" aria-label="Direct link to Lab 3：Simulcast 层观察" title="Direct link to Lab 3：Simulcast 层观察" translate="no">​</a></h3>
<ol>
<li class="">LiveKit 三人会议中，一端 DevTools 限速 300kbps</li>
<li class="">在 LiveKit Dashboard 观察该订阅者的下行码率</li>
<li class="">确认 LayerSelector 切换到 <code>rid=l</code></li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-4participant-事件">Lab 4：Participant 事件<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#lab-4participant-%E4%BA%8B%E4%BB%B6" class="hash-link" aria-label="Direct link to Lab 4：Participant 事件" title="Direct link to Lab 4：Participant 事件" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">RoomEvent</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">ParticipantConnected</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">p</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"joined:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> p</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">identity</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">RoomEvent</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">ParticipantDisconnected</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">p</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"left:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> p</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">identity</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">RoomEvent</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">TrackMuted</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pub</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> p</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"muted:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> p</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">identity</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-5对比-mesh-vs-sfu-连接数">Lab 5：对比 Mesh vs SFU 连接数<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#lab-5%E5%AF%B9%E6%AF%94-mesh-vs-sfu-%E8%BF%9E%E6%8E%A5%E6%95%B0" class="hash-link" aria-label="Direct link to Lab 5：对比 Mesh vs SFU 连接数" title="Direct link to Lab 5：对比 Mesh vs SFU 连接数" translate="no">​</a></h3>
<ol>
<li class="">修改 <code>examples/webrtc-lab/client/ch02-p2p-basic</code> 为 3 人 Mesh</li>
<li class=""><code>chrome://webrtc-internals</code> 数 PeerConnection 数量（应为 2）</li>
<li class="">切换 LiveKit SFU（应为 1）</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-6token-权限最小化">Lab 6：Token 权限最小化<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#lab-6token-%E6%9D%83%E9%99%90%E6%9C%80%E5%B0%8F%E5%8C%96" class="hash-link" aria-label="Direct link to Lab 6：Token 权限最小化" title="Direct link to Lab 6：Token 权限最小化" translate="no">​</a></h3>
<ol>
<li class="">生成 <code>canPublish: false</code> 的 Token</li>
<li class="">尝试 <code>enableCameraAndMicrophone()</code> → 应失败</li>
<li class="">验证仅观看模式</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-7webhook-接收">Lab 7：Webhook 接收<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#lab-7webhook-%E6%8E%A5%E6%94%B6" class="hash-link" aria-label="Direct link to Lab 7：Webhook 接收" title="Direct link to Lab 7：Webhook 接收" translate="no">​</a></h3>
<ol>
<li class="">配置 LiveKit <code>webhook.urls</code></li>
<li class="">加入/离开 Room，观察后端日志</li>
</ol>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十二本章小结">十二、本章小结<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#%E5%8D%81%E4%BA%8C%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="Direct link to 十二、本章小结" title="Direct link to 十二、本章小结" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">概念</th><th style="text-align:left">要点</th></tr></thead><tbody><tr><td style="text-align:left">Mesh</td><td style="text-align:left">无服务器，O(N²) 连接，≤4 人</td></tr><tr><td style="text-align:left">MCU</td><td style="text-align:left">混流转码，低客户端开销，高服务端 CPU</td></tr><tr><td style="text-align:left">SFU</td><td style="text-align:left">选择性转发，不转码，10k+ 规模</td></tr><tr><td style="text-align:left">Forwarder</td><td style="text-align:left">RTP 头重写 + LayerSelector + per-subscriber BWE</td></tr><tr><td style="text-align:left">LiveKit</td><td style="text-align:left">Room/Participant/Track 标准模型，推荐入门 SFU</td></tr><tr><td style="text-align:left">选型</td><td style="text-align:left">快速上线 LiveKit，深度定制 Pion/mediasoup</td></tr><tr><td style="text-align:left">历史</td><td style="text-align:left">Marratech → packet shuffler → Meet → LiveKit</td></tr></tbody></table>
<p><strong>下一篇（Ch13）</strong>：<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://webrtcforthecurious.com/docs/09-sfu/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — SFU</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史（Serge Lachapelle / Marratech）</a></li>
<li class=""><a href="https://github.com/pion/webrtc" target="_blank" rel="noopener noreferrer" class="">Pion WebRTC</a></li>
<li class=""><a href="https://github.com/pion/example-webrtc-applications/tree/master/sfu-ws" target="_blank" rel="noopener noreferrer" class="">Pion SFU Example</a></li>
<li class=""><a href="https://docs.livekit.io/home/get-started/intro-to-livekit/" target="_blank" rel="noopener noreferrer" class="">LiveKit Architecture</a></li>
<li class=""><a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍 — 本站推荐</a></li>
<li class=""><a href="https://mediasoup.org/documentation/" target="_blank" rel="noopener noreferrer" class="">mediasoup Documentation</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>SFU</category>
            <category>实时通信</category>
            <category>Deep Dive</category>
            <category>Production</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (11)：Simulcast、SVC 与选择性订阅]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc</guid>
            <pubDate>Mon, 22 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[Simulcast vs SVC、SFU 选择性转发、Dynacast 与大会议带宽权衡]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"从多播幻想到 packet shufflers——SFU 是 WebRTC 多人会议的必然归宿。" — <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious 历史</a></p>
</blockquote>
<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">Ch9 Simulcast 入门</a> 介绍了三档发布。本章深入 <strong>SFU 如何根据订阅者带宽选择转发层</strong>——这是从 P2P 跃迁到多人会议的核心机制。</p>
<p>Marratech 早期押注 IP 多播（Multicast），Serge Lachapelle 在 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 访谈</a> 中回忆：公网多播从未真正落地，行业最终转向 <strong>packet shufflers（SFU）</strong>——Simulcast + 选择性订阅是 SFU 的标配能力。Google 2010 年收购 Marratech 后，这套架构在 Meet 中大规模验证。</p>
<p>配套 Lab：<code>examples/webrtc-lab/client/ch02-p2p-basic</code> 扩展 Simulcast + <code>client/ch12-sfu-client</code>（LiveKit）。</p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Simulcast</strong></td><td style="text-align:left">联播</td><td style="text-align:left">同一视频源<strong>独立编码</strong>多个分辨率/码率层，同时发送</td></tr><tr><td style="text-align:left"><strong>SVC</strong></td><td style="text-align:left">Scalable Video Coding</td><td style="text-align:left">可伸缩视频编码，<strong>单流</strong>内含多层，层间有依赖关系</td></tr><tr><td style="text-align:left"><strong>RID</strong></td><td style="text-align:left">RTP Stream Identifier</td><td style="text-align:left">Simulcast 层的标识符（<code>h</code> / <code>m</code> / <code>l</code>）</td></tr><tr><td style="text-align:left"><strong>SSRC</strong></td><td style="text-align:left">Synchronization Source</td><td style="text-align:left">RTP 同步源标识，Simulcast 每层独立 SSRC</td></tr><tr><td style="text-align:left"><strong>MID</strong></td><td style="text-align:left">Media Identification</td><td style="text-align:left">Unified Plan 下 m-line 与 transceiver 的绑定标识</td></tr><tr><td style="text-align:left"><strong>LayerSelector</strong></td><td style="text-align:left">层选择器</td><td style="text-align:left">SFU 根据订阅者带宽选择转发的 Simulcast/SVC 层</td></tr><tr><td style="text-align:left"><strong>Dynacast</strong></td><td style="text-align:left">动态联播</td><td style="text-align:left">LiveKit 按需发布——无订阅者时不发送高层</td></tr><tr><td style="text-align:left"><strong>Dependency Descriptor</strong></td><td style="text-align:left">依赖描述符</td><td style="text-align:left">AV1/VP9 SVC 的层依赖关系 RTP 头扩展</td></tr><tr><td style="text-align:left"><strong>Temporal Layer</strong></td><td style="text-align:left">时间层</td><td style="text-align:left">SVC 中按帧率分层的子流（T0/T1/T2）</td></tr><tr><td style="text-align:left"><strong>Spatial Layer</strong></td><td style="text-align:left">空间层</td><td style="text-align:left">SVC 中按分辨率分层的子流（L0/L1/L2）</td></tr><tr><td style="text-align:left"><strong>Selective Subscription</strong></td><td style="text-align:left">选择性订阅</td><td style="text-align:left">订阅者或 SFU 按需求选择接收的层/Track</td></tr><tr><td style="text-align:left"><strong>Adaptive Stream</strong></td><td style="text-align:left">自适应流</td><td style="text-align:left">LiveKit 客户端自动调整订阅分辨率</td></tr><tr><td style="text-align:left"><strong>Content Hint</strong></td><td style="text-align:left">内容提示</td><td style="text-align:left"><code>motion</code> / <code>detail</code> / <code>text</code> 影响编码策略</td></tr><tr><td style="text-align:left"><strong>Pause Video</strong></td><td style="text-align:left">暂停视频</td><td style="text-align:left">带宽不足时 SFU 停止转发视频但保持音频</td></tr><tr><td style="text-align:left"><strong>Active Speaker</strong></td><td style="text-align:left">活跃说话者</td><td style="text-align:left">大会议中优先订阅当前发言人的高清层</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一为什么需要多层视频">一、为什么需要多层视频？<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#%E4%B8%80%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81%E5%A4%9A%E5%B1%82%E8%A7%86%E9%A2%91" class="hash-link" aria-label="Direct link to 一、为什么需要多层视频？" title="Direct link to 一、为什么需要多层视频？" translate="no">​</a></h2>
<!-- -->
<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">Ch10 GCC</a> 可以在单流内降码率，但<strong>无法在不转码的情况下降分辨率</strong>。Simulcast/SVC 让 SFU 在<strong>不做转码</strong>的前提下，为不同带宽的订阅者转发不同质量的层——这是 SFU 的核心价值。</p>
<p>Serge Lachapelle 在 Curious 访谈中解释：Meet 早期尝试过 MCU 混流，但转码延迟和 CPU 成本不可接受；<strong>packet shuffler + Simulcast</strong> 才是可扩展路径。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二simulcast-vs-svc-架构对比">二、Simulcast vs SVC 架构对比<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#%E4%BA%8Csimulcast-vs-svc-%E6%9E%B6%E6%9E%84%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to 二、Simulcast vs SVC 架构对比" title="Direct link to 二、Simulcast vs SVC 架构对比" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">特性</th><th style="text-align:left">Simulcast</th><th style="text-align:left">SVC</th></tr></thead><tbody><tr><td style="text-align:left">编码次数</td><td style="text-align:left">N 次独立编码</td><td style="text-align:left">1 次编码，多层输出</td></tr><tr><td style="text-align:left">上行带宽</td><td style="text-align:left">高（各层码率之和）</td><td style="text-align:left">低（仅最高层码率）</td></tr><tr><td style="text-align:left">下行灵活性</td><td style="text-align:left">SFU 选 SSRC/rid 转发</td><td style="text-align:left">SFU 解析 Dependency Descriptor 选层</td></tr><tr><td style="text-align:left">SFU 复杂度</td><td style="text-align:left">低（按 SSRC 切换）</td><td style="text-align:left">中（需理解层依赖）</td></tr><tr><td style="text-align:left">编解码器</td><td style="text-align:left">VP8/H.264/VP9/AV1 均可</td><td style="text-align:left">VP9 / AV1 为主</td></tr><tr><td style="text-align:left">CPU 消耗</td><td style="text-align:left">高（多编码器）</td><td style="text-align:left">低（单编码器）</td></tr><tr><td style="text-align:left">典型框架</td><td style="text-align:left">mediasoup / Janus</td><td style="text-align:left">LiveKit Dynacast</td></tr></tbody></table>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-选型决策树">2.1 选型决策树<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#21-%E9%80%89%E5%9E%8B%E5%86%B3%E7%AD%96%E6%A0%91" class="hash-link" aria-label="Direct link to 2.1 选型决策树" title="Direct link to 2.1 选型决策树" translate="no">​</a></h3>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三simulcast-的-sdp-与-rtp-细节">三、Simulcast 的 SDP 与 RTP 细节<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#%E4%B8%89simulcast-%E7%9A%84-sdp-%E4%B8%8E-rtp-%E7%BB%86%E8%8A%82" class="hash-link" aria-label="Direct link to 三、Simulcast 的 SDP 与 RTP 细节" title="Direct link to 三、Simulcast 的 SDP 与 RTP 细节" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-unified-plan--rid">3.1 Unified Plan + RID<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#31-unified-plan--rid" class="hash-link" aria-label="Direct link to 3.1 Unified Plan + RID" title="Direct link to 3.1 Unified Plan + RID" translate="no">​</a></h3>
<!-- -->
<p>SDP 关键行：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=simulcast:send h;m;l</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rid:h send</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rid:m send</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rid:l send</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:left"><code>a=simulcast:send h;m;l</code></td><td style="text-align:left">声明发送三个 Simulcast 层</td></tr><tr><td style="text-align:left"><code>a=rid:h send</code></td><td style="text-align:left">定义 rid 标识与方向</td></tr><tr><td style="text-align:left"><code>mid:0</code></td><td style="text-align:left">绑定到 Unified Plan 的 transceiver</td></tr><tr><td style="text-align:left"><code>rtp-stream-id</code></td><td style="text-align:left">RTP 头扩展携带 rid</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-发布端代码">3.2 发布端代码<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#32-%E5%8F%91%E5%B8%83%E7%AB%AF%E4%BB%A3%E7%A0%81" class="hash-link" aria-label="Direct link to 3.2 发布端代码" title="Direct link to 3.2 发布端代码" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// examples/webrtc-lab/client/ch02-p2p-basic 扩展</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> transceiver </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTransceiver</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">direction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"sendonly"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">sendEncodings</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"h"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1_500_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">scaleResolutionDownBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxFramerate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">30</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"m"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">500_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">scaleResolutionDownBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxFramerate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">24</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"l"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">150_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">scaleResolutionDownBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxFramerate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">15</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 验证 Simulcast 是否生效</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sender </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> transceiver</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sender</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> params </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Encodings:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">encodings</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">rid</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 期望: ["h", "m", "l"]</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>Simulcast 必须配合 Unified Plan</div><div class="admonitionContent_BuS1"><p><code>plan-b</code> 已废弃。确保 <code>RTCPeerConnection</code> 使用默认 Unified Plan，且 <code>addTransceiver</code> 而非 <code>addStream</code>。</p></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="33-屏幕共享-vs-摄像头-simulcast">3.3 屏幕共享 vs 摄像头 Simulcast<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#33-%E5%B1%8F%E5%B9%95%E5%85%B1%E4%BA%AB-vs-%E6%91%84%E5%83%8F%E5%A4%B4-simulcast" class="hash-link" aria-label="Direct link to 3.3 屏幕共享 vs 摄像头 Simulcast" title="Direct link to 3.3 屏幕共享 vs 摄像头 Simulcast" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 屏幕共享：更高码率，较少层</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> screenTransceiver </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTransceiver</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">screenTrack</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">direction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"sendonly"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">sendEncodings</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"h"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3_000_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxFramerate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">15</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"l"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">500_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxFramerate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">screenTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">contentHint</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"detail"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 保文字清晰</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四svc-层结构与-dependency-descriptor">四、SVC 层结构与 Dependency Descriptor<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#%E5%9B%9Bsvc-%E5%B1%82%E7%BB%93%E6%9E%84%E4%B8%8E-dependency-descriptor" class="hash-link" aria-label="Direct link to 四、SVC 层结构与 Dependency Descriptor" title="Direct link to 四、SVC 层结构与 Dependency Descriptor" translate="no">​</a></h2>
<!-- -->
<p>SVC 的关键优势：<strong>丢弃高层不影响低层解码</strong>。SFU 可以在 RTP 级别丢弃 L2/T2 包，订阅者仍能解码 L0/T0。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-vp9-svc-模式">4.1 VP9 SVC 模式<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#41-vp9-svc-%E6%A8%A1%E5%BC%8F" class="hash-link" aria-label="Direct link to 4.1 VP9 SVC 模式" title="Direct link to 4.1 VP9 SVC 模式" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">模式</th><th style="text-align:left">空间层</th><th style="text-align:left">时间层</th><th style="text-align:left">典型用途</th></tr></thead><tbody><tr><td style="text-align:left">L1T1</td><td style="text-align:left">1</td><td style="text-align:left">1</td><td style="text-align:left">最低开销</td></tr><tr><td style="text-align:left">L1T3</td><td style="text-align:left">1</td><td style="text-align:left">3</td><td style="text-align:left">帧率自适应</td></tr><tr><td style="text-align:left">L3T3</td><td style="text-align:left">3</td><td style="text-align:left">3</td><td style="text-align:left">全自适应（LiveKit 默认）</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-av1-dependency-descriptor">4.2 AV1 Dependency Descriptor<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#42-av1-dependency-descriptor" class="hash-link" aria-label="Direct link to 4.2 AV1 Dependency Descriptor" title="Direct link to 4.2 AV1 Dependency Descriptor" translate="no">​</a></h3>
<p>AV1 的 Dependency Descriptor（DD）头扩展让 SFU 无需解码即可知道每个 RTP 包属于哪一层：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=extmap:11 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension</span><br></div></code></pre></div></div></div>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五sfu-选择性转发流程">五、SFU 选择性转发流程<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#%E4%BA%94sfu-%E9%80%89%E6%8B%A9%E6%80%A7%E8%BD%AC%E5%8F%91%E6%B5%81%E7%A8%8B" class="hash-link" aria-label="Direct link to 五、SFU 选择性转发流程" title="Direct link to 五、SFU 选择性转发流程" translate="no">​</a></h2>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-sfu-forwarder-内部逻辑">5.1 SFU Forwarder 内部逻辑<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#51-sfu-forwarder-%E5%86%85%E9%83%A8%E9%80%BB%E8%BE%91" class="hash-link" aria-label="Direct link to 5.1 SFU Forwarder 内部逻辑" title="Direct link to 5.1 SFU Forwarder 内部逻辑" translate="no">​</a></h3>
<!-- -->
<p>Pion（Go）和 LiveKit（Go + Pion）都实现了 Forwarder + StreamTracker 管线。LiveKit 的 LayerSelector 会综合考虑：</p>
<ol>
<li class="">订阅者的 TWCC 带宽估计（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">Ch10</a>）</li>
<li class="">订阅者请求的视频质量（<code>HIGH</code> / <code>MEDIUM</code> / <code>LOW</code>）</li>
<li class="">发布者当前可用的 Simulcast 层</li>
<li class="">CPU/带宽保护阈值</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-层切换时序">5.2 层切换时序<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#52-%E5%B1%82%E5%88%87%E6%8D%A2%E6%97%B6%E5%BA%8F" class="hash-link" aria-label="Direct link to 5.2 层切换时序" title="Direct link to 5.2 层切换时序" translate="no">​</a></h3>
<!-- -->
<p>层切换不是瞬时的——SFU 需要等待新层 Keyframe（PLI/FIR 触发）。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六dynacast按需发布">六、Dynacast：按需发布<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#%E5%85%ADdynacast%E6%8C%89%E9%9C%80%E5%8F%91%E5%B8%83" class="hash-link" aria-label="Direct link to 六、Dynacast：按需发布" title="Direct link to 六、Dynacast：按需发布" translate="no">​</a></h2>
<p>LiveKit 的 <strong>Dynacast</strong> 进一步优化<strong>上行</strong>带宽：若无人订阅高层，停止发送该层：</p>
<!-- -->
<!-- -->
<p>Dynacast 对大型会议（100+ 参与者，大部分静音/小窗）节省的上行带宽非常可观。详见 <a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍</a>。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="61-dynacast-与-active-speaker">6.1 Dynacast 与 Active Speaker<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#61-dynacast-%E4%B8%8E-active-speaker" class="hash-link" aria-label="Direct link to 6.1 Dynacast 与 Active Speaker" title="Direct link to 6.1 Dynacast 与 Active Speaker" translate="no">​</a></h3>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七大会议带宽模型">七、大会议带宽模型<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#%E4%B8%83%E5%A4%A7%E4%BC%9A%E8%AE%AE%E5%B8%A6%E5%AE%BD%E6%A8%A1%E5%9E%8B" class="hash-link" aria-label="Direct link to 七、大会议带宽模型" title="Direct link to 七、大会议带宽模型" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">架构</th><th style="text-align:left">6 人会议每人连接数</th><th style="text-align:left">6 人每人上行</th><th style="text-align:left">6 人每人下行</th></tr></thead><tbody><tr><td style="text-align:left">Full Mesh</td><td style="text-align:left">5 条 P2P</td><td style="text-align:left">5 × 码率</td><td style="text-align:left">5 × 码率</td></tr><tr><td style="text-align:left">SFU</td><td style="text-align:left">1 条到 SFU</td><td style="text-align:left">1 × Simulcast 码率</td><td style="text-align:left">(N-1) × 单流码率</td></tr><tr><td style="text-align:left">MCU</td><td style="text-align:left">1 条到 MCU</td><td style="text-align:left">1 × 码率</td><td style="text-align:left">1 × 混流码率</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="71-带宽估算公式">7.1 带宽估算公式<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#71-%E5%B8%A6%E5%AE%BD%E4%BC%B0%E7%AE%97%E5%85%AC%E5%BC%8F" class="hash-link" aria-label="Direct link to 7.1 带宽估算公式" title="Direct link to 7.1 带宽估算公式" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Simulcast 上行 = rid_h + rid_m + rid_l（无 Dynacast）</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">              ≈ rid_active_layers（有 Dynacast）</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">SFU 下行/订阅者 = Σ(每个远程 participant 的选中层码率)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">SFU 总转发量   = Σ(每个订阅者的下行之和)</span><br></div></code></pre></div></div></div>
<p>示例：10 人会议，3 档 Simulcast（1.5M + 0.5M + 0.15M = 2.15M 上行/人），SFU 为每个订阅者转发 9 路 × 选中层（假设平均 500kbps）= 4.5Mbps/订阅者。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="72-100-人会议优化策略">7.2 100 人会议优化策略<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#72-100-%E4%BA%BA%E4%BC%9A%E8%AE%AE%E4%BC%98%E5%8C%96%E7%AD%96%E7%95%A5" class="hash-link" aria-label="Direct link to 7.2 100 人会议优化策略" title="Direct link to 7.2 100 人会议优化策略" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">策略</th><th style="text-align:left">节省</th><th style="text-align:left">实现</th></tr></thead><tbody><tr><td style="text-align:left">Active Speaker 高清</td><td style="text-align:left">下行 80%+</td><td style="text-align:left">仅 1 人 rid=h</td></tr><tr><td style="text-align:left">视频 Pause</td><td style="text-align:left">下行 50%+</td><td style="text-align:left">非可见 tile 不订阅</td></tr><tr><td style="text-align:left">Dynacast</td><td style="text-align:left">上行 60%+</td><td style="text-align:left">无订阅者停发高层</td></tr><tr><td style="text-align:left">音频 always-on</td><td style="text-align:left">—</td><td style="text-align:left">音频独立 Track，~64kbps/人</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八代码simulcast-发布与订阅控制">八、代码：Simulcast 发布与订阅控制<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#%E5%85%AB%E4%BB%A3%E7%A0%81simulcast-%E5%8F%91%E5%B8%83%E4%B8%8E%E8%AE%A2%E9%98%85%E6%8E%A7%E5%88%B6" class="hash-link" aria-label="Direct link to 八、代码：Simulcast 发布与订阅控制" title="Direct link to 八、代码：Simulcast 发布与订阅控制" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="81-发布端完整示例">8.1 发布端完整示例<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#81-%E5%8F%91%E5%B8%83%E7%AB%AF%E5%AE%8C%E6%95%B4%E7%A4%BA%E4%BE%8B" class="hash-link" aria-label="Direct link to 8.1 发布端完整示例" title="Direct link to 8.1 发布端完整示例" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">publishWithSimulcast</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">room</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> localTrack</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localParticipant</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">publishTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">localTrack</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">simulcast</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">videoEncoding</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1_500_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">maxFramerate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">30</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">videoSimulcastLayers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"h"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">scaleResolutionDownBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1_500_000</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"m"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">scaleResolutionDownBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">500_000</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"l"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">scaleResolutionDownBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">150_000</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<p>LiveKit SDK 封装了 <code>room.setVideoQuality(participant, quality)</code> 等高层 API，见 <a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍</a>。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="82-订阅质量控制">8.2 订阅质量控制<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#82-%E8%AE%A2%E9%98%85%E8%B4%A8%E9%87%8F%E6%8E%A7%E5%88%B6" class="hash-link" aria-label="Direct link to 8.2 订阅质量控制" title="Direct link to 8.2 订阅质量控制" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">VideoQuality</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"livekit-client"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 为特定参与者设置订阅质量</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setVideoQuality</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">remoteParticipant</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token maybe-class-name">VideoQuality</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">HIGH</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setVideoQuality</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">otherParticipant</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token maybe-class-name">VideoQuality</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">LOW</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 暂停某参与者视频（保留音频）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setVideoSubscription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">remoteParticipant</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="83-原生-webrtc-订阅层控制">8.3 原生 WebRTC 订阅层控制<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#83-%E5%8E%9F%E7%94%9F-webrtc-%E8%AE%A2%E9%98%85%E5%B1%82%E6%8E%A7%E5%88%B6" class="hash-link" aria-label="Direct link to 8.3 原生 WebRTC 订阅层控制" title="Direct link to 8.3 原生 WebRTC 订阅层控制" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> receiver </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getReceivers</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">track</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">kind </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> params </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> receiver</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">degradationPreference</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"maintain-framerate"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> receiver</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="84-getstats-验证-simulcast-层">8.4 getStats 验证 Simulcast 层<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#84-getstats-%E9%AA%8C%E8%AF%81-simulcast-%E5%B1%82" class="hash-link" aria-label="Direct link to 8.4 getStats 验证 Simulcast 层" title="Direct link to 8.4 getStats 验证 Simulcast 层" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">logSimulcastStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"outbound-rtp"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">rid</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">ssrc</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ssrc</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">framesEncoded</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">framesEncoded</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">targetBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">targetBitrate</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">frameWidth</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">frameWidth</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">frameHeight</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">frameHeight</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 期望看到 3 个 outbound-rtp，rid 分别为 h/m/l</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九marratech--google-meet-的架构启示">九、Marratech → Google Meet 的架构启示<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#%E4%B9%9Dmarratech--google-meet-%E7%9A%84%E6%9E%B6%E6%9E%84%E5%90%AF%E7%A4%BA" class="hash-link" aria-label="Direct link to 九、Marratech → Google Meet 的架构启示" title="Direct link to 九、Marratech → Google Meet 的架构启示" translate="no">​</a></h2>
<!-- -->
<p>Serge Lachapelle 在 Curious 访谈中的核心观点：<strong>SFU 不是对 P2P 的妥协，而是公网多人会议的唯一可行架构</strong>。Simulcast 解决了 SFU「不转码就无法适配不同带宽」的问题；SVC 和 Dynacast 则是 Simulcast 上行带宽代价的后续优化。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十常见陷阱">十、常见陷阱<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#%E5%8D%81%E5%B8%B8%E8%A7%81%E9%99%B7%E9%98%B1" class="hash-link" aria-label="Direct link to 十、常见陷阱" title="Direct link to 十、常见陷阱" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:center">#</th><th style="text-align:left">陷阱</th><th style="text-align:left">现象</th><th style="text-align:left">修复</th></tr></thead><tbody><tr><td style="text-align:center">1</td><td style="text-align:left">Simulcast 未在 SDP 生效</td><td style="text-align:left">仅 1 个 SSRC</td><td style="text-align:left">检查 Unified Plan + <code>sendEncodings</code></td></tr><tr><td style="text-align:center">2</td><td style="text-align:left">rid 命名不规范</td><td style="text-align:left">SFU 无法选层</td><td style="text-align:left">使用 <code>h/m/l</code> 或 <code>f/h/q</code> 约定</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left">三层码率之和超上行</td><td style="text-align:left">全部层丢包</td><td style="text-align:left">启用 Dynacast 或降低 maxBitrate</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left">SVC 层依赖解析错误</td><td style="text-align:left">订阅者花屏</td><td style="text-align:left">检查 DD 头扩展协商</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left">忽略音频独立转发</td><td style="text-align:left">视频降层但音频占带宽</td><td style="text-align:left">音频始终独立 Track</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left">SFU 转码误用</td><td style="text-align:left">延迟高、CPU 爆</td><td style="text-align:left">SFU 应只转发不转码</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left">大会议全量订阅</td><td style="text-align:left">下行带宽爆炸</td><td style="text-align:left">仅订阅 active speaker + 可见 tile</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left">层切换无 Keyframe</td><td style="text-align:left">切换后黑屏 2s</td><td style="text-align:left">SFU 发 PLI 请求 I 帧</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left">H.264 Simulcast profile 不一致</td><td style="text-align:left">解码失败</td><td style="text-align:left">统一 <code>profile-level-id</code></td></tr><tr><td style="text-align:center">10</td><td style="text-align:left">屏幕共享用摄像头三档</td><td style="text-align:left">文字模糊/带宽浪费</td><td style="text-align:left">屏幕共享单独 2 档配置</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十一实战-lab">十一、实战 Lab<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#%E5%8D%81%E4%B8%80%E5%AE%9E%E6%88%98-lab" class="hash-link" aria-label="Direct link to 十一、实战 Lab" title="Direct link to 十一、实战 Lab" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-1验证-simulcast-三档">Lab 1：验证 Simulcast 三档<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#lab-1%E9%AA%8C%E8%AF%81-simulcast-%E4%B8%89%E6%A1%A3" class="hash-link" aria-label="Direct link to Lab 1：验证 Simulcast 三档" title="Direct link to Lab 1：验证 Simulcast 三档" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token builtin class-name">cd</span><span class="token plain"> examples/webrtc-lab/signaling </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">npm</span><span class="token plain"> start</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">npx serve examples/webrtc-lab/client/ch02-p2p-basic</span><br></div></code></pre></div></div></div>
<ol>
<li class="">修改 <code>main.js</code> 添加 <code>sendEncodings</code> 三档</li>
<li class="">通话后运行 <code>logSimulcastStats(pc)</code></li>
<li class="">确认 3 个 <code>outbound-rtp</code> 报告</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-2带宽限制与层切换">Lab 2：带宽限制与层切换<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#lab-2%E5%B8%A6%E5%AE%BD%E9%99%90%E5%88%B6%E4%B8%8E%E5%B1%82%E5%88%87%E6%8D%A2" class="hash-link" aria-label="Direct link to Lab 2：带宽限制与层切换" title="Direct link to Lab 2：带宽限制与层切换" translate="no">​</a></h3>
<ol>
<li class="">Chrome DevTools 限速 300kbps</li>
<li class="">观察 <code>rid=l</code> 的 <code>bytesSent</code> 持续增长，<code>rid=h/m</code> 停滞</li>
<li class="">取消限速，观察 <code>rid=h</code> 恢复</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-3livekit-sfu-选择性订阅">Lab 3：LiveKit SFU 选择性订阅<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#lab-3livekit-sfu-%E9%80%89%E6%8B%A9%E6%80%A7%E8%AE%A2%E9%98%85" class="hash-link" aria-label="Direct link to Lab 3：LiveKit SFU 选择性订阅" title="Direct link to Lab 3：LiveKit SFU 选择性订阅" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">livekit-server </span><span class="token parameter variable" style="color:#36acaa">--dev</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 使用 examples/webrtc-lab/client/ch12-sfu-client</span><br></div></code></pre></div></div></div>
<ol>
<li class="">3 人加入同一 Room</li>
<li class="">订阅者 A 设置 <code>VideoQuality.HIGH</code>，订阅者 B 设置 <code>VideoQuality.LOW</code></li>
<li class="">在 LiveKit Dashboard 观察不同下行码率</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-4simulcast-vs-svc-上行对比">Lab 4：Simulcast vs SVC 上行对比<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#lab-4simulcast-vs-svc-%E4%B8%8A%E8%A1%8C%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to Lab 4：Simulcast vs SVC 上行对比" title="Direct link to Lab 4：Simulcast vs SVC 上行对比" translate="no">​</a></h3>
<ol>
<li class="">发布者 A：Simulcast 三档 VP8</li>
<li class="">发布者 B：SVC VP9（LiveKit 默认）</li>
<li class="">对比 <code>getStats</code> 中总 <code>bytesSent</code> 速率</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-5dynacast-验证">Lab 5：Dynacast 验证<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#lab-5dynacast-%E9%AA%8C%E8%AF%81" class="hash-link" aria-label="Direct link to Lab 5：Dynacast 验证" title="Direct link to Lab 5：Dynacast 验证" translate="no">​</a></h3>
<ol>
<li class="">发布者加入 Room 但不订阅任何远程 Track</li>
<li class="">观察 LiveKit 是否停止发送 h/m 层（通过发布者 <code>getStats</code>）</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-6层切换延迟测量">Lab 6：层切换延迟测量<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#lab-6%E5%B1%82%E5%88%87%E6%8D%A2%E5%BB%B6%E8%BF%9F%E6%B5%8B%E9%87%8F" class="hash-link" aria-label="Direct link to Lab 6：层切换延迟测量" title="Direct link to Lab 6：层切换延迟测量" translate="no">​</a></h3>
<ol>
<li class="">通话中 DevTools 从 Unlimited 切到 300kbps</li>
<li class="">记录 <code>rid</code> 变化时间戳</li>
<li class="">对比 SFU 层切换 vs 单流 GCC 降分辨率的速度</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-7屏幕共享-simulcast">Lab 7：屏幕共享 Simulcast<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#lab-7%E5%B1%8F%E5%B9%95%E5%85%B1%E4%BA%AB-simulcast" class="hash-link" aria-label="Direct link to Lab 7：屏幕共享 Simulcast" title="Direct link to Lab 7：屏幕共享 Simulcast" translate="no">​</a></h3>
<ol>
<li class="">发布屏幕共享 + <code>contentHint: "detail"</code></li>
<li class="">订阅者限速 500kbps</li>
<li class="">确认文字仍可读（l 层保分辨率）</li>
</ol>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十二本章小结">十二、本章小结<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#%E5%8D%81%E4%BA%8C%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="Direct link to 十二、本章小结" title="Direct link to 十二、本章小结" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">概念</th><th style="text-align:left">要点</th></tr></thead><tbody><tr><td style="text-align:left">Simulcast</td><td style="text-align:left">独立编码多层，SFU 按 rid 转发，上行代价高</td></tr><tr><td style="text-align:left">SVC</td><td style="text-align:left">单流多层，上行高效，SFU 需解析层依赖</td></tr><tr><td style="text-align:left">Dynacast</td><td style="text-align:left">按需发布，节省上行</td></tr><tr><td style="text-align:left">SFU 选层</td><td style="text-align:left">TWCC BWE + 订阅偏好 → LayerSelector</td></tr><tr><td style="text-align:left">大会议</td><td style="text-align:left">Active Speaker + Pause + Dynacast 三件套</td></tr><tr><td style="text-align:left">历史</td><td style="text-align:left">Marratech 多播 → packet shuffler → Meet/LiveKit</td></tr></tbody></table>
<p><strong>下一篇（Ch12）</strong>：<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU 架构与 Pion 实战</a></p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://webrtcforthecurious.com/docs/08-simulcast/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — Simulcast</a></li>
<li class=""><a href="https://webrtcforthecurious.com/docs/09-sfu/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — SFU</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史（Marratech / Serge Lachapelle）</a></li>
<li class=""><a href="https://docs.livekit.io/home/client/tracks/publish/#video-simulcast" target="_blank" rel="noopener noreferrer" class="">LiveKit Docs — Simulcast / SVC / Dynacast</a></li>
<li class=""><a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍 — 本站推荐</a></li>
<li class=""><a href="https://webrtchacks.com/simulcast/" target="_blank" rel="noopener noreferrer" class="">webrtcH4cKS — Simulcast</a></li>
<li class=""><a href="https://aomediacodec.github.io/av1-rtp-spec/" target="_blank" rel="noopener noreferrer" class="">AV1 RTP Specification — Dependency Descriptor</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>SFU</category>
            <category>实时通信</category>
            <category>Deep Dive</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (10)：带宽估计与拥塞控制（GCC）]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation</guid>
            <pubDate>Sun, 21 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[Google Congestion Control 算法、TWCC vs REMB、发送端/接收端带宽估计与码率自适应]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"拥塞控制不是可选项——它是实时通信能否在公网上存活的核心。" — <a href="https://webrtchacks.com/bandwidth-estimation-in-webrtc/" target="_blank" rel="noopener noreferrer" class="">webrtcH4cKS</a></p>
</blockquote>
<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">Ch8 RTCP</a> 介绍了 TWCC、REMB 等反馈机制。本章深入 <strong>Google Congestion Control（GCC）</strong>——WebRTC 发送端如何根据网络状况动态调整码率。GCC 由 Google 在 2010 年代为 Hangouts / Meet 打磨，Serge Lachapelle 在 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 历史访谈</a> 中回忆：Marratech 时代网络质量波动极大，<strong>自适应码率</strong>是从「能通」到「能用」的分水岭。</p>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>非标准但工业界通用</div><div class="admonitionContent_BuS1"><p>GCC 的 IETF 草案（<a href="https://datatracker.ietf.org/doc/html/draft-ietf-rmcat-gcc" target="_blank" rel="noopener noreferrer" class="">draft-ietf-rmcat-gcc</a>）从未最终发布为 RFC。BWE 领域存在多种实现（GCC、NADA、SCReAM），但 <strong>GCC + TWCC</strong> 是 Chrome/WebRTC 生态的事实标准。</p></div></div>
<p>配套 Lab：基于 <code>examples/webrtc-lab/client/ch02-p2p-basic</code> 扩展带宽监控脚本；信令服务见 <code>examples/webrtc-lab/signaling/server.js</code>。</p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>BWE</strong></td><td style="text-align:left">Bandwidth Estimation</td><td style="text-align:left">带宽估计——推断当前网络能承载多少发送码率</td></tr><tr><td style="text-align:left"><strong>GCC</strong></td><td style="text-align:left">Google Congestion Control</td><td style="text-align:left">Google 提出的发送端拥塞控制算法，Delay + Loss 双模块</td></tr><tr><td style="text-align:left"><strong>TWCC</strong></td><td style="text-align:left">Transport-wide Congestion Control</td><td style="text-align:left">逐包传输层拥塞反馈，RTCP <code>transport-cc</code></td></tr><tr><td style="text-align:left"><strong>REMB</strong></td><td style="text-align:left">Receiver Estimated Maximum Bitrate</td><td style="text-align:left">接收端直接告知最大码率（Legacy，<code>goog-remb</code>）</td></tr><tr><td style="text-align:left"><strong>TMMBR</strong></td><td style="text-align:left">Temporary Maximum Media Stream Bit Rate Request</td><td style="text-align:left">接收端请求降低某 SSRC 码率（Legacy）</td></tr><tr><td style="text-align:left"><strong>Delay-based BWE</strong></td><td style="text-align:left">—</td><td style="text-align:left">通过包到达间隔变化检测队列积压</td></tr><tr><td style="text-align:left"><strong>Loss-based BWE</strong></td><td style="text-align:left">—</td><td style="text-align:left">通过丢包率检测拥塞</td></tr><tr><td style="text-align:left"><strong>AIMD</strong></td><td style="text-align:left">Additive Increase Multiplicative Decrease</td><td style="text-align:left">加性增、乘性减——拥塞控制经典策略</td></tr><tr><td style="text-align:left"><strong>Overuse Detector</strong></td><td style="text-align:left">—</td><td style="text-align:left">GCC 中检测网络过载的模块</td></tr><tr><td style="text-align:left"><strong>Trendline Filter</strong></td><td style="text-align:left">趋势线滤波器</td><td style="text-align:left">Delay-based BWE 核心：拟合延迟梯度斜率</td></tr><tr><td style="text-align:left"><strong>Pacing</strong></td><td style="text-align:left">发包整形</td><td style="text-align:left">平滑 RTP 发送间隔，避免 burst 触发丢包</td></tr><tr><td style="text-align:left"><strong>Probing</strong></td><td style="text-align:left">带宽探测</td><td style="text-align:left">周期性试探性增码，发现剩余带宽</td></tr><tr><td style="text-align:left"><strong>Target Bitrate</strong></td><td style="text-align:left">目标码率</td><td style="text-align:left">GCC 输出，编码器实际跟随的发送速率上限</td></tr><tr><td style="text-align:left"><strong>Pacing Rate</strong></td><td style="text-align:left">整形速率</td><td style="text-align:left">通常略高于 Target Bitrate（~1.05x）</td></tr><tr><td style="text-align:left"><strong>Quality Limitation</strong></td><td style="text-align:left">质量限制</td><td style="text-align:left"><code>getStats</code> 中 <code>qualityLimitationReason</code> 指示降质原因</td></tr><tr><td style="text-align:left"><strong>NACK</strong></td><td style="text-align:left">Negative ACK</td><td style="text-align:left">丢包重传请求，增加发送压力</td></tr><tr><td style="text-align:left"><strong>FlexFEC</strong></td><td style="text-align:left">Flexible FEC</td><td style="text-align:left">前向纠错冗余，占用额外带宽预算</td></tr><tr><td style="text-align:left"><strong>rmcat</strong></td><td style="text-align:left">RTP Media Congestion Avoidance Techniques</td><td style="text-align:left">IETF 拥塞控制工作组</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一gcc-在媒体路径中的位置">一、GCC 在媒体路径中的位置<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#%E4%B8%80gcc-%E5%9C%A8%E5%AA%92%E4%BD%93%E8%B7%AF%E5%BE%84%E4%B8%AD%E7%9A%84%E4%BD%8D%E7%BD%AE" class="hash-link" aria-label="Direct link to 一、GCC 在媒体路径中的位置" title="Direct link to 一、GCC 在媒体路径中的位置" translate="no">​</a></h2>
<!-- -->
<p>GCC 是<strong>发送端算法</strong>：接收端只负责收集包到达时间并通过 RTCP 反馈；真正的拥塞判断和码率决策在发送端完成。这与 TCP 的接收端窗口不同——WebRTC 选择发送端控制是为了让 SFU 能统一调度多路订阅者的下行带宽。</p>
<p>Marratech 早期在瑞典企业内网用固定码率推流，公网扩展后 Serge Lachapelle 团队发现：<strong>没有发送端拥塞控制的 RTC 在真实互联网上不可运维</strong>。今天 Meet、Teams、LiveKit 的 SFU Forwarder 都在发送路径上复用同一套 GCC 逻辑。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二历史脉络从-remb-到-twcc">二、历史脉络：从 REMB 到 TWCC<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#%E4%BA%8C%E5%8E%86%E5%8F%B2%E8%84%89%E7%BB%9C%E4%BB%8E-remb-%E5%88%B0-twcc" class="hash-link" aria-label="Direct link to 二、历史脉络：从 REMB 到 TWCC" title="Direct link to 二、历史脉络：从 REMB 到 TWCC" translate="no">​</a></h2>
<!-- -->
<p>Serge Lachapelle 在 Google 收购 Marratech 后主导 WebRTC 标准化。Marratech 早期视频会议在瑞典企业网内运行良好，但扩展到公网后，<strong>固定码率</strong>导致大量「能连上但马赛克」的体验。GCC 的设计目标很明确：</p>
<ol>
<li class=""><strong>低延迟</strong>：不能像 TCP 那样把包堆在发送缓冲区</li>
<li class=""><strong>快速响应</strong>：200ms 内检测到拥塞并开始降码</li>
<li class=""><strong>公平性</strong>：与其他 TCP/UDP 流共存</li>
<li class=""><strong>SFU 友好</strong>：发送端控制，SFU 可为每个订阅者独立 BWE</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-gcc-与-rmcat-家族对比">2.1 GCC 与 rmcat 家族对比<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#21-gcc-%E4%B8%8E-rmcat-%E5%AE%B6%E6%97%8F%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to 2.1 GCC 与 rmcat 家族对比" title="Direct link to 2.1 GCC 与 rmcat 家族对比" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">算法</th><th style="text-align:left">控制点</th><th style="text-align:left">反馈</th><th style="text-align:left">WebRTC 采用</th></tr></thead><tbody><tr><td style="text-align:left"><strong>GCC</strong></td><td style="text-align:left">发送端</td><td style="text-align:left">TWCC + Loss</td><td style="text-align:left">Chrome/Firefox/Safari 默认</td></tr><tr><td style="text-align:left"><strong>NADA</strong></td><td style="text-align:left">发送端</td><td style="text-align:left">ECN + 延迟</td><td style="text-align:left">研究/部分实验</td></tr><tr><td style="text-align:left"><strong>SCReAM</strong></td><td style="text-align:left">发送端</td><td style="text-align:left">自包含反馈</td><td style="text-align:left">物联网/低功耗场景</td></tr><tr><td style="text-align:left"><strong>REMB</strong></td><td style="text-align:left">接收端建议</td><td style="text-align:left">RTCP REMB</td><td style="text-align:left">Legacy，已淘汰</td></tr></tbody></table>
<p>WebRTC 选择 GCC 不是因为它是唯一正确的算法，而是因为它在 Meet 规模下被验证过，且与 TWCC 配合最好。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三gcc-双模块架构">三、GCC 双模块架构<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#%E4%B8%89gcc-%E5%8F%8C%E6%A8%A1%E5%9D%97%E6%9E%B6%E6%9E%84" class="hash-link" aria-label="Direct link to 三、GCC 双模块架构" title="Direct link to 三、GCC 双模块架构" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">模块</th><th style="text-align:left">检测信号</th><th style="text-align:left">响应策略</th><th style="text-align:left">典型触发场景</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Delay-based</strong></td><td style="text-align:left">包到达间隔增大（队列积压）</td><td style="text-align:left">快速乘性降码</td><td style="text-align:left">Wi-Fi 竞争、4G 基站拥塞</td></tr><tr><td style="text-align:left"><strong>Loss-based</strong></td><td style="text-align:left">丢包率超过阈值（~2%）</td><td style="text-align:left">阶梯式降码</td><td style="text-align:left">物理链路丢包、TURN 过载</td></tr><tr><td style="text-align:left"><strong>合并策略</strong></td><td style="text-align:left">取两者较小值</td><td style="text-align:left">保守估计</td><td style="text-align:left">避免拥塞崩溃（congestion collapse）</td></tr><tr><td style="text-align:left"><strong>Probing</strong></td><td style="text-align:left">周期性试探性增码</td><td style="text-align:left">发现剩余带宽</td><td style="text-align:left">网络恢复后快速回升画质</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-delay-based-bwe-原理">3.1 Delay-based BWE 原理<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#31-delay-based-bwe-%E5%8E%9F%E7%90%86" class="hash-link" aria-label="Direct link to 3.1 Delay-based BWE 原理" title="Direct link to 3.1 Delay-based BWE 原理" translate="no">​</a></h3>
<!-- -->
<p>核心公式直觉：<strong>到达间隔 − 发送间隔 = 队列增长速率</strong>。持续为正表示网络正在排队，GCC 必须降码；持续为负表示网络有余量，可以 AIMD 增码。</p>
<p><strong>Trendline Filter</strong> 对延迟样本做线性回归，斜率持续为正触发 <code>overusing</code> 状态：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-loss-based-bwe-原理">3.2 Loss-based BWE 原理<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#32-loss-based-bwe-%E5%8E%9F%E7%90%86" class="hash-link" aria-label="Direct link to 3.2 Loss-based BWE 原理" title="Direct link to 3.2 Loss-based BWE 原理" translate="no">​</a></h3>
<p>当丢包率超过阈值时，Delay-based 可能尚未触发（例如随机无线丢包），Loss-based 模块作为安全网：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="33-probing-带宽探测">3.3 Probing 带宽探测<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#33-probing-%E5%B8%A6%E5%AE%BD%E6%8E%A2%E6%B5%8B" class="hash-link" aria-label="Direct link to 3.3 Probing 带宽探测" title="Direct link to 3.3 Probing 带宽探测" translate="no">​</a></h3>
<p>网络从拥塞恢复后，GCC 不会立刻跳回最高码率，而是通过 <strong>Probing</strong> 阶梯试探：</p>
<!-- -->
<p>Probing 与 Simulcast 层切换（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Ch11</a>）联动：带宽回升时先升 <code>rid=m</code>，再升 <code>rid=h</code>。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四twcc-vs-remb-深度对比">四、TWCC vs REMB 深度对比<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#%E5%9B%9Btwcc-vs-remb-%E6%B7%B1%E5%BA%A6%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to 四、TWCC vs REMB 深度对比" title="Direct link to 四、TWCC vs REMB 深度对比" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">特性</th><th style="text-align:left">TWCC</th><th style="text-align:left">REMB</th></tr></thead><tbody><tr><td style="text-align:left">粒度</td><td style="text-align:left">逐包延迟反馈</td><td style="text-align:left">接收端码率上限</td></tr><tr><td style="text-align:left">控制点</td><td style="text-align:left">发送端估计</td><td style="text-align:left">接收端建议</td></tr><tr><td style="text-align:left">SFU 友好</td><td style="text-align:left">✅ SFU 可代发 TWCC</td><td style="text-align:left">❌ 接收端 BWE 在 SFU 场景失效</td></tr><tr><td style="text-align:left">标准化</td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8888" target="_blank" rel="noopener noreferrer" class="">RFC 8888</a> 相关</td><td style="text-align:left">Legacy，逐步淘汰</td></tr><tr><td style="text-align:left">SDP 协商</td><td style="text-align:left"><code>a=rtcp-fb:* transport-cc</code></td><td style="text-align:left"><code>a=rtcp-fb:* goog-remb</code></td></tr><tr><td style="text-align:left">浏览器支持</td><td style="text-align:left">Chrome/Firefox/Safari 现代版</td><td style="text-align:left">仅旧版兼容</td></tr><tr><td style="text-align:left">反馈频率</td><td style="text-align:left">每 10–100ms 一批</td><td style="text-align:left">不定期</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>为什么 SFU 必须用 TWCC？</div><div class="admonitionContent_BuS1"><p>在 SFU 架构中，接收端 BWE（REMB）看到的是 SFU 到客户端的最后一段链路，无法感知发布者到 SFU 的上行拥塞。TWCC 让<strong>发送端</strong>（可以是浏览器或 SFU Forwarder）各自做 BWE，Simulcast 层选择（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Ch11</a>）才有准确输入。</p></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-sdp-中的-twcc-协商">4.1 SDP 中的 TWCC 协商<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#41-sdp-%E4%B8%AD%E7%9A%84-twcc-%E5%8D%8F%E5%95%86" class="hash-link" aria-label="Direct link to 4.1 SDP 中的 TWCC 协商" title="Direct link to 4.1 SDP 中的 TWCC 协商" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:96 transport-cc</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:111 transport-cc</span><br></div></code></pre></div></div></div>
<p><code>extmap</code> 为 RTP 头扩展分配 transport-wide sequence number；<code>rtcp-fb:transport-cc</code> 声明支持 TWCC 反馈。若协商失败，GCC 退化为仅 Loss-based BWE，画质自适应能力大幅下降。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-验证-twcc-是否生效">4.2 验证 TWCC 是否生效<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#42-%E9%AA%8C%E8%AF%81-twcc-%E6%98%AF%E5%90%A6%E7%94%9F%E6%95%88" class="hash-link" aria-label="Direct link to 4.2 验证 TWCC 是否生效" title="Direct link to 4.2 验证 TWCC 是否生效" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// examples/webrtc-lab/client/ch02-p2p-basic 扩展</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">checkTwccNegotiated</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> hasTransportCc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"codec"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mimeType</span><span class="token operator" style="color:#393A34">?.</span><span class="token method function property-access" style="color:#d73a49">includes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// 间接验证：outbound-rtp 有 targetBitrate 说明 GCC 在运行</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"transport"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">selectedCandidatePairChanges</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!==</span><span class="token plain"> </span><span class="token keyword nil" style="color:#00009f">undefined</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      hasTransportCc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 更直接：chrome://webrtc-internals → Graphs → Goog-cc</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sdp </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">sdp </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">""</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> sdp</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">includes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"transport-cc"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五码率控制与编码器集成">五、码率控制与编码器集成<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#%E4%BA%94%E7%A0%81%E7%8E%87%E6%8E%A7%E5%88%B6%E4%B8%8E%E7%BC%96%E7%A0%81%E5%99%A8%E9%9B%86%E6%88%90" class="hash-link" aria-label="Direct link to 五、码率控制与编码器集成" title="Direct link to 五、码率控制与编码器集成" translate="no">​</a></h2>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-动态限制码率">5.1 动态限制码率<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#51-%E5%8A%A8%E6%80%81%E9%99%90%E5%88%B6%E7%A0%81%E7%8E%87" class="hash-link" aria-label="Direct link to 5.1 动态限制码率" title="Direct link to 5.1 动态限制码率" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// examples/webrtc-lab/client/ch02-p2p-basic 扩展</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sender </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getSenders</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">s</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">track</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">kind </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> params </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">encodings</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">length</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">encodings</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">encodings</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">maxBitrate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">500_000</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 500 kbps 硬上限</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">encodings</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">maxFramerate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">15</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">encodings</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">scaleResolutionDownBy</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 分辨率降级</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<p>Simulcast 场景下每个 <code>encoding</code> 独立设置 <code>maxBitrate</code>（见 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">Ch9</a>）：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> params </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">encodings</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"h"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1_500_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">active</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"m"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">500_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">active</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"l"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">150_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">active</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<p>GCC 会在 <code>maxBitrate</code> <strong>之下</strong>进一步自适应——<code>maxBitrate</code> 是天花板，不是目标值。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-音频与视频码率分配">5.2 音频与视频码率分配<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#52-%E9%9F%B3%E9%A2%91%E4%B8%8E%E8%A7%86%E9%A2%91%E7%A0%81%E7%8E%87%E5%88%86%E9%85%8D" class="hash-link" aria-label="Direct link to 5.2 音频与视频码率分配" title="Direct link to 5.2 音频与视频码率分配" translate="no">​</a></h3>
<!-- -->
<table><thead><tr><th style="text-align:left">媒体</th><th style="text-align:left">典型码率</th><th style="text-align:left">GCC 行为</th></tr></thead><tbody><tr><td style="text-align:left">Opus 语音</td><td style="text-align:left">32–64 kbps</td><td style="text-align:left">相对稳定，DTX 静音时接近 0</td></tr><tr><td style="text-align:left">Opus 立体声</td><td style="text-align:left">64–128 kbps</td><td style="text-align:left">不受视频拥塞大幅影响</td></tr><tr><td style="text-align:left">VP8/VP9 视频</td><td style="text-align:left">150k–3M</td><td style="text-align:left">GCC 主控对象</td></tr><tr><td style="text-align:left">屏幕共享</td><td style="text-align:left">500k–5M</td><td style="text-align:left"><code>contentHint: "detail"</code> 倾向保分辨率</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="53-pacing-与发送平滑">5.3 Pacing 与发送平滑<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#53-pacing-%E4%B8%8E%E5%8F%91%E9%80%81%E5%B9%B3%E6%BB%91" class="hash-link" aria-label="Direct link to 5.3 Pacing 与发送平滑" title="Direct link to 5.3 Pacing 与发送平滑" translate="no">​</a></h3>
<!-- -->
<p>没有 Pacing 时，视频编码器按帧输出 burst（如 30fps 每 33ms 一次大 burst），容易填满路由器队列触发 Delay-based 降码。GCC 的 Pacing Rate 通常略高于 Target Bitrate（~1.05x），平滑发包间隔。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="54-degradationpreference-与-gcc-协作">5.4 degradationPreference 与 GCC 协作<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#54-degradationpreference-%E4%B8%8E-gcc-%E5%8D%8F%E4%BD%9C" class="hash-link" aria-label="Direct link to 5.4 degradationPreference 与 GCC 协作" title="Direct link to 5.4 degradationPreference 与 GCC 协作" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> params </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">degradationPreference</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"maintain-framerate"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// maintain-framerate: 先降分辨率</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// maintain-resolution: 先降帧率</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// balanced: 均衡</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<p><code>degradationPreference</code> 决定<strong>单流内</strong>降质策略；Simulcast 则由 SFU 做层切换，两者不要混用对抗。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六getstats-中的拥塞信号">六、getStats 中的拥塞信号<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#%E5%85%ADgetstats-%E4%B8%AD%E7%9A%84%E6%8B%A5%E5%A1%9E%E4%BF%A1%E5%8F%B7" class="hash-link" aria-label="Direct link to 六、getStats 中的拥塞信号" title="Direct link to 六、getStats 中的拥塞信号" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="61-完整监控脚本">6.1 完整监控脚本<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#61-%E5%AE%8C%E6%95%B4%E7%9B%91%E6%8E%A7%E8%84%9A%E6%9C%AC" class="hash-link" aria-label="Direct link to 6.1 完整监控脚本" title="Direct link to 6.1 完整监控脚本" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// examples/webrtc-lab/client/ch02-p2p-basic 扩展</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">collectCongestionMetrics</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> metrics </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">outbound</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">candidatePair</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">remoteInbound</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">transport</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"outbound-rtp"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      metrics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">outbound</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">rid</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">ssrc</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ssrc</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">targetBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">targetBitrate</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">framesEncoded</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">framesEncoded</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">qualityLimitationReason</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">qualityLimitationReason</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">qualityLimitationDurations</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">qualityLimitationDurations</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">encoderImplementation</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">encoderImplementation</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">retransmittedPacketsSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">retransmittedPacketsSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate-pair"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">state</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"succeeded"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">nominated</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      metrics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidatePair</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">availableOutgoingBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">availableOutgoingBitrate</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">availableIncomingBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">availableIncomingBitrate</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">currentRoundTripTime</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">currentRoundTripTime</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesReceived</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesReceived</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">localCandidateType</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localCandidateType</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">remoteCandidateType</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">remoteCandidateType</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"remote-inbound-rtp"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      metrics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">remoteInbound</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">packetsLost</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">packetsLost</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">fractionLost</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">fractionLost</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">roundTripTime</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">roundTripTime</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">jitter</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">jitter</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"transport"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      metrics</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">transport</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesReceived</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesReceived</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">dtlsState</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dtlsState</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> metrics</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 每 2 秒采样，计算码率变化率</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> prevBytes </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">setInterval</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> m </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">collectCongestionMetrics</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> totalBytes </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> m</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">outbound</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">reduce</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">s</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> o</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> s </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">o</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesSent</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> bitrate </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">totalBytes </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> prevBytes</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// bps over 2s window</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  prevBytes </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> totalBytes</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">table</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">m</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">outbound</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Instant bitrate:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token known-class-name class-name">Math</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">round</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">bitrate </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"kbps"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Candidate pair:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> m</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidatePair</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="62-关键字段解读">6.2 关键字段解读<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#62-%E5%85%B3%E9%94%AE%E5%AD%97%E6%AE%B5%E8%A7%A3%E8%AF%BB" class="hash-link" aria-label="Direct link to 6.2 关键字段解读" title="Direct link to 6.2 关键字段解读" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">类型</th><th style="text-align:left">含义</th><th style="text-align:left">告警条件</th></tr></thead><tbody><tr><td style="text-align:left"><code>targetBitrate</code></td><td style="text-align:left">outbound-rtp</td><td style="text-align:left">GCC 当前目标码率</td><td style="text-align:left">持续低于 <code>maxBitrate</code> 的 30%</td></tr><tr><td style="text-align:left"><code>qualityLimitationReason</code></td><td style="text-align:left">outbound-rtp</td><td style="text-align:left"><code>none</code> / <code>bandwidth</code> / <code>cpu</code> / <code>other</code></td><td style="text-align:left"><code>bandwidth</code> 表示 GCC 主动降质</td></tr><tr><td style="text-align:left"><code>qualityLimitationDurations</code></td><td style="text-align:left">outbound-rtp</td><td style="text-align:left">各原因累计时长</td><td style="text-align:left"><code>bandwidth</code> 占比 &gt; 50%</td></tr><tr><td style="text-align:left"><code>availableOutgoingBitrate</code></td><td style="text-align:left">candidate-pair</td><td style="text-align:left">估计可用出站带宽</td><td style="text-align:left">与 targetBitrate 差距 &gt; 50%</td></tr><tr><td style="text-align:left"><code>currentRoundTripTime</code></td><td style="text-align:left">candidate-pair</td><td style="text-align:left">当前 RTT</td><td style="text-align:left">&gt; 300ms 交互延迟明显</td></tr><tr><td style="text-align:left"><code>fractionLost</code></td><td style="text-align:left">remote-inbound-rtp</td><td style="text-align:left">最近报告周期丢包率</td><td style="text-align:left">&gt; 0.02 (2%)</td></tr><tr><td style="text-align:left"><code>retransmittedPacketsSent</code></td><td style="text-align:left">outbound-rtp</td><td style="text-align:left">NACK 重传包数</td><td style="text-align:left">持续增长说明丢包严重</td></tr></tbody></table>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="63-chromewebrtc-internals-图表对照">6.3 chrome://webrtc-internals 图表对照<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#63-chromewebrtc-internals-%E5%9B%BE%E8%A1%A8%E5%AF%B9%E7%85%A7" class="hash-link" aria-label="Direct link to 6.3 chrome://webrtc-internals 图表对照" title="Direct link to 6.3 chrome://webrtc-internals 图表对照" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">图表名</th><th style="text-align:left">对应 GCC 模块</th><th style="text-align:left">用途</th></tr></thead><tbody><tr><td style="text-align:left"><code>Goog-cc Target Bitrate</code></td><td style="text-align:left">合并输出</td><td style="text-align:left">主监控曲线</td></tr><tr><td style="text-align:left"><code>Goog-cc Delay-based Estimate</code></td><td style="text-align:left">Delay-based BWE</td><td style="text-align:left">队列积压诊断</td></tr><tr><td style="text-align:left"><code>Goog-cc Loss-based Estimate</code></td><td style="text-align:left">Loss-based BWE</td><td style="text-align:left">丢包拥塞诊断</td></tr><tr><td style="text-align:left"><code>Outbound RTP Video Bitrate</code></td><td style="text-align:left">实际发送</td><td style="text-align:left">与 Target 对比</td></tr><tr><td style="text-align:left"><code>Packets Lost</code></td><td style="text-align:left">Loss-based 输入</td><td style="text-align:left">丢包趋势</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七sfu-场景下的-gcc">七、SFU 场景下的 GCC<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#%E4%B8%83sfu-%E5%9C%BA%E6%99%AF%E4%B8%8B%E7%9A%84-gcc" class="hash-link" aria-label="Direct link to 七、SFU 场景下的 GCC" title="Direct link to 七、SFU 场景下的 GCC" translate="no">​</a></h2>
<!-- -->
<p>在 SFU 架构中，GCC 出现在三个位置：</p>
<ol>
<li class=""><strong>发布者 → SFU</strong>：浏览器上行 BWE，决定发送哪些 Simulcast 层</li>
<li class=""><strong>SFU → 订阅者</strong>：SFU Forwarder 为每个订阅者独立做下行 BWE 和层选择</li>
<li class=""><strong>订阅者接收</strong>：浏览器可选择性做额外适配</li>
</ol>
<p>LiveKit 的 Forwarder 实现了完整的 TWCC + LayerSelector 管线，详见 <a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍</a> 与 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">Ch12 SFU 架构</a>。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="71-上行-vs-下行-bwe-独立性">7.1 上行 vs 下行 BWE 独立性<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#71-%E4%B8%8A%E8%A1%8C-vs-%E4%B8%8B%E8%A1%8C-bwe-%E7%8B%AC%E7%AB%8B%E6%80%A7" class="hash-link" aria-label="Direct link to 7.1 上行 vs 下行 BWE 独立性" title="Direct link to 7.1 上行 vs 下行 BWE 独立性" translate="no">​</a></h3>
<!-- -->
<p>常见误区：发布者网络好 ≠ 订阅者能看到高清。SFU 为每个订阅者独立做下行 BWE。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八应用层与-gcc-的边界">八、应用层与 GCC 的边界<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#%E5%85%AB%E5%BA%94%E7%94%A8%E5%B1%82%E4%B8%8E-gcc-%E7%9A%84%E8%BE%B9%E7%95%8C" class="hash-link" aria-label="Direct link to 八、应用层与 GCC 的边界" title="Direct link to 八、应用层与 GCC 的边界" translate="no">​</a></h2>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>不要手动「猜测」带宽</div><div class="admonitionContent_BuS1"><p>应用层自行实现 BWE 并与 GCC 对抗是常见反模式。正确做法是设置合理的 <code>maxBitrate</code> 边界，让 GCC 在边界内自适应。SFU 层选择应读取 TWCC 反馈而非自行估算。</p></div></div>
<table><thead><tr><th style="text-align:left">应用层该做</th><th style="text-align:left">应用层不该做</th></tr></thead><tbody><tr><td style="text-align:left">设置 <code>maxBitrate</code> / <code>maxFramerate</code> 上限</td><td style="text-align:left">自行计算延迟梯度</td></tr><tr><td style="text-align:left">选择 Simulcast 层数</td><td style="text-align:left">绕过 GCC 强制固定码率</td></tr><tr><td style="text-align:left">根据 <code>qualityLimitationReason</code> 提示用户</td><td style="text-align:left">与 <code>setParameters</code> 高频对抗</td></tr><tr><td style="text-align:left">屏幕共享设更高 <code>maxBitrate</code></td><td style="text-align:left">用 DataChannel 灌满带宽</td></tr></tbody></table>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 正确：根据 GCC 信号做 UI 提示</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">collector</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onMetrics</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">m</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> video </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> m</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">outbound</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">o</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> o</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">video</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">qualityLimitationReason </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"bandwidth"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">showBanner</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"网络带宽不足，已自动降低画质"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">video</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">qualityLimitationReason </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"cpu"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">showBanner</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"设备性能不足，建议关闭高清或换设备"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九常见陷阱">九、常见陷阱<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#%E4%B9%9D%E5%B8%B8%E8%A7%81%E9%99%B7%E9%98%B1" class="hash-link" aria-label="Direct link to 九、常见陷阱" title="Direct link to 九、常见陷阱" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:center">#</th><th style="text-align:left">陷阱</th><th style="text-align:left">现象</th><th style="text-align:left">修复</th></tr></thead><tbody><tr><td style="text-align:center">1</td><td style="text-align:left">未协商 TWCC</td><td style="text-align:left">码率固定，拥塞时大量丢包</td><td style="text-align:left">SDP 添加 <code>transport-cc</code></td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><code>maxBitrate</code> 设太低</td><td style="text-align:left">画质始终模糊</td><td style="text-align:left">检查 <code>setParameters</code> 是否覆盖了默认值</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left">混淆 <code>targetBitrate</code> 与 <code>maxBitrate</code></td><td style="text-align:left">误以为已达上限</td><td style="text-align:left"><code>targetBitrate ≤ maxBitrate</code> 是正常的</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left">TURN relay 未计带宽</td><td style="text-align:left">服务器带宽爆满</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">Ch14</a> TURN 容量规划</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left">多 Tab 共享上行</td><td style="text-align:left">互相抢带宽</td><td style="text-align:left">单页面单 PeerConnection</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left">CPU 降质误判为带宽</td><td style="text-align:left"><code>qualityLimitationReason=cpu</code></td><td style="text-align:left">降分辨率而非降码率</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left">忽略 Pacing</td><td style="text-align:left">周期性卡顿</td><td style="text-align:left">浏览器内置，无需手动干预</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left">NACK 风暴</td><td style="text-align:left">重传包占比 &gt; 20%</td><td style="text-align:left">降码率 + 检查网络丢包</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left">屏幕共享用语音码率上限</td><td style="text-align:left">文字模糊</td><td style="text-align:left">屏幕共享单独设 2–5Mbps 上限</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left">SFU 忽略 per-subscriber BWE</td><td style="text-align:left">低带宽端卡顿</td><td style="text-align:left">Forwarder 独立 TWCC</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十实战-lab">十、实战 Lab<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#%E5%8D%81%E5%AE%9E%E6%88%98-lab" class="hash-link" aria-label="Direct link to 十、实战 Lab" title="Direct link to 十、实战 Lab" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-1devtools-带宽限制">Lab 1：DevTools 带宽限制<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#lab-1devtools-%E5%B8%A6%E5%AE%BD%E9%99%90%E5%88%B6" class="hash-link" aria-label="Direct link to Lab 1：DevTools 带宽限制" title="Direct link to Lab 1：DevTools 带宽限制" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># 启动 P2P Demo</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">cd</span><span class="token plain"> examples/webrtc-lab/signaling </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">npm</span><span class="token plain"> start</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 另开终端</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">npx serve examples/webrtc-lab/client/ch02-p2p-basic</span><br></div></code></pre></div></div></div>
<ol>
<li class="">打开 Chrome DevTools → Network → Throttling → Add custom profile: <strong>500 kbps</strong></li>
<li class="">发起视频通话，运行 <code>collectCongestionMetrics</code> 脚本</li>
<li class="">观察 <code>targetBitrate</code> 从 ~1.5Mbps 降至 ~400kbps</li>
<li class="">观察 <code>qualityLimitationReason</code> 变为 <code>bandwidth</code></li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-2twcc-反馈可视化">Lab 2：TWCC 反馈可视化<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#lab-2twcc-%E5%8F%8D%E9%A6%88%E5%8F%AF%E8%A7%86%E5%8C%96" class="hash-link" aria-label="Direct link to Lab 2：TWCC 反馈可视化" title="Direct link to Lab 2：TWCC 反馈可视化" translate="no">​</a></h3>
<ol>
<li class="">打开 <code>chrome://webrtc-internals</code></li>
<li class="">选择活跃的 PeerConnection → Graphs 标签</li>
<li class="">勾选 <code>Goog-cc</code> 相关图表（Target Bitrate、Delay-based estimate）</li>
<li class="">对比限速前后曲线变化</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-3simulcast-层与-gcc-联动">Lab 3：Simulcast 层与 GCC 联动<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#lab-3simulcast-%E5%B1%82%E4%B8%8E-gcc-%E8%81%94%E5%8A%A8" class="hash-link" aria-label="Direct link to Lab 3：Simulcast 层与 GCC 联动" title="Direct link to Lab 3：Simulcast 层与 GCC 联动" translate="no">​</a></h3>
<ol>
<li class="">开启 Simulcast 三档（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">Ch9 代码</a>）</li>
<li class="">限速 300kbps，观察 <code>getStats</code> 中仅 <code>rid=l</code> 的 outbound-rtp 活跃</li>
<li class="">取消限速，观察 <code>rid=h</code> 恢复</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-4丢包注入">Lab 4：丢包注入<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#lab-4%E4%B8%A2%E5%8C%85%E6%B3%A8%E5%85%A5" class="hash-link" aria-label="Direct link to Lab 4：丢包注入" title="Direct link to Lab 4：丢包注入" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Linux/macOS 需 root</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">sudo</span><span class="token plain"> tc qdisc </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> dev en0 root netem loss </span><span class="token number" style="color:#36acaa">5</span><span class="token plain">%</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 通话中观察 fractionLost 与 targetBitrate 下降</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">sudo</span><span class="token plain"> tc qdisc del dev en0 root</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-5对比-remb-时代行为">Lab 5：对比 REMB 时代行为<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#lab-5%E5%AF%B9%E6%AF%94-remb-%E6%97%B6%E4%BB%A3%E8%A1%8C%E4%B8%BA" class="hash-link" aria-label="Direct link to Lab 5：对比 REMB 时代行为" title="Direct link to Lab 5：对比 REMB 时代行为" translate="no">​</a></h3>
<p>阅读 <a href="https://webrtchacks.com/bandwidth-estimation-in-webrtc/" target="_blank" rel="noopener noreferrer" class="">webrtcH4cKS — Bandwidth Estimation</a> 中 Fippo 的实验，理解为何 REMB 在 SFU 场景失效。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-6cpu-降质-vs-带宽降质">Lab 6：CPU 降质 vs 带宽降质<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#lab-6cpu-%E9%99%8D%E8%B4%A8-vs-%E5%B8%A6%E5%AE%BD%E9%99%8D%E8%B4%A8" class="hash-link" aria-label="Direct link to Lab 6：CPU 降质 vs 带宽降质" title="Direct link to Lab 6：CPU 降质 vs 带宽降质" translate="no">​</a></h3>
<ol>
<li class="">开启 1080p 30fps 通话</li>
<li class="">用 Chrome Task Manager 观察 GPU/CPU</li>
<li class="">若 <code>qualityLimitationReason=cpu</code>，降低 <code>scaleResolutionDownBy</code></li>
<li class="">若 <code>qualityLimitationReason=bandwidth</code>，检查网络限速</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-7码率变化率计算">Lab 7：码率变化率计算<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#lab-7%E7%A0%81%E7%8E%87%E5%8F%98%E5%8C%96%E7%8E%87%E8%AE%A1%E7%AE%97" class="hash-link" aria-label="Direct link to Lab 7：码率变化率计算" title="Direct link to Lab 7：码率变化率计算" translate="no">​</a></h3>
<ol>
<li class="">运行 §6.1 脚本的 <code>bitrate</code> 计算</li>
<li class="">在限速切换瞬间观察码率下降斜率</li>
<li class="">记录从 1.5Mbps 到 400kbps 的收敛时间（通常 2–5 秒）</li>
</ol>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十一本章小结">十一、本章小结<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#%E5%8D%81%E4%B8%80%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="Direct link to 十一、本章小结" title="Direct link to 十一、本章小结" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">概念</th><th style="text-align:left">要点</th></tr></thead><tbody><tr><td style="text-align:left">GCC</td><td style="text-align:left">Delay + Loss 双模块，取 min 合并</td></tr><tr><td style="text-align:left">TWCC</td><td style="text-align:left">现代标准，发送端 BWE，SFU 友好</td></tr><tr><td style="text-align:left">REMB</td><td style="text-align:left">Legacy，接收端建议，逐步淘汰</td></tr><tr><td style="text-align:left">Probing</td><td style="text-align:left">网络恢复后阶梯试探回升</td></tr><tr><td style="text-align:left">监控</td><td style="text-align:left"><code>targetBitrate</code> + <code>qualityLimitationReason</code> + <code>availableOutgoingBitrate</code></td></tr><tr><td style="text-align:left">实践</td><td style="text-align:left">设 <code>maxBitrate</code> 边界，让 GCC 自适应，不要对抗</td></tr></tbody></table>
<p><strong>下一篇（Ch11）</strong>：<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast 与 SVC</a></p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://datatracker.ietf.org/doc/html/draft-ietf-rmcat-gcc" target="_blank" rel="noopener noreferrer" class="">draft-ietf-rmcat-gcc — Google Congestion Control</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8888" target="_blank" rel="noopener noreferrer" class="">RFC 8888 — RTP Control Protocol Feedback for Congestion Control</a></li>
<li class=""><a href="https://webrtchacks.com/bandwidth-estimation-in-webrtc/" target="_blank" rel="noopener noreferrer" class="">webrtcH4cKS — Bandwidth Estimation in WebRTC</a></li>
<li class=""><a href="https://blogwebrtc.org/" target="_blank" rel="noopener noreferrer" class="">Advancing WebRTC — GCC 分析 (Fippo)</a></li>
<li class=""><a href="https://webrtcforthecurious.com/docs/09-network-congestion-control/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — Congestion Control</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史（Serge Lachapelle 访谈）</a></li>
<li class=""><a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍 — 本站推荐</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>实时通信</category>
            <category>Deep Dive</category>
            <category>Monitoring</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (9)：音视频编解码与 Simulcast 入门]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro</guid>
            <pubDate>Sat, 20 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[Opus/VP8/VP9/H.264/AV1 编解码对比、Simulcast 三档发布与 RTCRtpEncodingParameters 实战]]></description>
            <content:encoded><![CDATA[<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">Ch4 SDP</a> 协商的核心是<strong>编解码器选择</strong>——<code>a=rtpmap</code> 行的背后，是数十年来视频压缩技术的积累。Ron Frederick 在 1992 年为实现 nv 工具而<strong>手写软件视频压缩</strong>（<a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious — RTP 历史</a>），因为 MPEG-1 当时无法实时编码——今天 WebRTC 的 Codec 选择同样是在<strong>压缩率、延迟、专利、硬件加速</strong>之间权衡。</p>
<p>音频方面，Opus 的故事同样精彩：Skype 团队在收购后于 2010 年 IETF 会议上推动 Opus 标准化，Maastricht 午餐会时已完成大部分工作（<a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 历史</a>）。Opus 继承了 Skype 的 SILK 和 Xiph 的 CELT 两条技术路线，成为 WebRTC 唯一的「指定音频编解码器」。</p>
<p>本章覆盖 <a href="https://datatracker.ietf.org/doc/html/rfc7587" target="_blank" rel="noopener noreferrer" class="">RFC 7587</a> Opus、VP8/VP9/H.264/AV1 对比、Simulcast 三档发布与 <code>RTCRtpEncodingParameters</code> 实战配置。</p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="零skype-与-opus-的诞生">零、Skype 与 Opus 的诞生<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#%E9%9B%B6skype-%E4%B8%8E-opus-%E7%9A%84%E8%AF%9E%E7%94%9F" class="hash-link" aria-label="Direct link to 零、Skype 与 Opus 的诞生" title="Direct link to 零、Skype 与 Opus 的诞生" translate="no">​</a></h2>
<p><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 历史</a> 记录了 Opus 标准化过程中一段有趣的插曲。2010 年 IETF 81 会议在 Maastricht 举行，Skype 工程师 Jean-Marc Valin 和 Koen Vos 带着几乎完成的 Opus 草案参会——<strong>午餐桌上就完成了大部分互操作测试</strong>。</p>
<table><thead><tr><th style="text-align:left">技术 lineage</th><th style="text-align:left">来源</th><th style="text-align:left">擅长场景</th></tr></thead><tbody><tr><td style="text-align:left"><strong>SILK</strong></td><td style="text-align:left">Skype 收购前自研</td><td style="text-align:left">8–40 kbps 语音，强抗丢包</td></tr><tr><td style="text-align:left"><strong>CELT</strong></td><td style="text-align:left">Xiph.Org（Ogg/Vorbis 团队）</td><td style="text-align:left">48–510 kbps 音乐，低延迟</td></tr><tr><td style="text-align:left"><strong>Opus</strong></td><td style="text-align:left">2012 合并标准化</td><td style="text-align:left">全码率覆盖，WebRTC 指定音频 Codec</td></tr></tbody></table>
<p>Microsoft 2011 年收购 Skype 后，Skype 团队将 SILK 技术贡献给 IETF，与开源社区 CELT 合并为 Opus。<a href="https://datatracker.ietf.org/doc/html/rfc7587" target="_blank" rel="noopener noreferrer" class="">RFC 7587</a> 2015 年发布，<a href="https://datatracker.ietf.org/doc/html/rfc8871" target="_blank" rel="noopener noreferrer" class="">RFC 8871</a> 将 Opus 列为 WebRTC <strong>Mandatory to Implement (MTI)</strong> 音频编解码器——所有 WebRTC 实现必须支持 Opus。</p>
<p>视频侧，Ron Frederick 1992 年为 nv 工具<strong>手写软件视频压缩</strong>（因为 MPEG-1 无法实时编码），这与今天 VP8/AV1 追求「实时 + 高压缩 + 免专利」的路线一脉相承。</p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Codec</strong></td><td style="text-align:left">Coder-Decoder</td><td style="text-align:left">编解码器，压缩/解压媒体数据</td></tr><tr><td style="text-align:left"><strong>Opus</strong></td><td style="text-align:left">—</td><td style="text-align:left">WebRTC 标准音频编解码器，<a href="https://datatracker.ietf.org/doc/html/rfc7587" target="_blank" rel="noopener noreferrer" class="">RFC 7587</a></td></tr><tr><td style="text-align:left"><strong>VP8</strong></td><td style="text-align:left">—</td><td style="text-align:left">Google 开源视频编解码器，<a href="https://datatracker.ietf.org/doc/html/rfc7741" target="_blank" rel="noopener noreferrer" class="">RFC 7741</a></td></tr><tr><td style="text-align:left"><strong>VP9</strong></td><td style="text-align:left">—</td><td style="text-align:left">VP8 继任者，更高压缩率，无 royalty</td></tr><tr><td style="text-align:left"><strong>H.264/AVC</strong></td><td style="text-align:left">—</td><td style="text-align:left">ITU/MPEG 标准，硬件加速最广泛</td></tr><tr><td style="text-align:left"><strong>AV1</strong></td><td style="text-align:left">—</td><td style="text-align:left">AOMedia 开源编解码器，最高压缩率</td></tr><tr><td style="text-align:left"><strong>Simulcast</strong></td><td style="text-align:left">—</td><td style="text-align:left">同时编码发送同一视频的多个分辨率层</td></tr><tr><td style="text-align:left"><strong>rid</strong></td><td style="text-align:left">RTP Stream ID</td><td style="text-align:left">Simulcast 层的标识符（h/m/l）</td></tr><tr><td style="text-align:left"><strong>SSRC</strong></td><td style="text-align:left">Synchronization Source</td><td style="text-align:left">每层 Simulcast 分配独立 SSRC</td></tr><tr><td style="text-align:left"><strong>RTCRtpEncodingParameters</strong></td><td style="text-align:left">—</td><td style="text-align:left">控制每路编码的码率、分辨率、rid</td></tr><tr><td style="text-align:left"><strong>scaleResolutionDownBy</strong></td><td style="text-align:left">—</td><td style="text-align:left">相对原始分辨率的下采样倍数</td></tr><tr><td style="text-align:left"><strong>maxBitrate</strong></td><td style="text-align:left">—</td><td style="text-align:left">该层编码的最大码率上限</td></tr><tr><td style="text-align:left"><strong>active</strong></td><td style="text-align:left">—</td><td style="text-align:left">该层是否激活发送</td></tr><tr><td style="text-align:left"><strong>Keyframe</strong></td><td style="text-align:left">I 帧 / IDR</td><td style="text-align:left">可独立解码的完整帧</td></tr><tr><td style="text-align:left"><strong>DTX</strong></td><td style="text-align:left">Discontinuous Transmission</td><td style="text-align:left">静音时停止发送，节省带宽</td></tr><tr><td style="text-align:left"><strong>FEC</strong></td><td style="text-align:left">Forward Error Correction</td><td style="text-align:left">带内前向纠错</td></tr><tr><td style="text-align:left"><strong>profile-level-id</strong></td><td style="text-align:left">—</td><td style="text-align:left">H.264 的 profile 与 level 标识</td></tr><tr><td style="text-align:left"><strong>contentHint</strong></td><td style="text-align:left">—</td><td style="text-align:left">提示编码器内容类型：<code>motion</code> / <code>detail</code> / <code>text</code></td></tr><tr><td style="text-align:left"><strong>Unified Plan</strong></td><td style="text-align:left">—</td><td style="text-align:left">WebRTC 现代 SDP 语义，Simulcast 必须使用</td></tr><tr><td style="text-align:left"><strong>MTI</strong></td><td style="text-align:left">Mandatory to Implement</td><td style="text-align:left">WebRTC 强制实现的编解码器</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一编解码在媒体管线中的位置">一、编解码在媒体管线中的位置<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#%E4%B8%80%E7%BC%96%E8%A7%A3%E7%A0%81%E5%9C%A8%E5%AA%92%E4%BD%93%E7%AE%A1%E7%BA%BF%E4%B8%AD%E7%9A%84%E4%BD%8D%E7%BD%AE" class="hash-link" aria-label="Direct link to 一、编解码在媒体管线中的位置" title="Direct link to 一、编解码在媒体管线中的位置" translate="no">​</a></h2>
<!-- -->
<p>编解码器是 WebRTC 媒体管线中<strong>CPU/GPU 消耗最大</strong>的环节。选择 Codec 不仅影响画质，还决定了：</p>
<ul>
<li class="">端到端延迟（编码复杂度）</li>
<li class="">带宽消耗（压缩效率）</li>
<li class="">设备兼容性（硬件加速支持）</li>
<li class="">专利成本（H.264 vs royalty-free）</li>
</ul>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二音频opusrfc-7587">二、音频：Opus（RFC 7587）<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#%E4%BA%8C%E9%9F%B3%E9%A2%91opusrfc-7587" class="hash-link" aria-label="Direct link to 二、音频：Opus（RFC 7587）" title="Direct link to 二、音频：Opus（RFC 7587）" translate="no">​</a></h2>
<p>WebRTC 音频几乎总是 <strong>Opus</strong>——Skype 团队在 IETF 标准化，2012 年发布 RFC 6381，2015 年更新为 <a href="https://datatracker.ietf.org/doc/html/rfc7587" target="_blank" rel="noopener noreferrer" class="">RFC 7587</a>。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-opus-的双模架构">2.1 Opus 的双模架构<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#21-opus-%E7%9A%84%E5%8F%8C%E6%A8%A1%E6%9E%B6%E6%9E%84" class="hash-link" aria-label="Direct link to 2.1 Opus 的双模架构" title="Direct link to 2.1 Opus 的双模架构" translate="no">​</a></h3>
<p>Opus 是两种编解码器的结合：</p>
<!-- -->
<table><thead><tr><th style="text-align:left">特性</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">Bitrate</td><td style="text-align:left">6–510 kbps 自适应</td></tr><tr><td style="text-align:left">帧长</td><td style="text-align:left">2.5ms / 5ms / 10ms / 20ms / 40ms / 60ms</td></tr><tr><td style="text-align:left">FEC</td><td style="text-align:left">内置前向纠错，抗丢包</td></tr><tr><td style="text-align:left">DTX</td><td style="text-align:left">静音检测，停止发送节省带宽</td></tr><tr><td style="text-align:left">采样率</td><td style="text-align:left">8kHz–48kHz（WebRTC 固定 48kHz）</td></tr><tr><td style="text-align:left">声道</td><td style="text-align:left">1（mono）或 2（stereo）</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="22-sdp-中的-opus-参数">2.2 SDP 中的 Opus 参数<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#22-sdp-%E4%B8%AD%E7%9A%84-opus-%E5%8F%82%E6%95%B0" class="hash-link" aria-label="Direct link to 2.2 SDP 中的 Opus 参数" title="Direct link to 2.2 SDP 中的 Opus 参数" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:111 opus/48000/2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=fmtp:111 minptime=10;useinbandfec=1;stereo=1;maxaveragebitrate=32000</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">fmtp 参数</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left"><code>minptime=10</code></td><td style="text-align:left">最小打包时长 10ms</td></tr><tr><td style="text-align:left"><code>useinbandfec=1</code></td><td style="text-align:left">启用带内 FEC</td></tr><tr><td style="text-align:left"><code>stereo=1</code></td><td style="text-align:left">声明支持立体声</td></tr><tr><td style="text-align:left"><code>maxaveragebitrate=32000</code></td><td style="text-align:left">限制平均码率 32kbps</td></tr><tr><td style="text-align:left"><code>cbr=1</code></td><td style="text-align:left">恒定码率模式</td></tr><tr><td style="text-align:left"><code>usedtx=1</code></td><td style="text-align:left">启用 DTX 静音检测</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="23-opus-码率与质量">2.3 Opus 码率与质量<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#23-opus-%E7%A0%81%E7%8E%87%E4%B8%8E%E8%B4%A8%E9%87%8F" class="hash-link" aria-label="Direct link to 2.3 Opus 码率与质量" title="Direct link to 2.3 Opus 码率与质量" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">场景</th><th style="text-align:left">推荐码率</th><th style="text-align:left">帧长</th></tr></thead><tbody><tr><td style="text-align:left">窄带语音（电话）</td><td style="text-align:left">12–20 kbps</td><td style="text-align:left">20ms</td></tr><tr><td style="text-align:left">宽带语音（会议）</td><td style="text-align:left">24–32 kbps</td><td style="text-align:left">20ms</td></tr><tr><td style="text-align:left">高质量语音</td><td style="text-align:left">48–64 kbps</td><td style="text-align:left">20ms</td></tr><tr><td style="text-align:left">音乐 / 屏幕共享音频</td><td style="text-align:left">64–128 kbps</td><td style="text-align:left">20ms</td></tr></tbody></table>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 限制 Opus 码率</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sender </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getSenders</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">s</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">track</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">kind </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"audio"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> params </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">encodings</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">maxBitrate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">32_000</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="24-red冗余编码">2.4 RED（冗余编码）<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#24-red%E5%86%97%E4%BD%99%E7%BC%96%E7%A0%81" class="hash-link" aria-label="Direct link to 2.4 RED（冗余编码）" title="Direct link to 2.4 RED（冗余编码）" translate="no">​</a></h3>
<p>SDP 中常见的 RED payload type 是 Opus 的冗余封装：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:63 red/48000/2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=fmtp:63 111/111/111/111/111</span><br></div></code></pre></div></div></div>
<p>RED 将多个 Opus 帧打包在一个 RTP 包中，提供包级别的冗余（与 in-band FEC 互补）。WebRTC 中 RED 的使用因浏览器而异。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="25-opus-编码模式选择">2.5 Opus 编码模式选择<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#25-opus-%E7%BC%96%E7%A0%81%E6%A8%A1%E5%BC%8F%E9%80%89%E6%8B%A9" class="hash-link" aria-label="Direct link to 2.5 Opus 编码模式选择" title="Direct link to 2.5 Opus 编码模式选择" translate="no">​</a></h3>
<p>Opus 编码器根据码率和内容自动切换 SILK/CELT/Hybrid 模式，但应用层可通过 SDP 和参数施加约束：</p>
<table><thead><tr><th style="text-align:left">模式</th><th style="text-align:left">触发条件</th><th style="text-align:left">典型码率</th></tr></thead><tbody><tr><td style="text-align:left">SILK</td><td style="text-align:left">语音主导，低码率</td><td style="text-align:left">6–40 kbps</td></tr><tr><td style="text-align:left">Hybrid</td><td style="text-align:left">语音 + 宽带</td><td style="text-align:left">32–64 kbps</td></tr><tr><td style="text-align:left">CELT</td><td style="text-align:left">音乐/高保真</td><td style="text-align:left">64–510 kbps</td></tr></tbody></table>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 屏幕共享时音频轨 often 是系统音——提高码率上限</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> audioSender </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getSenders</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">s</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">track</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">kind </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"audio"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> audioParams </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> audioSender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">audioParams</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">encodings</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">maxBitrate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">128_000</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> audioSender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">audioParams</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三视频编解码对比">三、视频编解码对比<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#%E4%B8%89%E8%A7%86%E9%A2%91%E7%BC%96%E8%A7%A3%E7%A0%81%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to 三、视频编解码对比" title="Direct link to 三、视频编解码对比" translate="no">​</a></h2>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-详细对比表">3.1 详细对比表<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#31-%E8%AF%A6%E7%BB%86%E5%AF%B9%E6%AF%94%E8%A1%A8" class="hash-link" aria-label="Direct link to 3.1 详细对比表" title="Direct link to 3.1 详细对比表" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">Codec</th><th style="text-align:left">压缩率</th><th style="text-align:left">编码延迟</th><th style="text-align:left">硬件加速</th><th style="text-align:left">专利</th><th style="text-align:left">WebRTC 支持</th><th style="text-align:left">推荐场景</th></tr></thead><tbody><tr><td style="text-align:left"><strong>VP8</strong></td><td style="text-align:left">中</td><td style="text-align:left">低</td><td style="text-align:left">广泛</td><td style="text-align:left">royalty-free</td><td style="text-align:left">全平台</td><td style="text-align:left">默认首选、低延迟</td></tr><tr><td style="text-align:left"><strong>VP9</strong></td><td style="text-align:left">高（比 VP8 约 30-50%）</td><td style="text-align:left">中</td><td style="text-align:left">较广泛</td><td style="text-align:left">royalty-free</td><td style="text-align:left">桌面为主</td><td style="text-align:left">带宽受限、高分辨率</td></tr><tr><td style="text-align:left"><strong>H.264</strong></td><td style="text-align:left">高</td><td style="text-align:left">低（硬编）</td><td style="text-align:left">最广泛</td><td style="text-align:left">有专利池</td><td style="text-align:left">全平台</td><td style="text-align:left">移动端、硬编优先</td></tr><tr><td style="text-align:left"><strong>AV1</strong></td><td style="text-align:left">最高（比 H.264 约 30-50%）</td><td style="text-align:left">高（软编）</td><td style="text-align:left">新兴</td><td style="text-align:left">royalty-free</td><td style="text-align:left">Chrome 117+</td><td style="text-align:left">未来方向、带宽极受限</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-vp8rfc-7741">3.2 VP8（RFC 7741）<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#32-vp8rfc-7741" class="hash-link" aria-label="Direct link to 3.2 VP8（RFC 7741）" title="Direct link to 3.2 VP8（RFC 7741）" translate="no">​</a></h3>
<p>Google 2010 年收购 On2 后开源 VP8，成为 WebRTC 最早的视频编解码器。</p>
<table><thead><tr><th style="text-align:left">特性</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">最大分辨率</td><td style="text-align:left">16384×16384（实际受设备限制）</td></tr><tr><td style="text-align:left">关键帧间隔</td><td style="text-align:left">可配置，WebRTC 默认约 3 秒或 PLI 触发</td></tr><tr><td style="text-align:left">错误恢复</td><td style="text-align:left">黄金帧（Golden Frame）机制</td></tr><tr><td style="text-align:left">RTP 打包</td><td style="text-align:left">单帧可拆多个 RTP 包，M bit 标记最后一包</td></tr></tbody></table>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:96 VP8/90000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:96 nack</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:96 nack pli</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:96 transport-cc</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="33-vp9">3.3 VP9<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#33-vp9" class="hash-link" aria-label="Direct link to 3.3 VP9" title="Direct link to 3.3 VP9" translate="no">​</a></h3>
<p>VP9 是 VP8 的继任者，主要优势在<strong>高分辨率</strong>和<strong>带宽节省</strong>：</p>
<table><thead><tr><th style="text-align:left">对比 VP8</th><th style="text-align:left">VP9</th></tr></thead><tbody><tr><td style="text-align:left">同画质码率</td><td style="text-align:left">降低 30-50%</td></tr><tr><td style="text-align:left">编码 CPU</td><td style="text-align:left">高约 2-3x</td></tr><tr><td style="text-align:left">SVC 支持</td><td style="text-align:left">原生 SVC（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Ch11</a>）</td></tr><tr><td style="text-align:left">硬件加速</td><td style="text-align:left">Android 较广泛，iOS 不支持</td></tr></tbody></table>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:98 VP9/90000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=fmtp:98 profile-id=0</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="34-h264avc">3.4 H.264/AVC<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#34-h264avc" class="hash-link" aria-label="Direct link to 3.4 H.264/AVC" title="Direct link to 3.4 H.264/AVC" translate="no">​</a></h3>
<p>H.264 是 ITU-T 和 MPEG 联合标准，硬件加速支持最广泛——几乎所有手机芯片都有 H.264 硬编硬解。</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:102 H264/90000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">fmtp 参数</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left"><code>profile-level-id=42e01f</code></td><td style="text-align:left">Baseline Profile, Level 3.1</td></tr><tr><td style="text-align:left"><code>packetization-mode=1</code></td><td style="text-align:left">非交错模式（WebRTC 要求）</td></tr><tr><td style="text-align:left"><code>level-asymmetry-allowed=1</code></td><td style="text-align:left">允许双方 level 不对称</td></tr></tbody></table>
<p><strong>profile-level-id 解码</strong>：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">42e01f →</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  42 = Baseline Profile (0x42)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  e0 = constraint_set flags</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  1f = Level 3.1 (0x1f = 31)</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>H.264 Profile 匹配</div><div class="admonitionContent_BuS1"><p>双方 profile-level-id 必须兼容。Offer 中列出多个 H.264 PT（不同 profile），Answer 选择双方都支持的那个。profile 不匹配是 H.264 协商失败的首要原因。</p></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="35-av1">3.5 AV1<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#35-av1" class="hash-link" aria-label="Direct link to 3.5 AV1" title="Direct link to 3.5 AV1" translate="no">​</a></h3>
<p>AV1 由 AOMedia（Google/Mozilla/Netflix 等）开发，压缩率最高但编码复杂度也最高。</p>
<table><thead><tr><th style="text-align:left">特性</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">压缩率</td><td style="text-align:left">比 H.264 高 30-50%，比 VP9 高 20-30%</td></tr><tr><td style="text-align:left">编码速度</td><td style="text-align:left">软编极慢（实时场景需硬编）</td></tr><tr><td style="text-align:left">硬件加速</td><td style="text-align:left">Intel/AMD/NVIDIA 新一代 GPU 开始支持</td></tr><tr><td style="text-align:left">WebRTC</td><td style="text-align:left">Chrome 117+ 支持，Safari/Firefox 逐步跟进</td></tr></tbody></table>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:41 AV1/90000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=fmtp:41 profile=0;level-idx=5;tier=0</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="36-codec-选择策略">3.6 Codec 选择策略<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#36-codec-%E9%80%89%E6%8B%A9%E7%AD%96%E7%95%A5" class="hash-link" aria-label="Direct link to 3.6 Codec 选择策略" title="Direct link to 3.6 Codec 选择策略" translate="no">​</a></h3>
<!-- -->
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 设置 Codec 偏好</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> transceiver </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTransceiver</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">direction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"sendrecv"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> caps </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token maybe-class-name">RTCRtpSender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getCapabilities</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 优先 H.264，其次 VP8</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> preferred </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> caps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">codecs</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">sort</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">a</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> b</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> order </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"video/H264"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"video/VP8"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"video/VP9"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string-property property" style="color:#36acaa">"video/AV1"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">3</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">order</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">a</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mimeType</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">??</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">99</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">-</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">order</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">b</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mimeType</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">??</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">99</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">transceiver</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setCodecPreferences</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">preferred</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="37-屏幕共享-vs-摄像头contenthint">3.7 屏幕共享 vs 摄像头：contentHint<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#37-%E5%B1%8F%E5%B9%95%E5%85%B1%E4%BA%AB-vs-%E6%91%84%E5%83%8F%E5%A4%B4contenthint" class="hash-link" aria-label="Direct link to 3.7 屏幕共享 vs 摄像头：contentHint" title="Direct link to 3.7 屏幕共享 vs 摄像头：contentHint" translate="no">​</a></h3>
<p>屏幕共享（<code>getDisplayMedia</code>）与摄像头的内容特性截然不同——静态文字需要 sharp edges，摄像头需要运动平滑：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> screenTrack </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> screenStream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getVideoTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">screenTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">contentHint</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"detail"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 或 "text" / "motion"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> cameraTrack </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> cameraStream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getVideoTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">cameraTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">contentHint</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"motion"</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">contentHint</th><th style="text-align:left">编码器行为</th><th style="text-align:left">适用</th></tr></thead><tbody><tr><td style="text-align:left"><code>motion</code></td><td style="text-align:left">优先帧率，允许模糊</td><td style="text-align:left">摄像头、游戏</td></tr><tr><td style="text-align:left"><code>detail</code></td><td style="text-align:left">优先清晰度，保文字边缘</td><td style="text-align:left">屏幕共享</td></tr><tr><td style="text-align:left"><code>text</code></td><td style="text-align:left">最高清晰度，低帧率可接受</td><td style="text-align:left">文档/代码演示</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="38-h264-与-simulcast-的限制">3.8 H.264 与 Simulcast 的限制<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#38-h264-%E4%B8%8E-simulcast-%E7%9A%84%E9%99%90%E5%88%B6" class="hash-link" aria-label="Direct link to 3.8 H.264 与 Simulcast 的限制" title="Direct link to 3.8 H.264 与 Simulcast 的限制" translate="no">​</a></h3>
<p>H.264 <strong>硬件编码器</strong>通常不支持 Simulcast 多路独立编码——同一时刻只能输出一路。Chrome 在 H.264 Simulcast 场景可能回退到软编或只发送单层。生产会议系统常见策略：</p>
<table><thead><tr><th style="text-align:left">策略</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">VP8/VP9 Simulcast</td><td style="text-align:left">桌面端默认，多层无专利顾虑</td></tr><tr><td style="text-align:left">H.264 单流 + SFU 转码</td><td style="text-align:left">移动端发送 H.264，SFU 转码给其他订阅者</td></tr><tr><td style="text-align:left">SVC（VP9/AV1）</td><td style="text-align:left">单编码多分层，见 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Ch11</a></td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四simulcast-三档发布">四、Simulcast 三档发布<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#%E5%9B%9Bsimulcast-%E4%B8%89%E6%A1%A3%E5%8F%91%E5%B8%83" class="hash-link" aria-label="Direct link to 四、Simulcast 三档发布" title="Direct link to 四、Simulcast 三档发布" translate="no">​</a></h2>
<p>Simulcast = <strong>同时编码并发送同一视频的多个分辨率/码率层</strong>，SFU 按订阅者带宽选择转发哪一层（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Ch11</a> 详解 SFU 侧路由）。</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-为什么需要-simulcast">4.1 为什么需要 Simulcast？<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#41-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-simulcast" class="hash-link" aria-label="Direct link to 4.1 为什么需要 Simulcast？" title="Direct link to 4.1 为什么需要 Simulcast？" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">场景</th><th style="text-align:left">无 Simulcast</th><th style="text-align:left">有 Simulcast</th></tr></thead><tbody><tr><td style="text-align:left">100 人会议，带宽各异</td><td style="text-align:left">所有人收到相同质量</td><td style="text-align:left">每人按带宽收不同层</td></tr><tr><td style="text-align:left">大屏 + 小窗</td><td style="text-align:left">小窗浪费高分辨率带宽</td><td style="text-align:left">小窗收 l 层</td></tr><tr><td style="text-align:left">带宽波动</td><td style="text-align:left">重协商 Codec/分辨率</td><td style="text-align:left">SFU 无缝切换层</td></tr></tbody></table>
<p>Simulcast 的代价是<strong>发送端 CPU/带宽</strong>：三档意味着编码器运行 3 次。但发送端只需上行 1.5Mbps（最高层），而非 100 × 1.5Mbps——这就是 SFU 架构的优势。</p>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>Unified Plan 是前提</div><div class="admonitionContent_BuS1"><p>Simulcast 要求 <strong>Unified Plan</strong> SDP 语义（现代浏览器默认）。Plan B 已废弃——若 <code>pc.getConfiguration().sdpSemantics !== 'unified-plan'</code>，Simulcast 不会生效。每个 rid 对应 <code>RTCRtpSender</code> 的一个 encoding，而非独立 m-line。</p></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-rtcrtpencodingparameters-实战">4.2 RTCRtpEncodingParameters 实战<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#42-rtcrtpencodingparameters-%E5%AE%9E%E6%88%98" class="hash-link" aria-label="Direct link to 4.2 RTCRtpEncodingParameters 实战" title="Direct link to 4.2 RTCRtpEncodingParameters 实战" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> transceiver </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTransceiver</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">direction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"sendonly"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">sendEncodings</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"h"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1_500_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">maxFramerate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">30</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">scaleResolutionDownBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1.0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">active</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"m"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">500_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">maxFramerate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">15</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">scaleResolutionDownBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2.0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">active</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"l"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">150_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">maxFramerate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">7.5</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">scaleResolutionDownBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4.0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">active</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="43-encodingparameters-字段详解">4.3 EncodingParameters 字段详解<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#43-encodingparameters-%E5%AD%97%E6%AE%B5%E8%AF%A6%E8%A7%A3" class="hash-link" aria-label="Direct link to 4.3 EncodingParameters 字段详解" title="Direct link to 4.3 EncodingParameters 字段详解" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">类型</th><th style="text-align:left">含义</th><th style="text-align:left">示例</th></tr></thead><tbody><tr><td style="text-align:left"><code>rid</code></td><td style="text-align:left">string</td><td style="text-align:left">RTP Stream ID，标识 Simulcast 层</td><td style="text-align:left"><code>"h"</code> / <code>"m"</code> / <code>"l"</code></td></tr><tr><td style="text-align:left"><code>active</code></td><td style="text-align:left">boolean</td><td style="text-align:left">是否激活该层发送</td><td style="text-align:left"><code>true</code></td></tr><tr><td style="text-align:left"><code>maxBitrate</code></td><td style="text-align:left">number</td><td style="text-align:left">最大码率（bps）</td><td style="text-align:left"><code>1500000</code></td></tr><tr><td style="text-align:left"><code>minBitrate</code></td><td style="text-align:left">number</td><td style="text-align:left">最小码率（bps）</td><td style="text-align:left"><code>30000</code></td></tr><tr><td style="text-align:left"><code>maxFramerate</code></td><td style="text-align:left">number</td><td style="text-align:left">最大帧率</td><td style="text-align:left"><code>30</code></td></tr><tr><td style="text-align:left"><code>scaleResolutionDownBy</code></td><td style="text-align:left">number</td><td style="text-align:left">下采样倍数（相对原始）</td><td style="text-align:left"><code>2.0</code> = 宽高各减半</td></tr><tr><td style="text-align:left"><code>priority</code></td><td style="text-align:left">string</td><td style="text-align:left">层优先级</td><td style="text-align:left"><code>"high"</code> / <code>"low"</code></td></tr><tr><td style="text-align:left"><code>networkPriority</code></td><td style="text-align:left">string</td><td style="text-align:left">网络优先级</td><td style="text-align:left"><code>"high"</code> / <code>"low"</code></td></tr><tr><td style="text-align:left"><code>ssrc</code></td><td style="text-align:left">number</td><td style="text-align:left">手动指定 SSRC（通常自动生成）</td><td style="text-align:left">—</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="44-sdp-中的-simulcast-协商">4.4 SDP 中的 Simulcast 协商<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#44-sdp-%E4%B8%AD%E7%9A%84-simulcast-%E5%8D%8F%E5%95%86" class="hash-link" aria-label="Direct link to 4.4 SDP 中的 Simulcast 协商" title="Direct link to 4.4 SDP 中的 Simulcast 协商" translate="no">​</a></h3>
<p>Offer 中 Simulcast 相关属性：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=simulcast:send h;m;l</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rid:1 send h</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rid:2 send m</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rid:3 send l</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">属性</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left"><code>a=simulcast:send h;m;l</code></td><td style="text-align:left">发送三档，rid 分别为 h/m/l</td></tr><tr><td style="text-align:left"><code>a=rid:1 send h</code></td><td style="text-align:left">rid=h 映射到 mid 内的层 1</td></tr><tr><td style="text-align:left"><code>rtp-stream-id</code> extmap</td><td style="text-align:left">RTP 头扩展携带 rid</td></tr><tr><td style="text-align:left"><code>repaired-rtp-stream-id</code></td><td style="text-align:left">RTX 重传时关联原始 rid</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="45-动态层管理">4.5 动态层管理<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#45-%E5%8A%A8%E6%80%81%E5%B1%82%E7%AE%A1%E7%90%86" class="hash-link" aria-label="Direct link to 4.5 动态层管理" title="Direct link to 4.5 动态层管理" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 运行时关闭低质量层（节省 CPU）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sender </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getSenders</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">s</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">track</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">kind </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> params </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">encodings</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">enc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">enc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">rid</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"l"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    enc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">active</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 关闭 l 层</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 运行时调整码率</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">encodings</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">enc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">enc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">rid</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"h"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    enc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">maxBitrate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2_000_000</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 提升 h 层到 2Mbps</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="46-simulcast-vs-svc">4.6 Simulcast vs SVC<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#46-simulcast-vs-svc" class="hash-link" aria-label="Direct link to 4.6 Simulcast vs SVC" title="Direct link to 4.6 Simulcast vs SVC" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">特性</th><th style="text-align:left">Simulcast</th><th style="text-align:left">SVC（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Ch11</a>）</th></tr></thead><tbody><tr><td style="text-align:left">编码次数</td><td style="text-align:left">N 次（每层独立）</td><td style="text-align:left">1 次（分层编码）</td></tr><tr><td style="text-align:left">CPU 消耗</td><td style="text-align:left">高</td><td style="text-align:left">低</td></tr><tr><td style="text-align:left">SFU 复杂度</td><td style="text-align:left">按 SSRC 选层</td><td style="text-align:left">按 temporal/spatial layer 选层</td></tr><tr><td style="text-align:left">兼容性</td><td style="text-align:left">全平台</td><td style="text-align:left">需要 Codec 支持 SVC（VP9/AV1）</td></tr><tr><td style="text-align:left">灵活性</td><td style="text-align:left">每层独立参数</td><td style="text-align:left">层间有依赖关系</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五码率控制">五、码率控制<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#%E4%BA%94%E7%A0%81%E7%8E%87%E6%8E%A7%E5%88%B6" class="hash-link" aria-label="Direct link to 五、码率控制" title="Direct link to 五、码率控制" translate="no">​</a></h2>
<p>WebRTC 的码率控制是多层协作的：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-应用层码率限制">5.1 应用层码率限制<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#51-%E5%BA%94%E7%94%A8%E5%B1%82%E7%A0%81%E7%8E%87%E9%99%90%E5%88%B6" class="hash-link" aria-label="Direct link to 5.1 应用层码率限制" title="Direct link to 5.1 应用层码率限制" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sender </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getSenders</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">s</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">track</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">kind </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> params </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 限制所有层</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">encodings</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">enc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  enc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">maxBitrate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">500_000</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-gcc-自适应">5.2 GCC 自适应<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#52-gcc-%E8%87%AA%E9%80%82%E5%BA%94" class="hash-link" aria-label="Direct link to 5.2 GCC 自适应" title="Direct link to 5.2 GCC 自适应" translate="no">​</a></h3>
<p>GCC（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">Ch10</a>）会在 <code>maxBitrate</code> 之下进一步自适应：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">实际发送码率 = min(maxBitrate, GCC估计带宽, CPU允许码率)</span><br></div></code></pre></div></div></div>
<p><code>getStats()</code> 中 <code>qualityLimitationReason</code> 会告诉你瓶颈在哪：</p>
<table><thead><tr><th style="text-align:left">值</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left"><code>bandwidth</code></td><td style="text-align:left">GCC 降低了码率</td></tr><tr><td style="text-align:left"><code>cpu</code></td><td style="text-align:left">编码器跟不上，主动降质</td></tr><tr><td style="text-align:left"><code>none</code></td><td style="text-align:left">无限制</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="53-初始码率与快速启动">5.3 初始码率与快速启动<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#53-%E5%88%9D%E5%A7%8B%E7%A0%81%E7%8E%87%E4%B8%8E%E5%BF%AB%E9%80%9F%E5%90%AF%E5%8A%A8" class="hash-link" aria-label="Direct link to 5.3 初始码率与快速启动" title="Direct link to 5.3 初始码率与快速启动" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 部分浏览器支持 degradationPreference</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> params </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">degradationPreference</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"maintain-framerate"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 其他选项: "maintain-resolution" | "balanced"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setParameters</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">params</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">degradationPreference</th><th style="text-align:left">行为</th></tr></thead><tbody><tr><td style="text-align:left"><code>maintain-framerate</code></td><td style="text-align:left">带宽不足时降分辨率，保帧率</td></tr><tr><td style="text-align:left"><code>maintain-resolution</code></td><td style="text-align:left">带宽不足时降帧率，保分辨率</td></tr><tr><td style="text-align:left"><code>balanced</code></td><td style="text-align:left">帧率和分辨率等比降低</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六编解码器协商过程">六、编解码器协商过程<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#%E5%85%AD%E7%BC%96%E8%A7%A3%E7%A0%81%E5%99%A8%E5%8D%8F%E5%95%86%E8%BF%87%E7%A8%8B" class="hash-link" aria-label="Direct link to 六、编解码器协商过程" title="Direct link to 六、编解码器协商过程" translate="no">​</a></h2>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="61-查看浏览器能力">6.1 查看浏览器能力<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#61-%E6%9F%A5%E7%9C%8B%E6%B5%8F%E8%A7%88%E5%99%A8%E8%83%BD%E5%8A%9B" class="hash-link" aria-label="Direct link to 6.1 查看浏览器能力" title="Direct link to 6.1 查看浏览器能力" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> audioCodecs </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token maybe-class-name">RTCRtpSender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getCapabilities</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"audio"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">codecs</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> videoCodecs </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token maybe-class-name">RTCRtpSender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getCapabilities</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">codecs</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Audio:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> audioCodecs</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">c</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mimeType</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Video:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> videoCodecs</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">c</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">c</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">mimeType</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c"> pt=</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">c</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">preferredPayloadType</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c"> hw=</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">c</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">hardwareAccelerated</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="62-强制特定-codec">6.2 强制特定 Codec<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#62-%E5%BC%BA%E5%88%B6%E7%89%B9%E5%AE%9A-codec" class="hash-link" aria-label="Direct link to 6.2 强制特定 Codec" title="Direct link to 6.2 强制特定 Codec" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> transceiver </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTransceiver</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">direction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"sendrecv"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> caps </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token maybe-class-name">RTCRtpSender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getCapabilities</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 只保留 VP8</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> vp8Only </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> caps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">codecs</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">c</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mimeType</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video/VP8"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">transceiver</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setCodecPreferences</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">vp8Only</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七常见问题与排查">七、常见问题与排查<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#%E4%B8%83%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E4%B8%8E%E6%8E%92%E6%9F%A5" class="hash-link" aria-label="Direct link to 七、常见问题与排查" title="Direct link to 七、常见问题与排查" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">问题</th><th style="text-align:left">原因</th><th style="text-align:left">解决</th></tr></thead><tbody><tr><td style="text-align:left">无 Simulcast 层</td><td style="text-align:left">未用 Unified Plan + sendEncodings</td><td style="text-align:left">用 <code>addTransceiver</code> + <code>rid</code></td></tr><tr><td style="text-align:left">SDP 无 <code>a=simulcast</code></td><td style="text-align:left">浏览器不支持或只有 1 层</td><td style="text-align:left">确认 Chrome 72+ / Firefox 66+</td></tr><tr><td style="text-align:left">H.264 协商失败</td><td style="text-align:left">profile-level-id 不匹配</td><td style="text-align:left">检查 fmtp 参数兼容性</td></tr><tr><td style="text-align:left">画质模糊</td><td style="text-align:left">码率过低或 scaleResolutionDownBy 过大</td><td style="text-align:left">提高 maxBitrate</td></tr><tr><td style="text-align:left">编码 CPU 过高</td><td style="text-align:left">Simulcast 3 层 + 软编</td><td style="text-align:left">减少层数或启用硬编</td></tr><tr><td style="text-align:left">AV1 不生效</td><td style="text-align:left">浏览器/设备不支持</td><td style="text-align:left">回退 VP9/VP8</td></tr><tr><td style="text-align:left">音频断断续续</td><td style="text-align:left">Opus 码率过低</td><td style="text-align:left">提高 maxBitrate 到 32kbps+</td></tr><tr><td style="text-align:left">setParameters 失败</td><td style="text-align:left">参数不合法或顺序错误</td><td style="text-align:left">先 getParameters 再修改</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="71-simulcast-不生效排查">7.1 Simulcast 不生效排查<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#71-simulcast-%E4%B8%8D%E7%94%9F%E6%95%88%E6%8E%92%E6%9F%A5" class="hash-link" aria-label="Direct link to 7.1 Simulcast 不生效排查" title="Direct link to 7.1 Simulcast 不生效排查" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 1. 确认 SDP 中有 simulcast 属性</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">includes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"simulcast"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 2. 确认有 3 个 outbound-rtp</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> outboundCount </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"outbound-rtp"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> outboundCount</span><span class="token operator" style="color:#393A34">++</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Outbound video streams:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> outboundCount</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 应为 3</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 3. 确认 rid 存在</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"outbound-rtp"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"rid:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">rid</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ssrc:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ssrc</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"bitrate:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">targetBitrate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八实战-lab">八、实战 Lab<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#%E5%85%AB%E5%AE%9E%E6%88%98-lab" class="hash-link" aria-label="Direct link to 八、实战 Lab" title="Direct link to 八、实战 Lab" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-1codec-能力探测">Lab 1：Codec 能力探测<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#lab-1codec-%E8%83%BD%E5%8A%9B%E6%8E%A2%E6%B5%8B" class="hash-link" aria-label="Direct link to Lab 1：Codec 能力探测" title="Direct link to Lab 1：Codec 能力探测" translate="no">​</a></h3>
<ol>
<li class="">在控制台运行 <code>RTCRtpSender.getCapabilities("video")</code></li>
<li class="">列出所有支持的 mimeType 和 hardwareAccelerated 标志</li>
<li class="">对比 Chrome 桌面 vs Chrome Android 的差异</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-2simulcast-三档验证">Lab 2：Simulcast 三档验证<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#lab-2simulcast-%E4%B8%89%E6%A1%A3%E9%AA%8C%E8%AF%81" class="hash-link" aria-label="Direct link to Lab 2：Simulcast 三档验证" title="Direct link to Lab 2：Simulcast 三档验证" translate="no">​</a></h3>
<ol>
<li class="">用 <code>addTransceiver</code> 配置 h/m/l 三档</li>
<li class=""><code>createOffer()</code> 后搜索 SDP 中的 <code>a=simulcast</code> 和 <code>a=rid</code></li>
<li class=""><code>getStats()</code> 确认 3 个 <code>outbound-rtp</code> 报告，各有不同 rid 和 SSRC</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-3带宽降层观察">Lab 3：带宽降层观察<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#lab-3%E5%B8%A6%E5%AE%BD%E9%99%8D%E5%B1%82%E8%A7%82%E5%AF%9F" class="hash-link" aria-label="Direct link to Lab 3：带宽降层观察" title="Direct link to Lab 3：带宽降层观察" translate="no">​</a></h3>
<ol>
<li class="">建立 Simulcast 通话</li>
<li class="">Chrome DevTools 限速 300 kbps</li>
<li class="">观察 SFU 或 P2P 对端收到的层从 h → m → l 切换</li>
<li class="">记录切换时 <code>inbound-rtp</code> 的 SSRC 变化</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-4vp8-vs-h264-画质对比">Lab 4：VP8 vs H.264 画质对比<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#lab-4vp8-vs-h264-%E7%94%BB%E8%B4%A8%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to Lab 4：VP8 vs H.264 画质对比" title="Direct link to Lab 4：VP8 vs H.264 画质对比" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 测试 1：强制 VP8</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">transceiver</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setCodecPreferences</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  caps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">codecs</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">c</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mimeType</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video/VP8"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 测试 2：强制 H.264</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">transceiver</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setCodecPreferences</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  caps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">codecs</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">c</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mimeType</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video/H264"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 在相同 maxBitrate 下对比主观画质和 CPU 占用</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-5动态层管理">Lab 5：动态层管理<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#lab-5%E5%8A%A8%E6%80%81%E5%B1%82%E7%AE%A1%E7%90%86" class="hash-link" aria-label="Direct link to Lab 5：动态层管理" title="Direct link to Lab 5：动态层管理" translate="no">​</a></h3>
<ol>
<li class="">开启 3 层 Simulcast</li>
<li class="">30 秒后关闭 l 层：<code>enc.active = false</code></li>
<li class=""><code>getStats()</code> 确认 outbound-rtp 从 3 个变为 2 个</li>
<li class="">重新开启 l 层，确认恢复</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-6opus-码率与质量">Lab 6：Opus 码率与质量<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#lab-6opus-%E7%A0%81%E7%8E%87%E4%B8%8E%E8%B4%A8%E9%87%8F" class="hash-link" aria-label="Direct link to Lab 6：Opus 码率与质量" title="Direct link to Lab 6：Opus 码率与质量" translate="no">​</a></h3>
<ol>
<li class="">分别设置 maxBitrate 为 16k / 32k / 64k</li>
<li class="">播放音乐片段，记录主观质量差异</li>
<li class="">启用 <code>useinbandfec=1</code>，10% 丢包下对比有无 FEC 的 <code>concealedSamples</code></li>
</ol>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九本章小结">九、本章小结<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#%E4%B9%9D%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="Direct link to 九、本章小结" title="Direct link to 九、本章小结" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">要点</th><th style="text-align:left">内容</th></tr></thead><tbody><tr><td style="text-align:left">音频</td><td style="text-align:left">Opus 是唯一标准，注意 FEC/DTX/码率配置</td></tr><tr><td style="text-align:left">视频</td><td style="text-align:left">VP8 默认，H.264 移动端，VP9/AV1 高压缩</td></tr><tr><td style="text-align:left">Simulcast</td><td style="text-align:left">rid + sendEncodings，每层独立 SSRC</td></tr><tr><td style="text-align:left">码率</td><td style="text-align:left">应用层 maxBitrate + GCC 自适应</td></tr><tr><td style="text-align:left">选型</td><td style="text-align:left">兼容性 &gt; 压缩率 &gt; 专利自由度</td></tr></tbody></table>
<p>从 Ron Frederick 1992 年手写软件视频压缩，到 Skype 团队标准化 Opus，再到今天 AV1 的硬件加速普及——编解码技术的演进直接塑造了 WebRTC 的能力边界。理解 Codec 特性，才能在延迟、画质、兼容性之间做出正确权衡。</p>
<p><strong>下一篇（Ch10）</strong>：<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与 GCC</a>——TWCC 反馈如何驱动发送端码率自适应。</p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc7587" target="_blank" rel="noopener noreferrer" class="">RFC 7587 — RTP Payload Format for Opus</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc7741" target="_blank" rel="noopener noreferrer" class="">RFC 7741 — RTP Payload Format for VP8</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc6184" target="_blank" rel="noopener noreferrer" class="">RFC 6184 — RTP Payload Format for H.264</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8871" target="_blank" rel="noopener noreferrer" class="">RFC 8871 — WebRTC Video Processing and Codec Requirements</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc6381" target="_blank" rel="noopener noreferrer" class="">RFC 6381 — Opus 首版</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — RTP 历史 / Opus / Skype</a></li>
<li class=""><a href="https://webrtchacks.com/simulcast/" target="_blank" rel="noopener noreferrer" class="">webrtcH4cKS — An Intro to Simulcast</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSender/setParameters" target="_blank" rel="noopener noreferrer" class="">MDN — RTCRtpSender.setParameters()</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpTransceiver" target="_blank" rel="noopener noreferrer" class="">MDN — RTCRtpTransceiver</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>实时通信</category>
            <category>Deep Dive</category>
            <category>SFU</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (8)：RTP/RTCP 媒体传输与 QoS]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp</guid>
            <pubDate>Fri, 19 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[RTP 包头结构、RTCP 反馈机制、Jitter Buffer、NACK/FEC 与 getStats API 解读]]></description>
            <content:encoded><![CDATA[<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">Ch7 DTLS/SRTP</a> 建立加密通道后，音频和视频以 <strong>RTP 包</strong>在 UDP 上传输。<a href="https://datatracker.ietf.org/doc/html/rfc3550" target="_blank" rel="noopener noreferrer" class="">RFC 3550</a> 定义了 RTP/RTCP——它的历史可追溯到 1992 年 Ron Frederick 的 <strong>nv</strong> 工具与 MBONE 多播实验（详见 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious — RTP 历史</a>）。</p>
<p>Frederick 在访谈中说：「如果一个东西需要实时数据流，又无需精确时序传输，它就可以从 RTP 中受益。」他最初为 MBONE 多播场景设计 RTP，后来 RTP 成为互联网实时音视频的事实标准。WebRTC 在 RTP 之上叠加了 SRTP 加密、AVPF 反馈 profile 和 TWCC 拥塞控制，但 RTP 包头结构与核心语义 thirty 年来未曾改变。</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>设计哲学</div><div class="admonitionContent_BuS1"><p>RTP 不要求数据按精确时序到达——它面向<strong>实时</strong>而非<strong>可靠</strong>。丢包用 FEC/NACK/PLC 补偿，而非 TCP 式重传阻塞。这正是 Ron Frederick 所说「如果一个东西需要实时数据流，又无需精确时序传输，它就可以从 RTP 中受益」。</p></div></div>
<p>本章覆盖 <a href="https://datatracker.ietf.org/doc/html/rfc3550" target="_blank" rel="noopener noreferrer" class="">RFC 3550</a> RTP 包头、RTCP SR/RR/NACK/PLI/TWCC、Jitter Buffer、FEC 策略，以及 <code>getStats()</code> 诊断实战。</p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="零ron-frederick-与-rtp-的诞生">零、Ron Frederick 与 RTP 的诞生<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#%E9%9B%B6ron-frederick-%E4%B8%8E-rtp-%E7%9A%84%E8%AF%9E%E7%94%9F" class="hash-link" aria-label="Direct link to 零、Ron Frederick 与 RTP 的诞生" title="Direct link to 零、Ron Frederick 与 RTP 的诞生" translate="no">​</a></h2>
<p><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史</a> 对 Ron Frederick 有一段专门访谈。1992 年，Frederick 在 Xerox PARC 为 <strong>nv</strong>（network video）工具开发传输层——当时 MBONE（Multicast Backbone）让研究者能在互联网上做多播实验，但缺少统一的实时包格式。</p>
<table><thead><tr><th style="text-align:left">年代</th><th style="text-align:left">事件</th><th style="text-align:left">意义</th></tr></thead><tbody><tr><td style="text-align:left">1992</td><td style="text-align:left">Frederick 发布 <strong>RTP/RTCP</strong> 草案</td><td style="text-align:left">为多播音视频定义统一容器</td></tr><tr><td style="text-align:left">1992</td><td style="text-align:left">手写软件视频压缩（nv 编解码）</td><td style="text-align:left">MPEG-1 无法实时，催生轻量压缩思路</td></tr><tr><td style="text-align:left">1996</td><td style="text-align:left">RTP 成为 IETF <strong>RFC 1889</strong>（后修订为 RFC 3550）</td><td style="text-align:left">从实验走向标准</td></tr><tr><td style="text-align:left">2000s</td><td style="text-align:left">单播 VoIP 取代 MBONE 多播</td><td style="text-align:left">RTP 适配单播 + RTCP 反馈</td></tr><tr><td style="text-align:left">2011+</td><td style="text-align:left">WebRTC 标准化</td><td style="text-align:left">SRTP + AVPF + TWCC 叠加在 RTP 之上</td></tr></tbody></table>
<p>Frederick 本人承认 RTCP 在简单单播场景显得「过重」——周期性 SR/RR 占用带宽，反馈类型不断膨胀（NACK、PLI、TWCC…）。但 WebRTC 恰恰证明：<strong>没有 RTCP 反馈，GCC 拥塞控制和丢包恢复都无法工作</strong>。RTP 的 12 字节头保持了极简；复杂性被有意推到了 RTCP 控制面。</p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>RTP</strong></td><td style="text-align:left">Real-time Transport Protocol</td><td style="text-align:left">实时传输协议，<a href="https://datatracker.ietf.org/doc/html/rfc3550" target="_blank" rel="noopener noreferrer" class="">RFC 3550</a></td></tr><tr><td style="text-align:left"><strong>RTCP</strong></td><td style="text-align:left">RTP Control Protocol</td><td style="text-align:left">RTP 的控制面，传输统计与反馈</td></tr><tr><td style="text-align:left"><strong>SSRC</strong></td><td style="text-align:left">Synchronization Source</td><td style="text-align:left">32 bit 同步源标识符，标识一个 RTP 流</td></tr><tr><td style="text-align:left"><strong>CSRC</strong></td><td style="text-align:left">Contributing Source</td><td style="text-align:left">混音/混流中的贡献源列表</td></tr><tr><td style="text-align:left"><strong>PT</strong></td><td style="text-align:left">Payload Type</td><td style="text-align:left">7 bit 载荷类型，映射到编解码器</td></tr><tr><td style="text-align:left"><strong>Sequence Number</strong></td><td style="text-align:left">—</td><td style="text-align:left">16 bit 序号，检测丢包与乱序</td></tr><tr><td style="text-align:left"><strong>Timestamp</strong></td><td style="text-align:left">—</td><td style="text-align:left">32 bit 时间戳，播放时序基准</td></tr><tr><td style="text-align:left"><strong>M bit</strong></td><td style="text-align:left">Marker Bit</td><td style="text-align:left">标记位，常标识视频帧边界</td></tr><tr><td style="text-align:left"><strong>SR</strong></td><td style="text-align:left">Sender Report</td><td style="text-align:left">RTCP 发送端报告，含发包数与时间戳</td></tr><tr><td style="text-align:left"><strong>RR</strong></td><td style="text-align:left">Receiver Report</td><td style="text-align:left">RTCP 接收端报告，含丢包率与 jitter</td></tr><tr><td style="text-align:left"><strong>NACK</strong></td><td style="text-align:left">Negative Acknowledgment</td><td style="text-align:left">请求重传丢失的 RTP 包</td></tr><tr><td style="text-align:left"><strong>PLI</strong></td><td style="text-align:left">Picture Loss Indication</td><td style="text-align:left">请求发送关键帧（I 帧）</td></tr><tr><td style="text-align:left"><strong>FIR</strong></td><td style="text-align:left">Full Intra Request</td><td style="text-align:left">强制完整帧内编码帧</td></tr><tr><td style="text-align:left"><strong>TWCC</strong></td><td style="text-align:left">Transport-wide Congestion Control</td><td style="text-align:left">逐包延迟反馈，驱动 GCC</td></tr><tr><td style="text-align:left"><strong>REMB</strong></td><td style="text-align:left">Receiver Estimated Maximum Bitrate</td><td style="text-align:left">接收端估计最大码率（Legacy）</td></tr><tr><td style="text-align:left"><strong>Jitter Buffer</strong></td><td style="text-align:left">—</td><td style="text-align:left">接收端缓冲，平滑网络抖动</td></tr><tr><td style="text-align:left"><strong>FEC</strong></td><td style="text-align:left">Forward Error Correction</td><td style="text-align:left">前向纠错，冗余包恢复丢失数据</td></tr><tr><td style="text-align:left"><strong>RTX</strong></td><td style="text-align:left">Retransmission</td><td style="text-align:left">重传流，独立 SSRC 承载重传包</td></tr><tr><td style="text-align:left"><strong>PLC</strong></td><td style="text-align:left">Packet Loss Concealment</td><td style="text-align:left">丢包隐藏，音频猜测填充</td></tr><tr><td style="text-align:left"><strong>Compound RTCP</strong></td><td style="text-align:left">—</td><td style="text-align:left">多个 RTCP 包合并在一个 UDP 载荷中发送</td></tr><tr><td style="text-align:left"><strong>ROC</strong></td><td style="text-align:left">Rollover Counter</td><td style="text-align:left">SRTP 内部序号扩展，见 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">Ch7</a></td></tr><tr><td style="text-align:left"><strong>MID</strong></td><td style="text-align:left">Media Identification</td><td style="text-align:left">BUNDLE 下区分 m-line 的 RTP 扩展</td></tr><tr><td style="text-align:left"><strong>abs-send-time</strong></td><td style="text-align:left">—</td><td style="text-align:left">绝对发送时间扩展，辅助带宽估计</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一rtp-在-webrtc-媒体路径中的位置">一、RTP 在 WebRTC 媒体路径中的位置<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#%E4%B8%80rtp-%E5%9C%A8-webrtc-%E5%AA%92%E4%BD%93%E8%B7%AF%E5%BE%84%E4%B8%AD%E7%9A%84%E4%BD%8D%E7%BD%AE" class="hash-link" aria-label="Direct link to 一、RTP 在 WebRTC 媒体路径中的位置" title="Direct link to 一、RTP 在 WebRTC 媒体路径中的位置" translate="no">​</a></h2>
<!-- -->
<p>RTP 位于编解码器与 SRTP 之间，是<strong>媒体数据的容器格式</strong>。它不关心载荷是 Opus 还是 VP8——只负责打上序号、时间戳和 SSRC，让接收端能正确排序、检测丢包、同步播放。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="11-rtp-的设计目标rfc-3550">1.1 RTP 的设计目标（RFC 3550）<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#11-rtp-%E7%9A%84%E8%AE%BE%E8%AE%A1%E7%9B%AE%E6%A0%87rfc-3550" class="hash-link" aria-label="Direct link to 1.1 RTP 的设计目标（RFC 3550）" title="Direct link to 1.1 RTP 的设计目标（RFC 3550）" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">目标</th><th style="text-align:left">实现方式</th></tr></thead><tbody><tr><td style="text-align:left">实时性</td><td style="text-align:left">UDP 传输，无重传阻塞</td></tr><tr><td style="text-align:left">多流复用</td><td style="text-align:left">SSRC 区分不同源</td></tr><tr><td style="text-align:left">时序恢复</td><td style="text-align:left">Timestamp + Sequence Number</td></tr><tr><td style="text-align:left">质量监控</td><td style="text-align:left">RTCP 周期性报告</td></tr><tr><td style="text-align:left">可扩展</td><td style="text-align:left">RTP Header Extension</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="12-与-rtcp-的分工">1.2 与 RTCP 的分工<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#12-%E4%B8%8E-rtcp-%E7%9A%84%E5%88%86%E5%B7%A5" class="hash-link" aria-label="Direct link to 1.2 与 RTCP 的分工" title="Direct link to 1.2 与 RTCP 的分工" translate="no">​</a></h3>
<p>RTP 只负责「送数据」，<strong>RTCP（RTP Control Protocol）</strong> 负责反馈网络质量。WebRTC 中 RTCP 与 RTP 共用 DTLS 通道（<code>a=rtcp-mux</code>），避免额外端口。Ron Frederick 在访谈中也提到 RTCP 复杂度是 RTP 在单播场景被诟病的原因之一——但 WebRTC 恰恰依赖这些反馈实现自适应码率。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二rtp-包头结构rfc-3550-section-51">二、RTP 包头结构（RFC 3550 Section 5.1）<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#%E4%BA%8Crtp-%E5%8C%85%E5%A4%B4%E7%BB%93%E6%9E%84rfc-3550-section-51" class="hash-link" aria-label="Direct link to 二、RTP 包头结构（RFC 3550 Section 5.1）" title="Direct link to 二、RTP 包头结构（RFC 3550 Section 5.1）" translate="no">​</a></h2>
<p>RTP 固定头至少 12 字节，后跟可选的 CSRC 列表和 Header Extension。</p>
<!-- -->
<p><code>CC &gt; 0</code> 时紧跟 <code>CC × 32 bit</code> 的 CSRC 列表；<code>X=1</code> 时再跟 Header Extension。完整位域定义见下表。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-固定头字段详解">2.1 固定头字段详解<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#21-%E5%9B%BA%E5%AE%9A%E5%A4%B4%E5%AD%97%E6%AE%B5%E8%AF%A6%E8%A7%A3" class="hash-link" aria-label="Direct link to 2.1 固定头字段详解" title="Direct link to 2.1 固定头字段详解" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">位数</th><th style="text-align:left">作用</th><th style="text-align:left">调试价值</th></tr></thead><tbody><tr><td style="text-align:left"><strong>V (Version)</strong></td><td style="text-align:left">2</td><td style="text-align:left">固定为 2</td><td style="text-align:left">—</td></tr><tr><td style="text-align:left"><strong>P (Padding)</strong></td><td style="text-align:left">1</td><td style="text-align:left">是否有尾部填充</td><td style="text-align:left">SRTP 加密后可能填充</td></tr><tr><td style="text-align:left"><strong>X (Extension)</strong></td><td style="text-align:left">1</td><td style="text-align:left">是否有 Header Extension</td><td style="text-align:left">TWCC、abs-send-time 等</td></tr><tr><td style="text-align:left"><strong>CC (CSRC Count)</strong></td><td style="text-align:left">4</td><td style="text-align:left">CSRC 标识符数量</td><td style="text-align:left">混音场景</td></tr><tr><td style="text-align:left"><strong>M (Marker)</strong></td><td style="text-align:left">1</td><td style="text-align:left">标记位</td><td style="text-align:left">视频帧最后一包 M=1</td></tr><tr><td style="text-align:left"><strong>PT (Payload Type)</strong></td><td style="text-align:left">7</td><td style="text-align:left">载荷类型</td><td style="text-align:left">PT=111 → Opus</td></tr><tr><td style="text-align:left"><strong>Sequence Number</strong></td><td style="text-align:left">16</td><td style="text-align:left">循环序号</td><td style="text-align:left"><code>packetsLost</code> 统计</td></tr><tr><td style="text-align:left"><strong>Timestamp</strong></td><td style="text-align:left">32</td><td style="text-align:left">播放时序基准</td><td style="text-align:left">音视频同步</td></tr><tr><td style="text-align:left"><strong>SSRC</strong></td><td style="text-align:left">32</td><td style="text-align:left">同步源标识</td><td style="text-align:left">Simulcast 每层不同 SSRC</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="22-timestamp-时钟频率">2.2 Timestamp 时钟频率<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#22-timestamp-%E6%97%B6%E9%92%9F%E9%A2%91%E7%8E%87" class="hash-link" aria-label="Direct link to 2.2 Timestamp 时钟频率" title="Direct link to 2.2 Timestamp 时钟频率" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">媒体</th><th style="text-align:left">时钟频率</th><th style="text-align:left">示例</th></tr></thead><tbody><tr><td style="text-align:left">音频 Opus</td><td style="text-align:left">48,000 Hz</td><td style="text-align:left">20ms 帧 = timestamp += 960</td></tr><tr><td style="text-align:left">视频 VP8/H264</td><td style="text-align:left">90,000 Hz</td><td style="text-align:left">30fps 帧 = timestamp += 3000</td></tr><tr><td style="text-align:left">RTX 重传</td><td style="text-align:left">与主 PT 相同</td><td style="text-align:left">复用主时钟</td></tr></tbody></table>
<p>Timestamp 的<strong>绝对值</strong>无意义，接收端只关心<strong>增量</strong>。音视频同步通过 RTCP SR 中的 NTP 时间戳与 RTP 时间戳的映射实现。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="23-sequence-number-与丢包检测">2.3 Sequence Number 与丢包检测<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#23-sequence-number-%E4%B8%8E%E4%B8%A2%E5%8C%85%E6%A3%80%E6%B5%8B" class="hash-link" aria-label="Direct link to 2.3 Sequence Number 与丢包检测" title="Direct link to 2.3 Sequence Number 与丢包检测" translate="no">​</a></h3>
<p>Seq 16 bit，循环 0→65535→0。接收端维护 <code>expected_seq</code>：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">if (received_seq == expected_seq):</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    正常，expected_seq++</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">elif (received_seq &gt; expected_seq):</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    检测到丢包，gap = received_seq - expected_seq</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    packetsLost += gap</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    expected_seq = received_seq + 1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">else:</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    乱序或重传，放入 Jitter Buffer 重排</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="24-ssrc-与-simulcast">2.4 SSRC 与 Simulcast<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#24-ssrc-%E4%B8%8E-simulcast" class="hash-link" aria-label="Direct link to 2.4 SSRC 与 Simulcast" title="Direct link to 2.4 SSRC 与 Simulcast" translate="no">​</a></h3>
<p>每个 RTP 流有唯一 SSRC。Simulcast 三档（h/m/l）各自分配不同 SSRC，SFU 按 SSRC 区分层。<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">Ch9</a> 详解 rid 与 SSRC 的映射。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="25-rtp-header-extension">2.5 RTP Header Extension<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#25-rtp-header-extension" class="hash-link" aria-label="Direct link to 2.5 RTP Header Extension" title="Direct link to 2.5 RTP Header Extension" translate="no">​</a></h3>
<p>WebRTC 中常见的扩展（通过 SDP <code>a=extmap</code> 协商）：</p>
<table><thead><tr><th style="text-align:left">extmap ID</th><th style="text-align:left">URI</th><th style="text-align:left">用途</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left"><code>ssrc-audio-level</code></td><td style="text-align:left">音频电平，用于说话人检测</td></tr><tr><td style="text-align:left">2</td><td style="text-align:left"><code>abs-send-time</code></td><td style="text-align:left">绝对发送时间，带宽估计</td></tr><tr><td style="text-align:left">3</td><td style="text-align:left"><code>transport-wide-cc</code></td><td style="text-align:left">TWCC 序列号</td></tr><tr><td style="text-align:left">4</td><td style="text-align:left"><code>mid</code></td><td style="text-align:left">媒体 ID，BUNDLE 复用区分</td></tr><tr><td style="text-align:left">5</td><td style="text-align:left"><code>rtp-stream-id</code></td><td style="text-align:left">Simulcast rid</td></tr></tbody></table>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="26-bundlemid-与-ssrc-映射">2.6 BUNDLE、MID 与 SSRC 映射<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#26-bundlemid-%E4%B8%8E-ssrc-%E6%98%A0%E5%B0%84" class="hash-link" aria-label="Direct link to 2.6 BUNDLE、MID 与 SSRC 映射" title="Direct link to 2.6 BUNDLE、MID 与 SSRC 映射" translate="no">​</a></h3>
<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">Ch4 SDP</a> 中 <code>a=group:BUNDLE</code> 将 audio/video 复用到同一 UDP 五元组。此时仅靠 PT 无法区分流——需要 <strong>MID</strong> Header Extension：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=group:BUNDLE 0 1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=mid:0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=mid:1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid</span><br></div></code></pre></div></div></div>
<!-- -->
<p>Simulcast 场景下，同一 MID 内可能有 3 个 SSRC（h/m/l），通过 <code>rtp-stream-id</code> 扩展区分。SFU 路由逻辑：<strong>MID 选 m-line → rid/SSRC 选 Simulcast 层</strong>。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="27-marker-bit-与视频帧边界">2.7 Marker Bit 与视频帧边界<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#27-marker-bit-%E4%B8%8E%E8%A7%86%E9%A2%91%E5%B8%A7%E8%BE%B9%E7%95%8C" class="hash-link" aria-label="Direct link to 2.7 Marker Bit 与视频帧边界" title="Direct link to 2.7 Marker Bit 与视频帧边界" translate="no">​</a></h3>
<p>视频编解码器将一帧拆成多个 RTP 包时，<strong>最后一包的 M bit = 1</strong>，接收端据此判断帧边界。Jitter Buffer 和解码器依赖 M bit 触发解码——若发送端错误设置 M bit，会出现「解码器等待永不结束」或「提前解码半帧」的花屏。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三rtcprtp-的控制面">三、RTCP：RTP 的控制面<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#%E4%B8%89rtcprtp-%E7%9A%84%E6%8E%A7%E5%88%B6%E9%9D%A2" class="hash-link" aria-label="Direct link to 三、RTCP：RTP 的控制面" title="Direct link to 三、RTCP：RTP 的控制面" translate="no">​</a></h2>
<p>RTCP 包类型在 200-207 范围（与 RTP PT 0-127 区分）。所有 RTCP 包共享 32 bit 公共头（<a href="https://datatracker.ietf.org/doc/html/rfc3550#section-6.1" target="_blank" rel="noopener noreferrer" class="">RFC 3550 §6.1</a>）：</p>
<!-- -->
<p>WebRTC 主要使用以下 RTCP 类型：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-sender-report-sr--pt-200">3.1 Sender Report (SR) — PT 200<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#31-sender-report-sr--pt-200" class="hash-link" aria-label="Direct link to 3.1 Sender Report (SR) — PT 200" title="Direct link to 3.1 Sender Report (SR) — PT 200" translate="no">​</a></h3>
<p>发送端周期性（通常 1 秒）发送 SR，包含：</p>
<table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left">NTP Timestamp</td><td style="text-align:left">发送时的 NTP 绝对时间</td></tr><tr><td style="text-align:left">RTP Timestamp</td><td style="text-align:left">对应的 RTP 时间戳</td></tr><tr><td style="text-align:left">Sender's Packet Count</td><td style="text-align:left">累计发送 RTP 包数</td></tr><tr><td style="text-align:left">Sender's Octet Count</td><td style="text-align:left">累计发送字节数</td></tr></tbody></table>
<p>SR 使接收端能将 RTP timestamp 映射到 wall-clock time，是<strong>音视频同步</strong>的基础。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-receiver-report-rr--pt-201">3.2 Receiver Report (RR) — PT 201<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#32-receiver-report-rr--pt-201" class="hash-link" aria-label="Direct link to 3.2 Receiver Report (RR) — PT 201" title="Direct link to 3.2 Receiver Report (RR) — PT 201" translate="no">​</a></h3>
<p>接收端周期性发送 RR，每个被接收的 SSRC 一条报告块（24 字节 / SSRC）：</p>
<!-- -->
<table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left">Fraction Lost</td><td style="text-align:left">自上次报告以来的丢包率（8 bit 分数）</td></tr><tr><td style="text-align:left">Cumulative Packets Lost</td><td style="text-align:left">累计丢包数（24 bit 有符号）</td></tr><tr><td style="text-align:left">Extended Highest Seq</td><td style="text-align:left">收到的最高序号</td></tr><tr><td style="text-align:left">Interarrival Jitter</td><td style="text-align:left">到达间隔抖动估计</td></tr><tr><td style="text-align:left">Last SR Timestamp (LSR)</td><td style="text-align:left">最近收到的 SR 的 NTP 中间 32 bit</td></tr><tr><td style="text-align:left">Delay Since Last SR (DLSR)</td><td style="text-align:left">自收到 SR 以来的延迟</td></tr></tbody></table>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="33-反馈消息rfc-4585-avpf">3.3 反馈消息（RFC 4585 AVPF）<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#33-%E5%8F%8D%E9%A6%88%E6%B6%88%E6%81%AFrfc-4585-avpf" class="hash-link" aria-label="Direct link to 3.3 反馈消息（RFC 4585 AVPF）" title="Direct link to 3.3 反馈消息（RFC 4585 AVPF）" translate="no">​</a></h3>
<p>WebRTC 使用 <strong>AVPF</strong>（Audio-Visual Profile with Feedback）profile，允许 RTCP 反馈消息在检测到事件后<strong>立即发送</strong>（而非等下一个 SR/RR 周期）。</p>
<table><thead><tr><th style="text-align:left">反馈类型</th><th style="text-align:left">SDP 中的 rtcp-fb</th><th style="text-align:left">FMT</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:left"><strong>NACK</strong></td><td style="text-align:left"><code>nack</code></td><td style="text-align:left">1</td><td style="text-align:left">请求重传丢失的 RTP 包</td></tr><tr><td style="text-align:left"><strong>PLI</strong></td><td style="text-align:left"><code>nack pli</code></td><td style="text-align:left">1</td><td style="text-align:left">Picture Loss Indication，请求关键帧</td></tr><tr><td style="text-align:left"><strong>FIR</strong></td><td style="text-align:left"><code>ccm fir</code></td><td style="text-align:left">4</td><td style="text-align:left">Full Intra Request，强制 I 帧</td></tr><tr><td style="text-align:left"><strong>REMB</strong></td><td style="text-align:left"><code>goog-remb</code></td><td style="text-align:left">—</td><td style="text-align:left">接收端估计最大码率（Legacy）</td></tr><tr><td style="text-align:left"><strong>TWCC</strong></td><td style="text-align:left"><code>transport-cc</code></td><td style="text-align:left">—</td><td style="text-align:left">Transport-wide Congestion Control（现代主流）</td></tr></tbody></table>
<p>这些反馈驱动 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">Ch10 GCC 拥塞控制</a>。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="34-nack-重传机制">3.4 NACK 重传机制<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#34-nack-%E9%87%8D%E4%BC%A0%E6%9C%BA%E5%88%B6" class="hash-link" aria-label="Direct link to 3.4 NACK 重传机制" title="Direct link to 3.4 NACK 重传机制" translate="no">​</a></h3>
<!-- -->
<p>NACK 包体（RFC 4585 Generic NACK FCI）：</p>
<!-- -->
<p>发送端维护一个 <strong>RTX 缓冲区</strong>（通常 1-3 秒），收到 NACK 后从缓冲区取出对应包，通过 <strong>RTX SSRC</strong> 重传。SDP 中 RTX 映射：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:97 rtx/90000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=fmtp:97 apt=96</span><br></div></code></pre></div></div></div>
<p>PT 97 是 RTX 流，关联主 PT 96（VP8）。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="35-pli-与-fir">3.5 PLI 与 FIR<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#35-pli-%E4%B8%8E-fir" class="hash-link" aria-label="Direct link to 3.5 PLI 与 FIR" title="Direct link to 3.5 PLI 与 FIR" translate="no">​</a></h3>
<p>当 NACK 重传来不及（丢包过多或延迟过大），接收端发送 <strong>PLI</strong> 请求发送端立即编码一个<strong>关键帧</strong>（I 帧/IDR）。关键帧不依赖前序帧，可独立解码。</p>
<table><thead><tr><th style="text-align:left">场景</th><th style="text-align:left">触发</th></tr></thead><tbody><tr><td style="text-align:left">新订阅者加入 SFU</td><td style="text-align:left">SFU 向发送端发 PLI</td></tr><tr><td style="text-align:left">连续丢包超过阈值</td><td style="text-align:left">接收端发 PLI</td></tr><tr><td style="text-align:left">解码器报错</td><td style="text-align:left">接收端发 PLI</td></tr></tbody></table>
<p>FIR 类似 PLI 但语义更强——强制完整帧内刷新，常用于切换分辨率层。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="36-twcctransport-wide-congestion-control">3.6 TWCC（Transport-wide Congestion Control）<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#36-twcctransport-wide-congestion-control" class="hash-link" aria-label="Direct link to 3.6 TWCC（Transport-wide Congestion Control）" title="Direct link to 3.6 TWCC（Transport-wide Congestion Control）" translate="no">​</a></h3>
<p>TWCC 是现代 WebRTC 拥塞控制的核心反馈机制。与 per-SSRC 的 RR 不同，TWCC 在 <strong>RTP Header Extension</strong> 中为每个包打上 transport-wide sequence number，接收端回传每个包的到达时间。</p>
<!-- -->
<p>发送端维护一个<strong>延迟梯度估计器</strong>（GCC 算法），根据 TWCC 反馈调整目标码率。详见 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">Ch10</a>。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="37-rtcp-带宽与-compound-packet">3.7 RTCP 带宽与 Compound Packet<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#37-rtcp-%E5%B8%A6%E5%AE%BD%E4%B8%8E-compound-packet" class="hash-link" aria-label="Direct link to 3.7 RTCP 带宽与 Compound Packet" title="Direct link to 3.7 RTCP 带宽与 Compound Packet" translate="no">​</a></h3>
<p>RFC 3550 规定 RTCP 占用会话带宽的 <strong>5%</strong>（至少 16 kbps）。发送端按以下比例分配：</p>
<table><thead><tr><th style="text-align:left">RTCP 类型</th><th style="text-align:left">带宽占比</th></tr></thead><tbody><tr><td style="text-align:left">SR/RR</td><td style="text-align:left">25%</td></tr><tr><td style="text-align:left">SDES（源描述）</td><td style="text-align:left">5%</td></tr><tr><td style="text-align:left">BYE / APP</td><td style="text-align:left">剩余</td></tr></tbody></table>
<p>WebRTC 中多个 RTCP 消息常合并为 <strong>Compound Packet</strong> 一次发送——例如 RR + NACK + TWCC feedback 在同一 UDP 载荷中。Wireshark 解析时需展开 Compound 内的子包。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="38-remb-与-abs-send-timelegacy">3.8 REMB 与 abs-send-time（Legacy）<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#38-remb-%E4%B8%8E-abs-send-timelegacy" class="hash-link" aria-label="Direct link to 3.8 REMB 与 abs-send-time（Legacy）" title="Direct link to 3.8 REMB 与 abs-send-time（Legacy）" translate="no">​</a></h3>
<p>在 TWCC 普及之前，Chrome 使用 <strong>REMB</strong>（Receiver Estimated Maximum Bitrate）和 <strong>abs-send-time</strong> 扩展做带宽估计：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:96 goog-remb</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">机制</th><th style="text-align:left">粒度</th><th style="text-align:left">现状</th></tr></thead><tbody><tr><td style="text-align:left">REMB</td><td style="text-align:left">整会话码率建议</td><td style="text-align:left">Legacy，部分 SFU 仍解析</td></tr><tr><td style="text-align:left">abs-send-time</td><td style="text-align:left">每包发送时间</td><td style="text-align:left">辅助 GCC，逐渐被 TWCC 取代</td></tr><tr><td style="text-align:left">TWCC</td><td style="text-align:left">每包到达时间反馈</td><td style="text-align:left"><strong>现代主流</strong></td></tr></tbody></table>
<p>新项目应只依赖 TWCC；对接旧 SFU 时需确认是否仍发送 REMB。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="39-音视频同步av-sync">3.9 音视频同步（A/V Sync）<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#39-%E9%9F%B3%E8%A7%86%E9%A2%91%E5%90%8C%E6%AD%A5av-sync" class="hash-link" aria-label="Direct link to 3.9 音视频同步（A/V Sync）" title="Direct link to 3.9 音视频同步（A/V Sync）" translate="no">​</a></h3>
<p>SR 中的 NTP Timestamp 与 RTP Timestamp 映射是 lip-sync 的基础：</p>
<!-- -->
<p>接收端分别维护 audio/video 的 RTP→wall-clock 映射，在 Jitter Buffer 输出时对齐。<code>getStats()</code> 中 <code>totalInterFrameDelay</code> 和 <code>jitterBufferDelay</code> 可间接反映 sync 质量——若视频 JB 持续大于音频，会出现「口型不同步」。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四jitter-buffer-工作原理">四、Jitter Buffer 工作原理<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#%E5%9B%9Bjitter-buffer-%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86" class="hash-link" aria-label="Direct link to 四、Jitter Buffer 工作原理" title="Direct link to 四、Jitter Buffer 工作原理" translate="no">​</a></h2>
<p>网络抖动导致 RTP 包到达时间不均匀。Jitter Buffer 在播放前缓冲并重新排序：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-jitter-计算rfc-3550-appendix-a8">4.1 Jitter 计算（RFC 3550 Appendix A.8）<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#41-jitter-%E8%AE%A1%E7%AE%97rfc-3550-appendix-a8" class="hash-link" aria-label="Direct link to 4.1 Jitter 计算（RFC 3550 Appendix A.8）" title="Direct link to 4.1 Jitter 计算（RFC 3550 Appendix A.8）" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">J(i) = J(i-1) + (|D(i-1,i)| - J(i-1)) / 16</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">D(i-1,i) = (arrival_time_i - arrival_time_{i-1}) - (timestamp_i - timestamp_{i-1})</span><br></div></code></pre></div></div></div>
<p>这是一个指数加权移动平均，反映在 RR 报告的 <code>jitter</code> 字段中。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-自适应-jitter-buffer">4.2 自适应 Jitter Buffer<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#42-%E8%87%AA%E9%80%82%E5%BA%94-jitter-buffer" class="hash-link" aria-label="Direct link to 4.2 自适应 Jitter Buffer" title="Direct link to 4.2 自适应 Jitter Buffer" translate="no">​</a></h3>
<p>WebRTC 的 Jitter Buffer 是<strong>自适应</strong>的：</p>
<table><thead><tr><th style="text-align:left">网络状况</th><th style="text-align:left">Buffer 行为</th></tr></thead><tbody><tr><td style="text-align:left">低抖动</td><td style="text-align:left">缩小缓冲 → 降低延迟</td></tr><tr><td style="text-align:left">高抖动</td><td style="text-align:left">扩大缓冲 → 平滑播放</td></tr><tr><td style="text-align:left">持续丢包</td><td style="text-align:left">触发 PLC / 请求关键帧</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="43-延迟权衡">4.3 延迟权衡<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#43-%E5%BB%B6%E8%BF%9F%E6%9D%83%E8%A1%A1" class="hash-link" aria-label="Direct link to 4.3 延迟权衡" title="Direct link to 4.3 延迟权衡" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">端到端延迟 = 采集延迟 + 编码延迟 + 网络延迟 + Jitter Buffer + 解码延迟 + 渲染延迟</span><br></div></code></pre></div></div></div>
<p>Buffer 越大越平滑，但端到端延迟越高——这是实时通信的经典权衡。音频典型缓冲 20-60ms，视频 0-30ms（视频 Jitter Buffer 通常更激进以降低延迟）。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="44-neteq音频">4.4 NetEQ（音频）<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#44-neteq%E9%9F%B3%E9%A2%91" class="hash-link" aria-label="Direct link to 4.4 NetEQ（音频）" title="Direct link to 4.4 NetEQ（音频）" translate="no">​</a></h3>
<p>WebRTC 音频使用 <strong>NetEQ</strong> 作为 Jitter Buffer 实现，集成了：</p>
<ul>
<li class="">自适应缓冲</li>
<li class="">时间拉伸（加速/减速播放）</li>
<li class="">PLC（丢包隐藏）</li>
<li class="">舒适噪声生成（DTX 期间）</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="45-音频-statsconcealedsamples-与-silentconcealedsamples">4.5 音频 stats：concealedSamples 与 silentConcealedSamples<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#45-%E9%9F%B3%E9%A2%91-statsconcealedsamples-%E4%B8%8E-silentconcealedsamples" class="hash-link" aria-label="Direct link to 4.5 音频 stats：concealedSamples 与 silentConcealedSamples" title="Direct link to 4.5 音频 stats：concealedSamples 与 silentConcealedSamples" translate="no">​</a></h3>
<p><code>getStats()</code> 中 inbound-rtp（audio）特有字段：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"inbound-rtp"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"audio"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> concealed </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">concealedSamples</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">??</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> total </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">totalSamplesReceived</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">??</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> plcRate </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">concealed </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> total </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">toFixed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">PLC rate: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">plcRate</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">%, jitter: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation punctuation" style="color:#393A34">(</span><span class="token template-string interpolation">r</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">jitter</span><span class="token template-string interpolation"> </span><span class="token template-string interpolation operator" style="color:#393A34">*</span><span class="token template-string interpolation"> </span><span class="token template-string interpolation number" style="color:#36acaa">1000</span><span class="token template-string interpolation punctuation" style="color:#393A34">)</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation method function property-access" style="color:#d73a49">toFixed</span><span class="token template-string interpolation punctuation" style="color:#393A34">(</span><span class="token template-string interpolation number" style="color:#36acaa">1</span><span class="token template-string interpolation punctuation" style="color:#393A34">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">ms</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left"><code>concealedSamples</code></td><td style="text-align:left">NetEQ 通过 PLC 合成的样本数</td></tr><tr><td style="text-align:left"><code>silentConcealedSamples</code></td><td style="text-align:left">静音段被隐藏的样本</td></tr><tr><td style="text-align:left"><code>fecPacketsReceived</code></td><td style="text-align:left">收到的 FEC 包数</td></tr><tr><td style="text-align:left"><code>insertedSamplesForDeceleration</code></td><td style="text-align:left">减速播放插入的样本（JB 膨胀）</td></tr><tr><td style="text-align:left"><code>removedSamplesForAcceleration</code></td><td style="text-align:left">加速播放移除的样本（JB 收缩）</td></tr></tbody></table>
<p><code>concealedSamples</code> 持续上升说明 FEC/NACK 未能恢复，NetEQ 在「猜」音频——主观音质发闷或断续。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五丢包补偿策略">五、丢包补偿策略<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#%E4%BA%94%E4%B8%A2%E5%8C%85%E8%A1%A5%E5%81%BF%E7%AD%96%E7%95%A5" class="hash-link" aria-label="Direct link to 五、丢包补偿策略" title="Direct link to 五、丢包补偿策略" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">机制</th><th style="text-align:left">原理</th><th style="text-align:left">适用</th><th style="text-align:left">延迟代价</th></tr></thead><tbody><tr><td style="text-align:left"><strong>NACK + Retransmission</strong></td><td style="text-align:left">RTCP NACK 触发重传</td><td style="text-align:left">视频，丢包率 &lt; 5%</td><td style="text-align:left">1 RTT</td></tr><tr><td style="text-align:left"><strong>FEC</strong></td><td style="text-align:left">发送冗余包（XOR / Reed-Solomon）</td><td style="text-align:left">高丢包网络（&gt; 5%）</td><td style="text-align:left">0 RTT，+带宽</td></tr><tr><td style="text-align:left"><strong>Opus In-band FEC</strong></td><td style="text-align:left">音频帧内嵌前向纠错</td><td style="text-align:left">音频抗丢包</td><td style="text-align:left">0 RTT，+码率</td></tr><tr><td style="text-align:left"><strong>PLC</strong></td><td style="text-align:left">丢包隐藏（猜测填充）</td><td style="text-align:left">音频最后防线</td><td style="text-align:left">0，质量下降</td></tr><tr><td style="text-align:left"><strong>ULPFEC</strong></td><td style="text-align:left">通用 FEC 框架</td><td style="text-align:left">视频冗余</td><td style="text-align:left">0 RTT，+带宽</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-opus-in-band-fec">5.1 Opus In-band FEC<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#51-opus-in-band-fec" class="hash-link" aria-label="Direct link to 5.1 Opus In-band FEC" title="Direct link to 5.1 Opus In-band FEC" translate="no">​</a></h3>
<p>SDP 中通过 <code>a=fmtp:111 useinbandfec=1</code> 启用。Opus 在编码时将前一帧的部分信息冗余编码进当前帧。丢包时解码器尝试从 FEC 数据恢复，无需重传。</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 检查 Opus FEC 是否启用</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sdp </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> fecEnabled </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> sdp</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">includes</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"useinbandfec=1"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Opus FEC:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> fecEnabled</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-视频-feculpfec--flexfec">5.2 视频 FEC（ULPFEC / FlexFEC）<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#52-%E8%A7%86%E9%A2%91-feculpfec--flexfec" class="hash-link" aria-label="Direct link to 5.2 视频 FEC（ULPFEC / FlexFEC）" title="Direct link to 5.2 视频 FEC（ULPFEC / FlexFEC）" translate="no">​</a></h3>
<p>WebRTC 支持 <a href="https://datatracker.ietf.org/doc/html/rfc5109" target="_blank" rel="noopener noreferrer" class="">RFC 5109 ULPFEC</a> 和 FlexFEC：</p>
<!-- -->
<p>FEC 牺牲带宽换取零延迟恢复，适合高丢包但延迟敏感的链路（如移动网络）。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="53-rtx-重传流">5.3 RTX 重传流<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#53-rtx-%E9%87%8D%E4%BC%A0%E6%B5%81" class="hash-link" aria-label="Direct link to 5.3 RTX 重传流" title="Direct link to 5.3 RTX 重传流" translate="no">​</a></h3>
<p>RTX 使用独立 SSRC 和 PT，避免重传包与原始包混淆：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">原始: SSRC=12345, PT=96, seq=100</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">重传: SSRC=67890, PT=97, seq=50 (RTX seq 独立计数)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      OSN=100 (Original Sequence Number, 在 RTX 载荷头中)</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="54-flexfec-与-ulpfec-选型">5.4 FlexFEC 与 ULPFEC 选型<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#54-flexfec-%E4%B8%8E-ulpfec-%E9%80%89%E5%9E%8B" class="hash-link" aria-label="Direct link to 5.4 FlexFEC 与 ULPFEC 选型" title="Direct link to 5.4 FlexFEC 与 ULPFEC 选型" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">方案</th><th style="text-align:left">RFC</th><th style="text-align:left">特点</th><th style="text-align:left">WebRTC 支持</th></tr></thead><tbody><tr><td style="text-align:left">ULPFEC</td><td style="text-align:left">RFC 5109</td><td style="text-align:left">XOR 冗余，实现简单</td><td style="text-align:left">广泛</td></tr><tr><td style="text-align:left">FlexFEC</td><td style="text-align:left">RFC 8627</td><td style="text-align:left">Reed-Solomon，可配置保护组</td><td style="text-align:left">Chrome 部分场景</td></tr><tr><td style="text-align:left">Opus in-band FEC</td><td style="text-align:left">RFC 7587</td><td style="text-align:left">音频帧内冗余</td><td style="text-align:left">默认推荐</td></tr></tbody></table>
<p>视频 FEC 牺牲约 10–30% 额外带宽。移动网络丢包 5–15% 时，FEC 比 NACK 更稳（零 RTT 恢复）；有线网络低丢包时 NACK+RTX 更省带宽。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六getstats-api-深度解读">六、getStats() API 深度解读<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#%E5%85%ADgetstats-api-%E6%B7%B1%E5%BA%A6%E8%A7%A3%E8%AF%BB" class="hash-link" aria-label="Direct link to 六、getStats() API 深度解读" title="Direct link to 六、getStats() API 深度解读" translate="no">​</a></h2>
<p><code>RTCPeerConnection.getStats()</code> 是诊断 RTP/RTCP 问题的核心工具。</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">setInterval</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">report</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"inbound-rtp"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">packetsLost</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">packetsLost</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">packetsReceived</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">packetsReceived</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">jitter</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">jitter</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">framesDecoded</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">framesDecoded</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">framesDropped</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">framesDropped</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">frameWidth</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">frameWidth</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">frameHeight</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">frameHeight</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">decoderImplementation</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">decoderImplementation</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">pliCount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pliCount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">nackCount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">nackCount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">fecPacketsReceived</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">fecPacketsReceived</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">fecPacketsDiscarded</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">fecPacketsDiscarded</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"outbound-rtp"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">targetBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">targetBitrate</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">framesEncoded</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">framesEncoded</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">qualityLimitationReason</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">qualityLimitationReason</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">qualityLimitationDurations</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">qualityLimitationDurations</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">nackCount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">nackCount</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">retransmittedPacketsSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">retransmittedPacketsSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"remote-inbound-rtp"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">roundTripTime</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">roundTripTime</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">fractionLost</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">fractionLost</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">jitter</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">jitter</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate-pair"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">state</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"succeeded"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">currentRoundTripTime</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">currentRoundTripTime</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">availableOutgoingBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">availableOutgoingBitrate</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesReceived</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesReceived</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="61-关键-stats-字段">6.1 关键 stats 字段<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#61-%E5%85%B3%E9%94%AE-stats-%E5%AD%97%E6%AE%B5" class="hash-link" aria-label="Direct link to 6.1 关键 stats 字段" title="Direct link to 6.1 关键 stats 字段" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">类型</th><th style="text-align:left">含义</th><th style="text-align:left">告警阈值</th></tr></thead><tbody><tr><td style="text-align:left"><code>packetsLost</code></td><td style="text-align:left">inbound-rtp</td><td style="text-align:left">累计丢包</td><td style="text-align:left">&gt; 1% 需关注</td></tr><tr><td style="text-align:left"><code>jitter</code></td><td style="text-align:left">inbound-rtp</td><td style="text-align:left">到达抖动（秒）</td><td style="text-align:left">&gt; 0.03s 需关注</td></tr><tr><td style="text-align:left"><code>jitterBufferDelay</code></td><td style="text-align:left">inbound-rtp</td><td style="text-align:left">JB 累计延迟</td><td style="text-align:left">持续增长 → 网络恶化</td></tr><tr><td style="text-align:left"><code>framesDropped</code></td><td style="text-align:left">inbound-rtp</td><td style="text-align:left">解码前丢弃帧数</td><td style="text-align:left">持续增长 → 性能瓶颈</td></tr><tr><td style="text-align:left"><code>pliCount</code></td><td style="text-align:left">inbound-rtp</td><td style="text-align:left">收到的 PLI 次数</td><td style="text-align:left">频繁 → 网络差或订阅切换</td></tr><tr><td style="text-align:left"><code>nackCount</code></td><td style="text-align:left">inbound/outbound</td><td style="text-align:left">NACK 次数</td><td style="text-align:left">频繁 → 丢包严重</td></tr><tr><td style="text-align:left"><code>qualityLimitationReason</code></td><td style="text-align:left">outbound-rtp</td><td style="text-align:left">画质限制原因</td><td style="text-align:left"><code>bandwidth</code>/<code>cpu</code></td></tr><tr><td style="text-align:left"><code>targetBitrate</code></td><td style="text-align:left">outbound-rtp</td><td style="text-align:left">GCC 目标码率</td><td style="text-align:left">持续下降 → 拥塞</td></tr><tr><td style="text-align:left"><code>availableOutgoingBitrate</code></td><td style="text-align:left">candidate-pair</td><td style="text-align:left">可用出站带宽估计</td><td style="text-align:left">—</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="62-qualitylimitationreason-解读">6.2 qualityLimitationReason 解读<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#62-qualitylimitationreason-%E8%A7%A3%E8%AF%BB" class="hash-link" aria-label="Direct link to 6.2 qualityLimitationReason 解读" title="Direct link to 6.2 qualityLimitationReason 解读" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">值</th><th style="text-align:left">含义</th><th style="text-align:left">对策</th></tr></thead><tbody><tr><td style="text-align:left"><code>none</code></td><td style="text-align:left">无限制</td><td style="text-align:left">正常</td></tr><tr><td style="text-align:left"><code>cpu</code></td><td style="text-align:left">CPU 编码能力不足</td><td style="text-align:left">降分辨率/换硬编</td></tr><tr><td style="text-align:left"><code>bandwidth</code></td><td style="text-align:left">带宽不足</td><td style="text-align:left">检查网络/TURN</td></tr><tr><td style="text-align:left"><code>other</code></td><td style="text-align:left">其他原因</td><td style="text-align:left">查 webrtc-internals</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="63-stats-报告关联">6.3 Stats 报告关联<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#63-stats-%E6%8A%A5%E5%91%8A%E5%85%B3%E8%81%94" class="hash-link" aria-label="Direct link to 6.3 Stats 报告关联" title="Direct link to 6.3 Stats 报告关联" translate="no">​</a></h3>
<!-- -->
<p>通过 <code>report.id</code> 和 <code>report.transportId</code> / <code>report.codecId</code> 关联不同报告。Ch13 将完整讲解 stats 字段与 Prometheus 指标化。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七常见问题与排查">七、常见问题与排查<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#%E4%B8%83%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E4%B8%8E%E6%8E%92%E6%9F%A5" class="hash-link" aria-label="Direct link to 七、常见问题与排查" title="Direct link to 七、常见问题与排查" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">问题</th><th style="text-align:left">可能原因</th><th style="text-align:left">排查</th></tr></thead><tbody><tr><td style="text-align:left">视频马赛克/花屏</td><td style="text-align:left">丢包 + NACK 来不及</td><td style="text-align:left">查 <code>packetsLost</code>、<code>nackCount</code></td></tr><tr><td style="text-align:left">音频断续</td><td style="text-align:left">Jitter 过大或 PLC 频繁触发</td><td style="text-align:left">查 <code>jitter</code>、<code>concealedSamples</code></td></tr><tr><td style="text-align:left">单向音视频</td><td style="text-align:left">SSRC 不匹配 / 只收到 RR 无 RTP</td><td style="text-align:left">查 inbound-rtp 是否存在</td></tr><tr><td style="text-align:left">延迟持续增长</td><td style="text-align:left">Jitter Buffer 膨胀</td><td style="text-align:left">查 <code>jitterBufferDelay</code> 趋势</td></tr><tr><td style="text-align:left">画质突然下降</td><td style="text-align:left">PLI 触发关键帧等待</td><td style="text-align:left">查 <code>pliCount</code> 突增</td></tr><tr><td style="text-align:left">码率不达预期</td><td style="text-align:left">GCC 限制或 CPU 瓶颈</td><td style="text-align:left">查 <code>qualityLimitationReason</code></td></tr><tr><td style="text-align:left">Simulcast 层收不到</td><td style="text-align:left">SFU 未转发对应 SSRC</td><td style="text-align:left">查多个 inbound-rtp 报告</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="71-丢包率计算">7.1 丢包率计算<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#71-%E4%B8%A2%E5%8C%85%E7%8E%87%E8%AE%A1%E7%AE%97" class="hash-link" aria-label="Direct link to 7.1 丢包率计算" title="Direct link to 7.1 丢包率计算" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">calcLossRate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">report</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> total </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">packetsReceived</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">packetsLost</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">total </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">packetsLost</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> total </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">100</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">toFixed</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"%"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="72-模拟弱网">7.2 模拟弱网<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#72-%E6%A8%A1%E6%8B%9F%E5%BC%B1%E7%BD%91" class="hash-link" aria-label="Direct link to 7.2 模拟弱网" title="Direct link to 7.2 模拟弱网" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># Linux: 5% 丢包 + 100ms 延迟</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">sudo</span><span class="token plain"> tc qdisc </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> dev eth0 root netem loss </span><span class="token number" style="color:#36acaa">5</span><span class="token plain">% delay 100ms</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># macOS: 使用 Network Link Conditioner（Xcode Additional Tools）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Chrome DevTools: Network → Throttling → Custom → 500 kbps</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八实战-lab">八、实战 Lab<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#%E5%85%AB%E5%AE%9E%E6%88%98-lab" class="hash-link" aria-label="Direct link to 八、实战 Lab" title="Direct link to 八、实战 Lab" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-1观察-rtp-包头">Lab 1：观察 RTP 包头<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#lab-1%E8%A7%82%E5%AF%9F-rtp-%E5%8C%85%E5%A4%B4" class="hash-link" aria-label="Direct link to Lab 1：观察 RTP 包头" title="Direct link to Lab 1：观察 RTP 包头" translate="no">​</a></h3>
<ol>
<li class="">建立 P2P 通话，Wireshark 过滤 <code>rtp</code></li>
<li class="">展开一个 RTP 包，记录 SSRC、Seq、Timestamp、PT、M bit</li>
<li class="">对比音频包（PT=111）与视频包（PT=96）的 Timestamp 增量</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-2丢包与-nack">Lab 2：丢包与 NACK<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#lab-2%E4%B8%A2%E5%8C%85%E4%B8%8E-nack" class="hash-link" aria-label="Direct link to Lab 2：丢包与 NACK" title="Direct link to Lab 2：丢包与 NACK" translate="no">​</a></h3>
<ol>
<li class=""><code>sudo tc qdisc add dev eth0 root netem loss 3%</code></li>
<li class=""><code>getStats()</code> 观察 <code>packetsLost</code> 和 <code>nackCount</code> 上升</li>
<li class="">移除 netem：<code>sudo tc qdisc del dev eth0 root</code></li>
<li class="">观察 PLI 是否触发（<code>pliCount</code> 突增）</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-3jitter-buffer-延迟">Lab 3：Jitter Buffer 延迟<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#lab-3jitter-buffer-%E5%BB%B6%E8%BF%9F" class="hash-link" aria-label="Direct link to Lab 3：Jitter Buffer 延迟" title="Direct link to Lab 3：Jitter Buffer 延迟" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">setInterval</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"inbound-rtp"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"audio"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> avgJBDelay </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">jitterBufferDelay</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">jitterBufferEmittedCount</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">JB avg delay: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation punctuation" style="color:#393A34">(</span><span class="token template-string interpolation">avgJBDelay </span><span class="token template-string interpolation operator" style="color:#393A34">*</span><span class="token template-string interpolation"> </span><span class="token template-string interpolation number" style="color:#36acaa">1000</span><span class="token template-string interpolation punctuation" style="color:#393A34">)</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation method function property-access" style="color:#d73a49">toFixed</span><span class="token template-string interpolation punctuation" style="color:#393A34">(</span><span class="token template-string interpolation number" style="color:#36acaa">1</span><span class="token template-string interpolation punctuation" style="color:#393A34">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">ms, jitter: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation punctuation" style="color:#393A34">(</span><span class="token template-string interpolation">r</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">jitter</span><span class="token template-string interpolation"> </span><span class="token template-string interpolation operator" style="color:#393A34">*</span><span class="token template-string interpolation"> </span><span class="token template-string interpolation number" style="color:#36acaa">1000</span><span class="token template-string interpolation punctuation" style="color:#393A34">)</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation method function property-access" style="color:#d73a49">toFixed</span><span class="token template-string interpolation punctuation" style="color:#393A34">(</span><span class="token template-string interpolation number" style="color:#36acaa">1</span><span class="token template-string interpolation punctuation" style="color:#393A34">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">ms</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<p>逐步增加 <code>netem delay</code>，观察 JB 延迟如何自适应增长。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-4twcc-与码率">Lab 4：TWCC 与码率<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#lab-4twcc-%E4%B8%8E%E7%A0%81%E7%8E%87" class="hash-link" aria-label="Direct link to Lab 4：TWCC 与码率" title="Direct link to Lab 4：TWCC 与码率" translate="no">​</a></h3>
<ol>
<li class="">Chrome DevTools 限速 500 kbps</li>
<li class="">观察 <code>outbound-rtp.targetBitrate</code> 下降</li>
<li class="">观察 <code>qualityLimitationReason</code> 变为 <code>bandwidth</code></li>
<li class="">恢复带宽，观察码率回升</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-5rtcp-包分析">Lab 5：RTCP 包分析<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#lab-5rtcp-%E5%8C%85%E5%88%86%E6%9E%90" class="hash-link" aria-label="Direct link to Lab 5：RTCP 包分析" title="Direct link to Lab 5：RTCP 包分析" translate="no">​</a></h3>
<ol>
<li class="">Wireshark 过滤 <code>rtcp</code></li>
<li class="">区分 SR（PT=200）和 RR（PT=201）</li>
<li class="">找到 NACK/PLI 反馈包（PT=205，FMT=1）</li>
<li class="">记录 RR 中的 <code>fraction lost</code> 和 <code>jitter</code> 字段</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-6fec-效果对比">Lab 6：FEC 效果对比<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#lab-6fec-%E6%95%88%E6%9E%9C%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to Lab 6：FEC 效果对比" title="Direct link to Lab 6：FEC 效果对比" translate="no">​</a></h3>
<ol>
<li class="">在 Opus SDP 中确认 <code>useinbandfec=1</code></li>
<li class="">10% 丢包下对比开启/关闭 FEC 的 <code>concealedSamples</code> 差异</li>
<li class="">记录主观音频质量差异</li>
</ol>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九本章小结">九、本章小结<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#%E4%B9%9D%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="Direct link to 九、本章小结" title="Direct link to 九、本章小结" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">要点</th><th style="text-align:left">内容</th></tr></thead><tbody><tr><td style="text-align:left">RTP 角色</td><td style="text-align:left">实时媒体容器：序号 + 时间戳 + SSRC</td></tr><tr><td style="text-align:left">RTCP 角色</td><td style="text-align:left">质量反馈：SR/RR 统计 + NACK/PLI/TWCC 事件</td></tr><tr><td style="text-align:left">丢包策略</td><td style="text-align:left">音频 FEC+PLC，视频 NACK+RTX+PLI</td></tr><tr><td style="text-align:left">延迟来源</td><td style="text-align:left">Jitter Buffer 是主要可调因素</td></tr><tr><td style="text-align:left">诊断工具</td><td style="text-align:left"><code>getStats()</code> + Wireshark + netem</td></tr></tbody></table>
<p>Ron Frederick 1992 年在 Xerox PARC 创造 RTP 时，大概不会想到它会成为三十年后全球视频会议的基础协议。RTP 的美在于极简——12 字节头 + 载荷——把复杂性留给上层（编解码、加密、拥塞控制）。WebRTC 正是这些上层复杂性的集大成者。</p>
<p><strong>下一篇（Ch9）</strong>：<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">编解码与 Simulcast</a>——RTP 载荷中的 Opus/VP8 帧如何编码，以及 Simulcast 如何用多个 SSRC 实现多档发布。</p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc3550" target="_blank" rel="noopener noreferrer" class="">RFC 3550 — RTP: A Transport Protocol for Real-Time Applications</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc4585" target="_blank" rel="noopener noreferrer" class="">RFC 4585 — Extended RTP Profile for RTCP-Based Feedback (AVPF)</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc5109" target="_blank" rel="noopener noreferrer" class="">RFC 5109 — RTP Payload Format for Generic FEC</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc4588" target="_blank" rel="noopener noreferrer" class="">RFC 4588 — RTP Retransmission Payload Format (RTX)</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/draft-holmer-rmcat-transport-wide-cc-extensions" target="_blank" rel="noopener noreferrer" class="">draft-holmer-rmcat-transport-wide-cc — TWCC</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8627" target="_blank" rel="noopener noreferrer" class="">RFC 8627 — RTP FlexFEC</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — RTP 历史访谈</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/getStats" target="_blank" rel="noopener noreferrer" class="">MDN — RTCPeerConnection.getStats()</a></li>
<li class=""><a href="https://webrtchacks.com/" target="_blank" rel="noopener noreferrer" class="">webrtcH4cKS — Demystifying WebRTC RTCP</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>RTP</category>
            <category>实时通信</category>
            <category>Deep Dive</category>
            <category>Monitoring</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (7)：DTLS 握手与 SRTP 加密体系]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp</guid>
            <pubDate>Thu, 18 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[DTLS over UDP 握手流程、Certificate Fingerprint 验证、SRTP 密钥导出与 E2EE 入门]]></description>
            <content:encoded><![CDATA[<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">Ch5 ICE</a> 连通 UDP 通道后，媒体并非明文传输。WebRTC 强制走 <strong>DTLS 握手 → 导出 SRTP 密钥 → 加密 RTP</strong>，这是 <a href="https://datatracker.ietf.org/doc/html/rfc8827" target="_blank" rel="noopener noreferrer" class="">RFC 8827 WebRTC Security Architecture</a> 的核心要求。</p>
<p>理解这一层的设计意图：Gmail 语音视频聊天时代，安全是事后补丁；WebRTC 从标准化之初就将 <strong>默认加密</strong> 作为不可协商的底线（参见 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious — WebRTC 历史</a> 中「安全是重中之重」）。Google 工程师在 2010 年前后推动 DTLS-SRTP 成为唯一媒体保护方案，彻底告别了早期 VoIP 明文 RTP 的惯例。</p>
<p><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 历史</a> 中，Serge Lachapelle 回忆：收购 GIPS 后，团队内部曾激烈讨论「是否允许开发者关闭加密」。最终结论是一刀切——<strong>媒体必须加密，没有开关</strong>。这一决策直接写进了 <a href="https://datatracker.ietf.org/doc/html/rfc8827" target="_blank" rel="noopener noreferrer" class="">RFC 8827</a>：<code>a=crypto</code>（SDES）被废弃，<strong>DTLS-SRTP 是唯一允许的密钥协商方式</strong>。对开发者而言，这意味着你永远不会在 <code>chrome://webrtc-internals</code> 里看到明文 RTP 载荷——除非主动做 E2EE 之上的二次加密。</p>
<p>本章覆盖 <a href="https://datatracker.ietf.org/doc/html/rfc6347" target="_blank" rel="noopener noreferrer" class="">RFC 6347</a> DTLS 握手、<a href="https://datatracker.ietf.org/doc/html/rfc5764" target="_blank" rel="noopener noreferrer" class="">RFC 5764</a> 密钥导出、<a href="https://datatracker.ietf.org/doc/html/rfc3711" target="_blank" rel="noopener noreferrer" class="">RFC 3711</a> SRTP 包处理、Certificate Fingerprint 验证，以及 Insertable Streams 端到端加密入门。</p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>DTLS</strong></td><td style="text-align:left">Datagram Transport Layer Security</td><td style="text-align:left">在 UDP 上运行的 TLS 变体，<a href="https://datatracker.ietf.org/doc/html/rfc6347" target="_blank" rel="noopener noreferrer" class="">RFC 6347</a> 定义</td></tr><tr><td style="text-align:left"><strong>SRTP</strong></td><td style="text-align:left">Secure Real-time Transport Protocol</td><td style="text-align:left">RTP 的加密扩展，<a href="https://datatracker.ietf.org/doc/html/rfc3711" target="_blank" rel="noopener noreferrer" class="">RFC 3711</a> 定义</td></tr><tr><td style="text-align:left"><strong>SRTCP</strong></td><td style="text-align:left">Secure RTCP</td><td style="text-align:left">RTCP 控制包的加密版本，与 SRTP 共用密钥材料</td></tr><tr><td style="text-align:left"><strong>DTLS-SRTP</strong></td><td style="text-align:left">—</td><td style="text-align:left">从 DTLS 握手导出 SRTP 密钥的 profile，<a href="https://datatracker.ietf.org/doc/html/rfc5764" target="_blank" rel="noopener noreferrer" class="">RFC 5764</a></td></tr><tr><td style="text-align:left"><strong>Fingerprint</strong></td><td style="text-align:left">Certificate Fingerprint</td><td style="text-align:left">SDP <code>a=fingerprint</code> 中声明的证书哈希，绑定信令与 DTLS</td></tr><tr><td style="text-align:left"><strong>Master Secret</strong></td><td style="text-align:left">—</td><td style="text-align:left">DTLS 握手协商出的主密钥，经 KDF 导出 SRTP 密钥</td></tr><tr><td style="text-align:left"><strong>SRTP Master Key</strong></td><td style="text-align:left">—</td><td style="text-align:left">用于加密单个 RTP 会话的对称密钥（128/256 bit）</td></tr><tr><td style="text-align:left"><strong>SRTP Salt</strong></td><td style="text-align:left">—</td><td style="text-align:left">与 Master Key 配合使用的盐值，参与 IV 派生</td></tr><tr><td style="text-align:left"><strong>Authentication Tag</strong></td><td style="text-align:left">—</td><td style="text-align:left">SRTP 包尾的 MAC，验证完整性与真实性</td></tr><tr><td style="text-align:left"><strong>AEAD</strong></td><td style="text-align:left">Authenticated Encryption with Associated Data</td><td style="text-align:left">现代 SRTP 加密模式（AES-GCM），同时提供加密与认证</td></tr><tr><td style="text-align:left"><strong>use_srtp</strong></td><td style="text-align:left">DTLS Extension</td><td style="text-align:left">告知对端握手完成后密钥用于 SRTP 而非 DTLS 应用数据</td></tr><tr><td style="text-align:left"><strong>setup</strong></td><td style="text-align:left">SDP Attribute</td><td style="text-align:left">DTLS 角色：<code>active</code>/<code>passive</code>/<code>actpass</code></td></tr><tr><td style="text-align:left"><strong>Hop-by-hop</strong></td><td style="text-align:left">—</td><td style="text-align:left">默认模式：SFU 解密再加密，中间节点可见明文</td></tr><tr><td style="text-align:left"><strong>E2EE</strong></td><td style="text-align:left">End-to-End Encryption</td><td style="text-align:left">端到端加密，中间 SFU 无法解密</td></tr><tr><td style="text-align:left"><strong>Insertable Streams</strong></td><td style="text-align:left">—</td><td style="text-align:left">WebRTC Encoded Transform API，在编码帧上插入自定义加密</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一安全栈在协议栈中的位置">一、安全栈在协议栈中的位置<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#%E4%B8%80%E5%AE%89%E5%85%A8%E6%A0%88%E5%9C%A8%E5%8D%8F%E8%AE%AE%E6%A0%88%E4%B8%AD%E7%9A%84%E4%BD%8D%E7%BD%AE" class="hash-link" aria-label="Direct link to 一、安全栈在协议栈中的位置" title="Direct link to 一、安全栈在协议栈中的位置" translate="no">​</a></h2>
<p>WebRTC 媒体路径上的安全是分层的：ICE 只解决「包能送到哪」，DTLS 解决「密钥如何协商」，SRTP 解决「每个 RTP 包如何加密」。</p>
<!-- -->
<table><thead><tr><th style="text-align:left">层</th><th style="text-align:left">标准</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:left">DTLS</td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc6347" target="_blank" rel="noopener noreferrer" class="">RFC 6347</a></td><td style="text-align:left">在 UDP 上完成 TLS 式握手，协商密钥</td></tr><tr><td style="text-align:left">DTLS-SRTP</td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc5764" target="_blank" rel="noopener noreferrer" class="">RFC 5764</a></td><td style="text-align:left">从 DTLS 导出 SRTP 会话密钥</td></tr><tr><td style="text-align:left">SRTP</td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc3711" target="_blank" rel="noopener noreferrer" class="">RFC 3711</a></td><td style="text-align:left">RTP 包加密（AES-CM / AEAD）与认证（HMAC-SHA1）</td></tr><tr><td style="text-align:left">SRTCP</td><td style="text-align:left">RFC 3711</td><td style="text-align:left">RTCP 控制包同样加密</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>与 HTTPS 的对比</div><div class="admonitionContent_BuS1"><p>HTTPS = <strong>TLS over TCP</strong>，WebRTC = <strong>DTLS over UDP</strong>。UDP 不保证顺序和可靠送达，DTLS 必须处理丢包、重传和乱序——这也是为什么 DTLS 握手必须在 ICE 连通<strong>之后</strong>、RTP 发送<strong>之前</strong>完成。</p><p>与 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Ch6 Data Channel</a> 对比：Data Channel 走 <strong>SCTP over DTLS</strong>（应用数据在 DTLS 记录层内传输），而音视频走 <strong>DTLS-SRTP</strong>（DTLS 只用于密钥协商，媒体数据在 SRTP 层加密后直发 UDP）。</p></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="11-为什么不用-tls">1.1 为什么不用 TLS？<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#11-%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E7%94%A8-tls" class="hash-link" aria-label="Direct link to 1.1 为什么不用 TLS？" title="Direct link to 1.1 为什么不用 TLS？" translate="no">​</a></h3>
<p>TCP 的三次握手 + 队头阻塞与实时媒体不兼容。ICE 已经选定了 UDP 五元组，在此之上叠加 DTLS 是自然选择。DTLS 1.2 是 WebRTC 的强制基线；DTLS 1.3 在部分实现中逐步引入，但核心密钥导出逻辑仍遵循 RFC 5764 profile。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="12-savpf-与-srtp-的关系">1.2 SAVPF 与 SRTP 的关系<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#12-savpf-%E4%B8%8E-srtp-%E7%9A%84%E5%85%B3%E7%B3%BB" class="hash-link" aria-label="Direct link to 1.2 SAVPF 与 SRTP 的关系" title="Direct link to 1.2 SAVPF 与 SRTP 的关系" translate="no">​</a></h3>
<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">Ch4 SDP</a> 中 m-line 的 <code>UDP/TLS/RTP/SAVPF</code> 已经声明了完整协议栈：</p>
<ul>
<li class=""><strong>TLS</strong> → 实际指 DTLS</li>
<li class=""><strong>SAVP</strong> → Secure AV Profile（SRTP）</li>
<li class=""><strong>F</strong> → Feedback（RTCP 反馈，<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">Ch8</a> 详解）</li>
</ul>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二certificate-fingerprintsdp-与-dtls-的绑定">二、Certificate Fingerprint：SDP 与 DTLS 的绑定<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#%E4%BA%8Ccertificate-fingerprintsdp-%E4%B8%8E-dtls-%E7%9A%84%E7%BB%91%E5%AE%9A" class="hash-link" aria-label="Direct link to 二、Certificate Fingerprint：SDP 与 DTLS 的绑定" title="Direct link to 二、Certificate Fingerprint：SDP 与 DTLS 的绑定" translate="no">​</a></h2>
<p>WebRTC 不使用传统 CA 证书链验证对端身份，而是用 <strong>自签名证书 + SDP 指纹交叉验证</strong>。这是 WebRTC 安全模型最独特的设计之一。</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-sdp-中的指纹与角色">2.1 SDP 中的指纹与角色<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#21-sdp-%E4%B8%AD%E7%9A%84%E6%8C%87%E7%BA%B9%E4%B8%8E%E8%A7%92%E8%89%B2" class="hash-link" aria-label="Direct link to 2.1 SDP 中的指纹与角色" title="Direct link to 2.1 SDP 中的指纹与角色" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=fingerprint:sha-256 4A:AD:BA:62:79:B9:59:F4:7D:BD:65:F4:4C:87:3D:F4:2B:2B:5E:9E:8D:8E:8B:8A:8C:8D:8E:8F:90:91:92</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=setup:actpass</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left"><code>setup</code> 值</th><th style="text-align:left">含义</th><th style="text-align:left">典型场景</th></tr></thead><tbody><tr><td style="text-align:left"><code>active</code></td><td style="text-align:left">本端主动发起 DTLS ClientHello</td><td style="text-align:left">Answer 方常设为 active</td></tr><tr><td style="text-align:left"><code>passive</code></td><td style="text-align:left">本端等待对端发起</td><td style="text-align:left">被动等待方</td></tr><tr><td style="text-align:left"><code>actpass</code></td><td style="text-align:left">可主可客</td><td style="text-align:left">Offer 方常用</td></tr></tbody></table>
<p><strong>角色协商规则</strong>：Offer 方声明 <code>actpass</code>，Answer 方选择 <code>active</code> 或 <code>passive</code>，最终一方为 Client、一方为 Server。双方不能同时 passive——否则握手永不开始。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="22-指纹算法">2.2 指纹算法<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#22-%E6%8C%87%E7%BA%B9%E7%AE%97%E6%B3%95" class="hash-link" aria-label="Direct link to 2.2 指纹算法" title="Direct link to 2.2 指纹算法" translate="no">​</a></h3>
<p>WebRTC 要求支持 <strong>SHA-256</strong>（<code>a=fingerprint:sha-256</code>）。旧实现可能还支持 SHA-1，但现代浏览器已弃用。指纹计算方式：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">fingerprint = hash_algorithm( DER_encoded_certificate )</span><br></div></code></pre></div></div></div>
<p>DER 是证书的 ASN.1 二进制编码，<strong>不是</strong> PEM 文本。浏览器在 <code>createOffer()</code> 时自动将指纹写入 SDP，开发者通常无需手动计算。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="23-设计意图与安全边界">2.3 设计意图与安全边界<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#23-%E8%AE%BE%E8%AE%A1%E6%84%8F%E5%9B%BE%E4%B8%8E%E5%AE%89%E5%85%A8%E8%BE%B9%E7%95%8C" class="hash-link" aria-label="Direct link to 2.3 设计意图与安全边界" title="Direct link to 2.3 设计意图与安全边界" translate="no">​</a></h3>
<p>信令通道（WebSocket）可能是 HTTP 而非 HTTPS，指纹机制确保即使信令被窃听，攻击者也无法在 DTLS 层冒充对端——因为 SDP 中的指纹与 DTLS 证书必须一致。</p>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>指纹验证的局限</div><div class="admonitionContent_BuS1"><p>指纹机制防的是 <strong>DTLS 层的中间人</strong>，不防 <strong>信令层的冒充</strong>。如果攻击者能篡改 SDP 中的 fingerprint 和 ICE 凭证，仍可实施 MITM。因此生产环境信令<strong>必须</strong>使用 WSS + 身份认证。</p></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="24-读取本地证书信息">2.4 读取本地证书信息<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#24-%E8%AF%BB%E5%8F%96%E6%9C%AC%E5%9C%B0%E8%AF%81%E4%B9%A6%E4%BF%A1%E6%81%AF" class="hash-link" aria-label="Direct link to 2.4 读取本地证书信息" title="Direct link to 2.4 读取本地证书信息" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// chrome://webrtc-internals 中可查看，也可用 getStats 间接获取</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addEventListener</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"connectionstatechange"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">connectionState</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"connected"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">report</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"transport"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"DTLS state:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dtlsState</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Selected cipher:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">tlsVersion</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dtlsCipher</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三dtls-握手完整时序rfc-6347">三、DTLS 握手完整时序（RFC 6347）<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#%E4%B8%89dtls-%E6%8F%A1%E6%89%8B%E5%AE%8C%E6%95%B4%E6%97%B6%E5%BA%8Frfc-6347" class="hash-link" aria-label="Direct link to 三、DTLS 握手完整时序（RFC 6347）" title="Direct link to 三、DTLS 握手完整时序（RFC 6347）" translate="no">​</a></h2>
<p>DTLS 在 UDP 上的握手与 TLS 类似，但增加了 <strong>Epoch/Sequence Number</strong>、<strong>HelloVerifyRequest</strong>（防 DoS）和 <strong>分片重传</strong> 机制。</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-use_srtp-扩展">3.1 use_srtp 扩展<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#31-use_srtp-%E6%89%A9%E5%B1%95" class="hash-link" aria-label="Direct link to 3.1 use_srtp 扩展" title="Direct link to 3.1 use_srtp 扩展" translate="no">​</a></h3>
<p><code>use_srtp</code> 是 DTLS 握手的核心扩展，告知对端：握手完成后密钥将用于 SRTP 而非普通 DTLS 应用数据。扩展结构（RFC 5764）：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">use_srtp extension:</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  SRTPProtectionProfile profiles[]  // 如 AES128_CM_HMAC_SHA1_80</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  MKI (optional)</span><br></div></code></pre></div></div></div>
<p>WebRTC 中常见的 SRTP Protection Profile：</p>
<table><thead><tr><th style="text-align:left">Profile</th><th style="text-align:left">加密</th><th style="text-align:left">认证</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left"><code>AES128_CM_HMAC_SHA1_80</code></td><td style="text-align:left">AES-128 Counter Mode</td><td style="text-align:left">HMAC-SHA1 80-bit tag</td><td style="text-align:left">经典 SRTP，RFC 3711</td></tr><tr><td style="text-align:left"><code>AES128_CM_HMAC_SHA1_32</code></td><td style="text-align:left">AES-128 CM</td><td style="text-align:left">HMAC-SHA1 32-bit tag</td><td style="text-align:left">较短 tag，较少使用</td></tr><tr><td style="text-align:left"><code>AEAD_AES_128_GCM</code></td><td style="text-align:left">AES-128 GCM</td><td style="text-align:left">内建认证</td><td style="text-align:left">现代 AEAD 模式</td></tr><tr><td style="text-align:left"><code>AEAD_AES_256_GCM</code></td><td style="text-align:left">AES-256 GCM</td><td style="text-align:left">内建认证</td><td style="text-align:left">更强密钥长度</td></tr></tbody></table>
<p>ClientHello 和 ServerHello 中各自声明支持的 profile 列表，最终取<strong>交集</strong>中优先级最高者。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-dtls-与-ice-的时序依赖">3.2 DTLS 与 ICE 的时序依赖<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#32-dtls-%E4%B8%8E-ice-%E7%9A%84%E6%97%B6%E5%BA%8F%E4%BE%9D%E8%B5%96" class="hash-link" aria-label="Direct link to 3.2 DTLS 与 ICE 的时序依赖" title="Direct link to 3.2 DTLS 与 ICE 的时序依赖" translate="no">​</a></h3>
<!-- -->
<p><strong>关键约束</strong>：</p>
<ol>
<li class="">DTLS 握手在 ICE <code>connected</code> 之后才开始</li>
<li class="">SRTP 加密在 DTLS <code>connected</code> 之后才开始</li>
<li class="">浏览器在 DTLS 完成前<strong>不会</strong>发送可解码的 RTP 媒体</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="33-dtls-重传与丢包">3.3 DTLS 重传与丢包<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#33-dtls-%E9%87%8D%E4%BC%A0%E4%B8%8E%E4%B8%A2%E5%8C%85" class="hash-link" aria-label="Direct link to 3.3 DTLS 重传与丢包" title="Direct link to 3.3 DTLS 重传与丢包" translate="no">​</a></h3>
<p>UDP 上 DTLS 记录可能丢失。RFC 6347 规定：</p>
<ul>
<li class="">发送方维护重传定时器</li>
<li class="">收到重复的握手消息时静默丢弃（状态机已前进）</li>
<li class="">应用数据（Finished 之后）由上层处理丢包</li>
</ul>
<p>在弱网环境下，DTLS 握手可能比 ICE 连通多花数百毫秒。<code>chrome://webrtc-internals</code> 中可观察 <code>dtlsState</code> 从 <code>new</code> → <code>connecting</code> → <code>connected</code> 的过渡。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="34-dtls-记录层rfc-6347-section-4">3.4 DTLS 记录层（RFC 6347 Section 4）<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#34-dtls-%E8%AE%B0%E5%BD%95%E5%B1%82rfc-6347-section-4" class="hash-link" aria-label="Direct link to 3.4 DTLS 记录层（RFC 6347 Section 4）" title="Direct link to 3.4 DTLS 记录层（RFC 6347 Section 4）" translate="no">​</a></h3>
<p>DTLS 在 UDP 上复用 TLS 记录格式，但每条记录前增加 <strong>Epoch（2 字节）</strong> 和 <strong>Sequence Number（48 bit）</strong>（<a href="https://datatracker.ietf.org/doc/html/rfc6347#section-4" target="_blank" rel="noopener noreferrer" class="">RFC 6347 §4</a>）：</p>
<!-- -->
<p><code>Length</code> 之后为 Fragment 载荷；Certificate 等握手消息超过 MTU 时会按 RFC 6347 §4.1 分片，每条分片复用同一 Sequence Number 并携带 Fragment Offset。</p>
<table><thead><tr><th style="text-align:left">Content Type</th><th style="text-align:left">值</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left"><code>change_cipher_spec</code></td><td style="text-align:left">20</td><td style="text-align:left">通知对端切换到协商好的密码套件</td></tr><tr><td style="text-align:left"><code>alert</code></td><td style="text-align:left">21</td><td style="text-align:left">警告或致命错误（如 fingerprint 不匹配）</td></tr><tr><td style="text-align:left"><code>handshake</code></td><td style="text-align:left">22</td><td style="text-align:left">ClientHello / Certificate / Finished 等</td></tr><tr><td style="text-align:left"><code>application_data</code></td><td style="text-align:left">23</td><td style="text-align:left">Data Channel 的 SCTP 载荷（<strong>不是</strong> SRTP）</td></tr></tbody></table>
<p><strong>Epoch 机制</strong>：握手完成前 Epoch=0，ChangeCipherSpec 之后 Epoch=1。接收方按 Epoch 维护独立的 anti-replay 窗口——这与 SRTP 的重放保护是两套独立机制。</p>
<p><strong>分片与重组</strong>：UDP MTU 通常 1200–1500 字节，大型 Certificate 消息会被 DTLS 分片。若中间防火墙丢弃 oversized UDP 包，常见症状是 DTLS 永远停在 <code>connecting</code>——排查时可对比 TURN relay 与 host 直连路径。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="35-自定义证书rtccertificate">3.5 自定义证书（RTCCertificate）<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#35-%E8%87%AA%E5%AE%9A%E4%B9%89%E8%AF%81%E4%B9%A6rtccertificate" class="hash-link" aria-label="Direct link to 3.5 自定义证书（RTCCertificate）" title="Direct link to 3.5 自定义证书（RTCCertificate）" translate="no">​</a></h3>
<p>默认情况下，浏览器为每个 PeerConnection 自动生成 ECDSA 自签名证书。如需固定证书（例如 SFU 需要预注册 fingerprint），可使用 <code>RTCPeerConnection.generateCertificate()</code>：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> cert </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token maybe-class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">generateCertificate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ECDSA"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">namedCurve</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"P-256"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">ICE_SERVERS</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">certificates</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">cert</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>证书有效期</div><div class="admonitionContent_BuS1"><p><code>generateCertificate</code> 生成的证书默认有效期约 30 天（实现相关）。长会话若跨越证书过期需 renegotiation。生产 SFU 应能处理对端证书轮换而不中断媒体。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四srtp-密钥导出rfc-5764">四、SRTP 密钥导出（RFC 5764）<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#%E5%9B%9Bsrtp-%E5%AF%86%E9%92%A5%E5%AF%BC%E5%87%BArfc-5764" class="hash-link" aria-label="Direct link to 四、SRTP 密钥导出（RFC 5764）" title="Direct link to 四、SRTP 密钥导出（RFC 5764）" translate="no">​</a></h2>
<p>DTLS 握手完成后，双方各持有一个 <strong>DTLS Master Secret</strong>。RFC 5764 定义了标准的密钥导出函数（KDF），将其变换为 SRTP 会话密钥。</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-导出公式">4.1 导出公式<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#41-%E5%AF%BC%E5%87%BA%E5%85%AC%E5%BC%8F" class="hash-link" aria-label="Direct link to 4.1 导出公式" title="Direct link to 4.1 导出公式" translate="no">​</a></h3>
<p>RFC 5764 Section 4.2 定义的伪代码：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">SRTP_key = TLS-PRF(master_secret, "EXTRACTOR-dtls_srtp", random1 + random2)[0..key_len-1]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">SRTP_salt = TLS-PRF(master_secret, "EXTRACTOR-dtls_srtp", random1 + random2)[key_len..key_len+salt_len-1]</span><br></div></code></pre></div></div></div>
<p>其中 <code>random1</code> 和 <code>random2</code> 是 ClientHello 和 ServerHello 中的 <code>client_random</code> / <code>server_random</code>。Client 和 Server 各自导出<strong>对方写方向</strong>的密钥——即 A 用 Server Write Key 解密 B 发来的包，用 Client Write Key 加密自己发出的包。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-密钥材料长度">4.2 密钥材料长度<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#42-%E5%AF%86%E9%92%A5%E6%9D%90%E6%96%99%E9%95%BF%E5%BA%A6" class="hash-link" aria-label="Direct link to 4.2 密钥材料长度" title="Direct link to 4.2 密钥材料长度" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">Profile</th><th style="text-align:left">Key Length</th><th style="text-align:left">Salt Length</th></tr></thead><tbody><tr><td style="text-align:left">AES128_CM_HMAC_SHA1_80</td><td style="text-align:left">16 bytes (128 bit)</td><td style="text-align:left">14 bytes (112 bit)</td></tr><tr><td style="text-align:left">AEAD_AES_128_GCM</td><td style="text-align:left">16 bytes</td><td style="text-align:left">12 bytes (96 bit)</td></tr><tr><td style="text-align:left">AEAD_AES_256_GCM</td><td style="text-align:left">32 bytes (256 bit)</td><td style="text-align:left">12 bytes</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="43-密钥生命周期">4.3 密钥生命周期<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#43-%E5%AF%86%E9%92%A5%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F" class="hash-link" aria-label="Direct link to 4.3 密钥生命周期" title="Direct link to 4.3 密钥生命周期" translate="no">​</a></h3>
<ul>
<li class="">每次 PeerConnection 重建（重新 offer/answer）会生成<strong>新证书</strong>和<strong>新密钥</strong></li>
<li class="">ICE restart 如果伴随 renegotiation，同样轮换密钥</li>
<li class="">没有「会话内 rekey」——长通话密钥不变（E2EE 方案可能自行实现 rekey）</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="44-方向性谁加密谁解密">4.4 方向性：谁加密、谁解密<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#44-%E6%96%B9%E5%90%91%E6%80%A7%E8%B0%81%E5%8A%A0%E5%AF%86%E8%B0%81%E8%A7%A3%E5%AF%86" class="hash-link" aria-label="Direct link to 4.4 方向性：谁加密、谁解密" title="Direct link to 4.4 方向性：谁加密、谁解密" translate="no">​</a></h3>
<p>RFC 5764 的密钥命名遵循 TLS 惯例——<strong>Client Write Key</strong> 和 <strong>Server Write Key</strong> 分别用于 Client→Server 和 Server→Client 方向。WebRTC 中：</p>
<!-- -->
<p>接收端用<strong>对端 Write Key</strong> 解密——A 发送时用 A 的 Write Key 加密，B 用对应的 Read Key（即 A 的 Write Key）解密。搞混方向是自建 SFU 时最常见的 SRTP 解密失败原因。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五srtp-包处理rfc-3711">五、SRTP 包处理（RFC 3711）<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#%E4%BA%94srtp-%E5%8C%85%E5%A4%84%E7%90%86rfc-3711" class="hash-link" aria-label="Direct link to 五、SRTP 包处理（RFC 3711）" title="Direct link to 五、SRTP 包处理（RFC 3711）" translate="no">​</a></h2>
<p>SRTP 在 RTP 包头之后、UDP 载荷末尾添加加密与认证，不改变 RTP 头本身的语义（SSRC、Seq、Timestamp 仍明文）。</p>
<p>线上 SRTP 包布局（<a href="https://datatracker.ietf.org/doc/html/rfc3711" target="_blank" rel="noopener noreferrer" class="">RFC 3711</a>）——RTP 头明文，载荷加密，尾部追加认证标签：</p>
<!-- -->
<p>AEAD_AES_128_GCM 模式下 Auth Tag 为 128 bit，GCM 认证内建于加密过程，不再单独追加 HMAC。</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-iv初始化向量派生">5.1 IV（初始化向量）派生<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#51-iv%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%91%E9%87%8F%E6%B4%BE%E7%94%9F" class="hash-link" aria-label="Direct link to 5.1 IV（初始化向量）派生" title="Direct link to 5.1 IV（初始化向量）派生" translate="no">​</a></h3>
<p>AES Counter Mode 下，每个包的 IV 由以下输入派生：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">k_s = session key</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">salt = session salt</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">SSRC = RTP 同步源</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">index = RTP sequence number (作为 counter)</span><br></div></code></pre></div></div></div>
<p><strong>重要</strong>：SSRC 和 Seq 虽在 RTP 头中明文传输，但攻击者无法据此解密——没有 session key 和 salt 就无法恢复 counter 值。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-认证标签">5.2 认证标签<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#52-%E8%AE%A4%E8%AF%81%E6%A0%87%E7%AD%BE" class="hash-link" aria-label="Direct link to 5.2 认证标签" title="Direct link to 5.2 认证标签" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">模式</th><th style="text-align:left">Tag 长度</th><th style="text-align:left">位置</th></tr></thead><tbody><tr><td style="text-align:left">HMAC-SHA1_80</td><td style="text-align:left">10 bytes (80 bit)</td><td style="text-align:left">RTP 载荷末尾</td></tr><tr><td style="text-align:left">HMAC-SHA1_32</td><td style="text-align:left">4 bytes (32 bit)</td><td style="text-align:left">RTP 载荷末尾</td></tr><tr><td style="text-align:left">AEAD_GCM</td><td style="text-align:left">16 bytes (128 bit)</td><td style="text-align:left">内建于 GCM</td></tr></tbody></table>
<p>接收端验证 tag 失败 → <strong>静默丢弃</strong>包，不触发重传。这与 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">Ch8</a> 中 NACK 丢包机制形成互补：SRTP 丢弃是安全策略，NACK 丢包是网络策略。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="53-srtcp-加密">5.3 SRTCP 加密<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#53-srtcp-%E5%8A%A0%E5%AF%86" class="hash-link" aria-label="Direct link to 5.3 SRTCP 加密" title="Direct link to 5.3 SRTCP 加密" translate="no">​</a></h3>
<p>RTCP 包（SR/RR/NACK/PLI 等）使用同一套密钥材料加密，称 SRTCP。SRTCP trailer 含 <strong>SRTCP Index（32 bit）</strong> 与可选 <strong>MKI</strong>：</p>
<!-- -->
<p><code>a=rtcp-mux</code> 下 RTP 和 SRTCP 在同一 UDP 端口，通过包类型区分（RTP PT vs RTCP PT 范围 200-207）。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="54-重放保护">5.4 重放保护<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#54-%E9%87%8D%E6%94%BE%E4%BF%9D%E6%8A%A4" class="hash-link" aria-label="Direct link to 5.4 重放保护" title="Direct link to 5.4 重放保护" translate="no">​</a></h3>
<p>SRTP 维护一个 <strong>重放窗口</strong>（默认 64 或 128 包）。序号落在窗口之外的包被视为重放攻击而丢弃。WebRTC 中 RTP Seq 16 bit 循环，窗口足够覆盖正常乱序。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="55-rocrollover-counter-与包索引">5.5 ROC：Rollover Counter 与包索引<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#55-rocrollover-counter-%E4%B8%8E%E5%8C%85%E7%B4%A2%E5%BC%95" class="hash-link" aria-label="Direct link to 5.5 ROC：Rollover Counter 与包索引" title="Direct link to 5.5 ROC：Rollover Counter 与包索引" translate="no">​</a></h3>
<p>RTP Sequence Number 只有 16 bit，约 65536 个包后回绕。SRTP 内部维护 <strong>ROC（Rollover Counter）</strong> 将 seq 扩展为 48 bit 的 <strong>packet index</strong>：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">packet_index = (ROC &lt;&lt; 16) | sequence_number</span><br></div></code></pre></div></div></div>
<p>IV 派生和重放检测都基于 packet index，而非裸 seq。当 seq 从 65535 跳到 0 时，ROC 递增。自建媒体服务器若未正确维护 ROC，会在 seq 回绕时出现大面积解密失败——症状是「通话约 20 分钟后突然无声无画」。</p>
<table><thead><tr><th style="text-align:left">概念</th><th style="text-align:left">位数</th><th style="text-align:left">用途</th></tr></thead><tbody><tr><td style="text-align:left">RTP Sequence Number</td><td style="text-align:left">16 bit</td><td style="text-align:left">线上可见，丢包检测</td></tr><tr><td style="text-align:left">ROC</td><td style="text-align:left">32 bit</td><td style="text-align:left">SRTP 内部，处理 seq 回绕</td></tr><tr><td style="text-align:left">Packet Index</td><td style="text-align:left">48 bit</td><td style="text-align:left">IV 派生、重放窗口</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="56-mkimaster-key-index">5.6 MKI（Master Key Index）<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#56-mkimaster-key-index" class="hash-link" aria-label="Direct link to 5.6 MKI（Master Key Index）" title="Direct link to 5.6 MKI（Master Key Index）" translate="no">​</a></h3>
<p>RFC 3711 允许在 SRTP 包中携带 <strong>MKI</strong> 字段，标识使用哪组 Master Key——用于会话内 rekey。WebRTC 浏览器实现<strong>不使用 MKI</strong>（每次 renegotiation 整体换密钥），但阅读 Wireshark 或自建 SFU 源码时会遇到此字段。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六e2ee从-hop-by-hop-到端到端">六、E2EE：从 Hop-by-hop 到端到端<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#%E5%85%ADe2ee%E4%BB%8E-hop-by-hop-%E5%88%B0%E7%AB%AF%E5%88%B0%E7%AB%AF" class="hash-link" aria-label="Direct link to 六、E2EE：从 Hop-by-hop 到端到端" title="Direct link to 六、E2EE：从 Hop-by-hop 到端到端" translate="no">​</a></h2>
<p>默认 SRTP 保护的是 <strong>传输链路</strong>，不是 <strong>端到端内容</strong>。SFU 必须解密 RTP 才能做路由、转码、混流。</p>
<!-- -->
<table><thead><tr><th style="text-align:left">模式</th><th style="text-align:left">SFU 能否看到明文</th><th style="text-align:left">密钥管理</th><th style="text-align:left">适用场景</th></tr></thead><tbody><tr><td style="text-align:left">默认 SRTP</td><td style="text-align:left">能（SFU 参与 DTLS-SRTP）</td><td style="text-align:left">浏览器自动生成</td><td style="text-align:left">一般会议</td></tr><tr><td style="text-align:left">Insertable Streams E2EE</td><td style="text-align:left">不能</td><td style="text-align:left">应用层分发</td><td style="text-align:left">医疗、金融、高合规</td></tr><tr><td style="text-align:left">SFrame (Frame Encryption)</td><td style="text-align:left">不能</td><td style="text-align:left">集中式或 MLS</td><td style="text-align:left">Zoom 等商用方案</td></tr></tbody></table>
<!-- -->
<p><strong>SFrame</strong>（<a href="https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-sframe" target="_blank" rel="noopener noreferrer" class="">draft-ietf-mmusic-sframe</a>）是 IETF 标准化的帧级 E2EE 方案，与 Insertable Streams 思路类似但定义了统一的帧头格式和密钥轮换语义。Zoom、Google Meet 的部分 E2EE 模式基于 SFrame 或变体。浏览器原生 API 仍以 Insertable Streams / Encoded Transform 为主——应用层可自行实现 SFrame 封包格式。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="61-insertable-streams--encoded-transform">6.1 Insertable Streams / Encoded Transform<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#61-insertable-streams--encoded-transform" class="hash-link" aria-label="Direct link to 6.1 Insertable Streams / Encoded Transform" title="Direct link to 6.1 Insertable Streams / Encoded Transform" translate="no">​</a></h3>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpScriptTransform" target="_blank" rel="noopener noreferrer" class="">Insertable Streams API</a>（现称 WebRTC Encoded Transform）允许在<strong>编码后、网络发送前</strong>插入自定义加密层：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 发送端：在 Worker 中注册 Transform</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> worker </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Worker</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"e2ee-worker.js"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sender </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getSenders</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">s</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">track</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">kind </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"transform"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">transform</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCRtpScriptTransform</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">worker</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">operation</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"encrypt"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">key</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> derivedKey</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 应用层协商的 E2EE 密钥</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 接收端</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> receiver </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getReceivers</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">track</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">kind </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"transform"</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> receiver</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  receiver</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">transform</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCRtpScriptTransform</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">worker</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">operation</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"decrypt"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">key</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> derivedKey</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// e2ee-worker.js 简化示例</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function-variable function" style="color:#d73a49">onrtctransform</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> transformer </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> reader </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> transformer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">readable</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getReader</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> writer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> transformer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">writable</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getWriter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">process</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">while</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">value</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> frame</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> done </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> reader</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">read</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">done</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// frame 是 EncodedVideoFrame / EncodedAudioFrame</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> encrypted </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">encryptFrame</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">frame</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> key</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> writer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">write</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">encrypted</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">process</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="62-e2ee-密钥分发">6.2 E2EE 密钥分发<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#62-e2ee-%E5%AF%86%E9%92%A5%E5%88%86%E5%8F%91" class="hash-link" aria-label="Direct link to 6.2 E2EE 密钥分发" title="Direct link to 6.2 E2EE 密钥分发" translate="no">​</a></h3>
<p>SRTP 密钥由 DTLS 自动协商，但 E2EE 密钥需要<strong>应用层</strong>分发：</p>
<!-- -->
<p>常见方案：MLS（Messaging Layer Security）、Signal 的 Double Ratchet、或会议加入时由主持人分发对称密钥。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="63-e2ee-的代价">6.3 E2EE 的代价<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#63-e2ee-%E7%9A%84%E4%BB%A3%E4%BB%B7" class="hash-link" aria-label="Direct link to 6.3 E2EE 的代价" title="Direct link to 6.3 E2EE 的代价" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">能力</th><th style="text-align:left">默认 SRTP</th><th style="text-align:left">E2EE</th></tr></thead><tbody><tr><td style="text-align:left">SFU 转码</td><td style="text-align:left">支持</td><td style="text-align:left">不支持（密文无法解码）</td></tr><tr><td style="text-align:left">服务端录制</td><td style="text-align:left">支持</td><td style="text-align:left">需客户端录制或密钥托管</td></tr><tr><td style="text-align:left">服务端混流</td><td style="text-align:left">支持</td><td style="text-align:left">不支持</td></tr><tr><td style="text-align:left">带宽自适应</td><td style="text-align:left">SFU 可看内容决策</td><td style="text-align:left">仅依赖 TWCC/GCC</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="64-生产环境安全清单">6.4 生产环境安全清单<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#64-%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E5%AE%89%E5%85%A8%E6%B8%85%E5%8D%95" class="hash-link" aria-label="Direct link to 6.4 生产环境安全清单" title="Direct link to 6.4 生产环境安全清单" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">检查项</th><th style="text-align:left">要求</th><th style="text-align:left">常见失误</th></tr></thead><tbody><tr><td style="text-align:left">信令通道</td><td style="text-align:left">WSS + 身份认证</td><td style="text-align:left">HTTP 明文 WebSocket</td></tr><tr><td style="text-align:left">SDP 完整性</td><td style="text-align:left">签名或 E2E 加密信令</td><td style="text-align:left">中间人篡改 fingerprint</td></tr><tr><td style="text-align:left">TURN 凭证</td><td style="text-align:left">短期 token，按 room 隔离</td><td style="text-align:left">长期共享 username/password</td></tr><tr><td style="text-align:left">证书轮换</td><td style="text-align:left">SFU 支持 renegotiation</td><td style="text-align:left">固定密钥永不轮换</td></tr><tr><td style="text-align:left">E2EE 密钥</td><td style="text-align:left">独立于 SRTP，前向安全</td><td style="text-align:left">复用 SRTP 密钥做 E2EE</td></tr><tr><td style="text-align:left">日志脱敏</td><td style="text-align:left">不记录 SDP 全文</td><td style="text-align:left">日志泄露 ice-pwd</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七常见问题与排查">七、常见问题与排查<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#%E4%B8%83%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E4%B8%8E%E6%8E%92%E6%9F%A5" class="hash-link" aria-label="Direct link to 七、常见问题与排查" title="Direct link to 七、常见问题与排查" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">问题</th><th style="text-align:left">原因</th><th style="text-align:left">解决</th></tr></thead><tbody><tr><td style="text-align:left">DTLS 握手超时</td><td style="text-align:left">ICE 未真正连通 / 防火墙阻断 UDP</td><td style="text-align:left">检查 ICE state、TURN 配置</td></tr><tr><td style="text-align:left"><code>dtlsState: failed</code></td><td style="text-align:left">fingerprint 不匹配</td><td style="text-align:left">确保 SDP 未被篡改；信令用 WSS</td></tr><tr><td style="text-align:left">双方同时 passive</td><td style="text-align:left">Answer 未正确设置 <code>setup:active</code></td><td style="text-align:left">检查 SDP setup 属性</td></tr><tr><td style="text-align:left">SRTP 解密失败</td><td style="text-align:left">密钥导出 profile 不匹配</td><td style="text-align:left">确认双方支持相同 SRTP profile</td></tr><tr><td style="text-align:left">音视频无声无画但 ICE connected</td><td style="text-align:left">DTLS 未完成</td><td style="text-align:left">等待 <code>connectionState === 'connected'</code></td></tr><tr><td style="text-align:left">E2EE 花屏</td><td style="text-align:left">Transform 中帧边界处理错误</td><td style="text-align:left">保留 frame metadata（timestamp, type）</td></tr><tr><td style="text-align:left">证书频繁轮换</td><td style="text-align:left">每次 renegotiation 生成新证书</td><td style="text-align:left">正常行为；注意 SFU 需处理 re-key</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="71-wireshark-解密技巧">7.1 Wireshark 解密技巧<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#71-wireshark-%E8%A7%A3%E5%AF%86%E6%8A%80%E5%B7%A7" class="hash-link" aria-label="Direct link to 7.1 Wireshark 解密技巧" title="Direct link to 7.1 Wireshark 解密技巧" translate="no">​</a></h3>
<p>Wireshark 可通过 <code>Edit → Preferences → Protocols → DTLS</code> 配置 Pre-Master-Secret log，但浏览器不直接导出。更实用的方法：</p>
<ol>
<li class="">过滤 <code>dtls</code> 观察握手消息</li>
<li class="">过滤 <code>stun || dtls || rtp</code> 看完整建立过程</li>
<li class="">在 <code>chrome://webrtc-internals</code> 导出 SDP 对比两端 fingerprint</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="72-fingerprint-不匹配实验">7.2 fingerprint 不匹配实验<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#72-fingerprint-%E4%B8%8D%E5%8C%B9%E9%85%8D%E5%AE%9E%E9%AA%8C" class="hash-link" aria-label="Direct link to 7.2 fingerprint 不匹配实验" title="Direct link to 7.2 fingerprint 不匹配实验" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 仅用于 Lab 调试，不要在生产使用</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> offer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 篡改 fingerprint 中的一个字节</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> offer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">replace</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">(</span><span class="token regex regex-source language-regex" style="color:#36acaa">a=fingerprint:sha-256 </span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">)</span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">(</span><span class="token regex regex-source language-regex char-class char-class-punctuation punctuation" style="color:#393A34">[</span><span class="token regex regex-source language-regex char-class range" style="color:#36acaa">0</span><span class="token regex regex-source language-regex char-class range range-punctuation operator" style="color:#393A34">-</span><span class="token regex regex-source language-regex char-class range" style="color:#36acaa">9</span><span class="token regex regex-source language-regex char-class range" style="color:#36acaa">A</span><span class="token regex regex-source language-regex char-class range range-punctuation operator" style="color:#393A34">-</span><span class="token regex regex-source language-regex char-class range" style="color:#36acaa">F</span><span class="token regex regex-source language-regex char-class char-class-punctuation punctuation" style="color:#393A34">]</span><span class="token regex regex-source language-regex quantifier number" style="color:#36acaa">{2}</span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">)</span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token string" style="color:#e3116c">"$1FF"</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 将第一个字节改为 FF</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 预期：对端 DTLS 握手失败，connectionState → failed</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八实战-lab">八、实战 Lab<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#%E5%85%AB%E5%AE%9E%E6%88%98-lab" class="hash-link" aria-label="Direct link to 八、实战 Lab" title="Direct link to 八、实战 Lab" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-1观察-dtls-握手">Lab 1：观察 DTLS 握手<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#lab-1%E8%A7%82%E5%AF%9F-dtls-%E6%8F%A1%E6%89%8B" class="hash-link" aria-label="Direct link to Lab 1：观察 DTLS 握手" title="Direct link to Lab 1：观察 DTLS 握手" translate="no">​</a></h3>
<ol>
<li class="">建立 P2P 连接（可用 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">Ch2</a> 的 demo）</li>
<li class="">Wireshark 过滤 <code>dtls</code>，观察 ClientHello → ServerHello → Certificate → Finished</li>
<li class="">在 ClientHello 中展开 <code>use_srtp</code> 扩展，记录协商的 Protection Profile</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-2指纹篡改实验">Lab 2：指纹篡改实验<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#lab-2%E6%8C%87%E7%BA%B9%E7%AF%A1%E6%94%B9%E5%AE%9E%E9%AA%8C" class="hash-link" aria-label="Direct link to Lab 2：指纹篡改实验" title="Direct link to Lab 2：指纹篡改实验" translate="no">​</a></h3>
<ol>
<li class="">在信令服务器中拦截 SDP，修改 <code>a=fingerprint</code> 的一个十六进制字符</li>
<li class="">观察浏览器控制台 / <code>webrtc-internals</code> 中 <code>dtlsState</code> 变为 <code>failed</code></li>
<li class="">恢复正确指纹，确认连接成功</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-3getstats-监控-dtls-状态">Lab 3：getStats 监控 DTLS 状态<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#lab-3getstats-%E7%9B%91%E6%8E%A7-dtls-%E7%8A%B6%E6%80%81" class="hash-link" aria-label="Direct link to Lab 3：getStats 监控 DTLS 状态" title="Direct link to Lab 3：getStats 监控 DTLS 状态" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">setInterval</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"transport"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">dtlsState</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dtlsState</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">selectedCandidatePairId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">selectedCandidatePairId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesReceived</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesReceived</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-4e2ee-transform-骨架">Lab 4：E2EE Transform 骨架<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#lab-4e2ee-transform-%E9%AA%A8%E6%9E%B6" class="hash-link" aria-label="Direct link to Lab 4：E2EE Transform 骨架" title="Direct link to Lab 4：E2EE Transform 骨架" translate="no">​</a></h3>
<ol>
<li class="">创建 <code>e2ee-worker.js</code>，实现 passthrough transform（不加密，只透传帧）</li>
<li class="">注册到 sender/receiver 的 <code>transform</code> 属性</li>
<li class="">确认视频通话仍正常——验证 Transform 管线可用</li>
<li class="">逐步加入 AES-GCM 加密逻辑</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-5对比-data-channel-与-srtp-的-dtls-用法">Lab 5：对比 Data Channel 与 SRTP 的 DTLS 用法<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#lab-5%E5%AF%B9%E6%AF%94-data-channel-%E4%B8%8E-srtp-%E7%9A%84-dtls-%E7%94%A8%E6%B3%95" class="hash-link" aria-label="Direct link to Lab 5：对比 Data Channel 与 SRTP 的 DTLS 用法" title="Direct link to Lab 5：对比 Data Channel 与 SRTP 的 DTLS 用法" translate="no">​</a></h3>
<ol>
<li class="">同一 PeerConnection 上同时开启音视频 + Data Channel</li>
<li class="">Wireshark 中观察：Data Channel 消息在 DTLS Application Data 中，RTP 在 SRTP 加密层</li>
<li class="">理解「一个 DTLS 会话，两种数据路径」</li>
</ol>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九本章小结">九、本章小结<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#%E4%B9%9D%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="Direct link to 九、本章小结" title="Direct link to 九、本章小结" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">要点</th><th style="text-align:left">内容</th></tr></thead><tbody><tr><td style="text-align:left">安全底线</td><td style="text-align:left">WebRTC 媒体<strong>强制加密</strong>，不可协商关闭 SRTP</td></tr><tr><td style="text-align:left">身份验证</td><td style="text-align:left">自签名证书 + SDP fingerprint 交叉验证，非 CA 链</td></tr><tr><td style="text-align:left">密钥来源</td><td style="text-align:left">DTLS 握手 → RFC 5764 KDF → SRTP Master Key + Salt</td></tr><tr><td style="text-align:left">加密范围</td><td style="text-align:left">RTP 载荷加密 + 认证；RTP 头明文</td></tr><tr><td style="text-align:left">E2EE</td><td style="text-align:left">Insertable Streams 在 SRTP 之内再加一层，SFU 无法解密</td></tr></tbody></table>
<p><strong>下一篇（Ch8）</strong>：<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输</a>——SRTP 加密保护的 RTP 包如何承载 Opus/VP8 帧，以及 RTCP 如何反馈丢包与驱动拥塞控制。</p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8827" target="_blank" rel="noopener noreferrer" class="">RFC 8827 — WebRTC Security Architecture</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc6347" target="_blank" rel="noopener noreferrer" class="">RFC 6347 — Datagram Transport Layer Security</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc5764" target="_blank" rel="noopener noreferrer" class="">RFC 5764 — DTLS Extension to Establish Keys for SRTP</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc3711" target="_blank" rel="noopener noreferrer" class="">RFC 3711 — The Secure Real-time Transport Protocol (SRTP)</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc7714" target="_blank" rel="noopener noreferrer" class="">RFC 7714 — AES-GCM for SRTP</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8122" target="_blank" rel="noopener noreferrer" class="">RFC 8122 — DTLS-SRTP in SDP</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史 / 安全动机</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpScriptTransform" target="_blank" rel="noopener noreferrer" class="">MDN — RTCRtpScriptTransform</a></li>
<li class=""><a href="https://www.w3.org/TR/webrtc-encoded-transform/" target="_blank" rel="noopener noreferrer" class="">W3C — WebRTC Encoded Transform</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>实时通信</category>
            <category>Security</category>
            <category>Deep Dive</category>
            <category>TLS</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (6)：Data Channel 与 SCTP over DTLS]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-data-channel</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-data-channel</guid>
            <pubDate>Wed, 17 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[RTCDataChannel API、SCTP 多路复用、有序/无序传输与 P2P 数据传输实战]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"媒体走 RTP，数据走 SCTP——WebRTC 用两条逻辑通道完成实时通信的全部载荷。"</p>
</blockquote>
<p>WebRTC 三大核心 API：<code>getUserMedia</code>（Ch1 采集媒体）、<code>RTCPeerConnection</code>（Ch2 建立连接）、<code>RTCDataChannel</code>（本章 传输任意数据）。Data Channel 让你在已建立的 <strong>DTLS 加密通道</strong> 上，以 P2P 方式传输文本、二进制、文件——无需额外服务器中转。</p>
<p>与 MBONE 多播时代「一份数据广播给所有人」不同（见 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious — 历史</a>），Data Channel 是<strong>单播 P2P</strong> 模型：每个 PeerConnection 只有两端，数据经 ICE 选出的最优路径直达对端——或经 TURN 中继（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">Ch5</a>）。Ron Frederick 曾设想用 RTP + IP 多播做文件传输——「原始的做种者可以立即将多播流发送到所有接收者」——但 WebRTC 选择了更务实的路径：在已建立的 DTLS 通道上用 SCTP 做可靠/部分可靠的消息传输。</p>
<p>本章覆盖 RTCDataChannel API、SCTP over DTLS 协议栈、DCEP 建立协议、有序/无序传输、背压控制、文件分片传输，以及与 WebSocket 的架构对比。</p>
<p>配套代码：<code>examples/webrtc-lab/client/ch06-data-channel/</code></p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>RTCDataChannel</strong></td><td style="text-align:left">—</td><td style="text-align:left">WebRTC 的数据传输 API，在 PeerConnection 上创建逻辑数据通道</td></tr><tr><td style="text-align:left"><strong>SCTP</strong></td><td style="text-align:left">Stream Control Transmission Protocol</td><td style="text-align:left">面向消息的传输协议，支持多流、有序/无序、部分可靠性</td></tr><tr><td style="text-align:left"><strong>DTLS</strong></td><td style="text-align:left">Datagram Transport Layer Security</td><td style="text-align:left">基于 UDP 的 TLS，WebRTC 的加密层，见 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">Ch7</a></td></tr><tr><td style="text-align:left"><strong>SCTP over DTLS</strong></td><td style="text-align:left">—</td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8832" target="_blank" rel="noopener noreferrer" class="">RFC 8832</a>，SCTP 载荷封装在 DTLS 记录中</td></tr><tr><td style="text-align:left"><strong>DCEP</strong></td><td style="text-align:left">Data Channel Establishment Protocol</td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8832" target="_blank" rel="noopener noreferrer" class="">RFC 8832</a> §6，SCTP 上建立 Data Channel 的控制协议</td></tr><tr><td style="text-align:left"><strong>ordered</strong></td><td style="text-align:left">—</td><td style="text-align:left">是否保证消息按发送顺序到达</td></tr><tr><td style="text-align:left"><strong>maxRetransmits</strong></td><td style="text-align:left">—</td><td style="text-align:left">最大重传次数，<code>0</code> 表示不重传（类 UDP）</td></tr><tr><td style="text-align:left"><strong>maxPacketLifeTime</strong></td><td style="text-align:left">—</td><td style="text-align:left">消息最大存活时间（毫秒），超时丢弃</td></tr><tr><td style="text-align:left"><strong>binaryType</strong></td><td style="text-align:left">—</td><td style="text-align:left">接收二进制消息时的 JS 类型：<code>blob</code> 或 <code>arraybuffer</code></td></tr><tr><td style="text-align:left"><strong>bufferedAmount</strong></td><td style="text-align:left">—</td><td style="text-align:left">尚未发送完成的字节数，用于背压控制</td></tr><tr><td style="text-align:left"><strong>negotiated</strong></td><td style="text-align:left">—</td><td style="text-align:left">是否由 SDP 协商创建（而非 <code>createDataChannel</code>）</td></tr><tr><td style="text-align:left"><strong>label</strong></td><td style="text-align:left">—</td><td style="text-align:left">Data Channel 的人类可读名称，用于区分多个通道</td></tr><tr><td style="text-align:left"><strong>id</strong></td><td style="text-align:left">—</td><td style="text-align:left">Data Channel 的数字标识符（0-65534），多通道时必填</td></tr><tr><td style="text-align:left"><strong>PPID</strong></td><td style="text-align:left">Payload Protocol Identifier</td><td style="text-align:left">SCTP 层标识消息类型（字符串/二进制/空）</td></tr><tr><td style="text-align:left"><strong>partial reliability</strong></td><td style="text-align:left">部分可靠性</td><td style="text-align:left">SCTP 允许配置「最多重传 N 次」或「最多存活 T 毫秒」</td></tr><tr><td style="text-align:left"><strong>usrsctp</strong></td><td style="text-align:left">—</td><td style="text-align:left">用户空间 SCTP 实现，WebRTC 浏览器内核使用</td></tr><tr><td style="text-align:left"><strong>stream id</strong></td><td style="text-align:left">SCTP Stream ID</td><td style="text-align:left">SCTP 流标识，每个 Data Channel 映射到一个 stream</td></tr><tr><td style="text-align:left"><strong>DataChannel 状态</strong></td><td style="text-align:left">—</td><td style="text-align:left"><code>connecting</code> → <code>open</code> → <code>closing</code> → <code>closed</code></td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一协议栈sctp-over-dtls-over-ice">一、协议栈：SCTP over DTLS over ICE<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E4%B8%80%E5%8D%8F%E8%AE%AE%E6%A0%88sctp-over-dtls-over-ice" class="hash-link" aria-label="Direct link to 一、协议栈：SCTP over DTLS over ICE" title="Direct link to 一、协议栈：SCTP over DTLS over ICE" translate="no">​</a></h2>
<p>Data Channel 不是独立的 UDP 套接字，而是嵌套在 WebRTC 协议栈中：</p>
<!-- -->
<p>与 SRTP 媒体通道的关系：</p>
<!-- -->
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>关键认知</div><div class="admonitionContent_BuS1"><p>Data Channel 与 SRTP <strong>共享同一个 DTLS 会话和 ICE 传输</strong>。ICE 连通（Ch5）后，DTLS 握手完成，SCTP 关联建立，Data Channel 才能 <code>open</code>。因此 <code>dc.onopen</code> 通常晚于 <code>iceConnectionState=connected</code>。</p></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="11-为什么选-sctp-而非-tcp">1.1 为什么选 SCTP 而非 TCP<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#11-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%89-sctp-%E8%80%8C%E9%9D%9E-tcp" class="hash-link" aria-label="Direct link to 1.1 为什么选 SCTP 而非 TCP" title="Direct link to 1.1 为什么选 SCTP 而非 TCP" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">特性</th><th style="text-align:left">TCP</th><th style="text-align:left">SCTP</th><th style="text-align:left">WebRTC 需求</th></tr></thead><tbody><tr><td style="text-align:left">消息边界</td><td style="text-align:left">字节流，需自行分帧</td><td style="text-align:left"><strong>原生消息边界</strong></td><td style="text-align:left">游戏状态、文件分片</td></tr><tr><td style="text-align:left">多路复用</td><td style="text-align:left">一个连接一个流</td><td style="text-align:left"><strong>多 stream</strong></td><td style="text-align:left">多个 Data Channel</td></tr><tr><td style="text-align:left">队头阻塞</td><td style="text-align:left">有</td><td style="text-align:left">按 stream 隔离</td><td style="text-align:left">文件传输不阻塞聊天</td></tr><tr><td style="text-align:left">部分可靠性</td><td style="text-align:left">无</td><td style="text-align:left"><strong>可配置</strong></td><td style="text-align:left">位置更新可丢包</td></tr><tr><td style="text-align:left">NAT 友好</td><td style="text-align:left">需额外封装</td><td style="text-align:left">封装在 DTLS/UDP 中</td><td style="text-align:left">与 ICE 兼容</td></tr></tbody></table>
<p>WebRTC 没有在 UDP 上直接跑 TCP（如 TCP-over-UDP），而是选择了 SCTP——IETF 为 WebRTC 专门定义了 SCTP over DTLS（<a href="https://datatracker.ietf.org/doc/html/rfc8831" target="_blank" rel="noopener noreferrer" class="">RFC 8831</a>、<a href="https://datatracker.ietf.org/doc/html/rfc8832" target="_blank" rel="noopener noreferrer" class="">RFC 8832</a>）。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二sctp-关联建立与-dcep">二、SCTP 关联建立与 DCEP<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E4%BA%8Csctp-%E5%85%B3%E8%81%94%E5%BB%BA%E7%AB%8B%E4%B8%8E-dcep" class="hash-link" aria-label="Direct link to 二、SCTP 关联建立与 DCEP" title="Direct link to 二、SCTP 关联建立与 DCEP" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-sctp-四路握手">2.1 SCTP 四路握手<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#21-sctp-%E5%9B%9B%E8%B7%AF%E6%8F%A1%E6%89%8B" class="hash-link" aria-label="Direct link to 2.1 SCTP 四路握手" title="Direct link to 2.1 SCTP 四路握手" translate="no">​</a></h3>
<p>DTLS 握手完成后，双方 SCTP 栈交换 INIT / INIT-ACK / COOKIE-ECHO / COOKIE-ACK，建立 SCTP 关联（association）：</p>
<!-- -->
<p>浏览器的 SCTP 实现基于 <strong>usrsctp</strong>（用户空间 SCTP 库），由 WebRTC 原生层封装，JavaScript 层只看到 <code>RTCDataChannel</code> API。</p>
<p>SCTP 公共头（<a href="https://datatracker.ietf.org/doc/html/rfc4960#section-3.1" target="_blank" rel="noopener noreferrer" class="">RFC 4960 §3.1</a>）：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="22-dcepdata-channel-establishment-protocol">2.2 DCEP：Data Channel Establishment Protocol<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#22-dcepdata-channel-establishment-protocol" class="hash-link" aria-label="Direct link to 2.2 DCEP：Data Channel Establishment Protocol" title="Direct link to 2.2 DCEP：Data Channel Establishment Protocol" translate="no">​</a></h3>
<p>Data Channel 不是 SCTP 关联建立就自动可用——需要 <strong>DCEP</strong> 在 SCTP 上协商每个 channel 的参数：</p>
<table><thead><tr><th style="text-align:left">DCEP 消息</th><th style="text-align:left">方向</th><th style="text-align:left">内容</th></tr></thead><tbody><tr><td style="text-align:left"><strong>OPEN</strong></td><td style="text-align:left">发起方 → 应答方</td><td style="text-align:left">label、protocol、ordered、maxRetransmits/maxPacketLifeTime、stream id</td></tr><tr><td style="text-align:left"><strong>ACK</strong></td><td style="text-align:left">应答方 → 发起方</td><td style="text-align:left">确认 channel 建立</td></tr></tbody></table>
<p>发起方调用 <code>createDataChannel("chat")</code> 后，浏览器在 SCTP 上自动发送 DCEP OPEN；应答方 <code>ondatachannel</code> 触发，回复 ACK 后双方 <code>readyState</code> 变为 <code>open</code>。</p>
<p>DCEP OPEN 固定头（<a href="https://datatracker.ietf.org/doc/html/rfc8832#section-6.1" target="_blank" rel="noopener noreferrer" class="">RFC 8832 §6.1</a>），后跟可变长 Label 与 Protocol 字符串：</p>
<!-- -->
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三为什么需要-data-channel">三、为什么需要 Data Channel<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E4%B8%89%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-data-channel" class="hash-link" aria-label="Direct link to 三、为什么需要 Data Channel" title="Direct link to 三、为什么需要 Data Channel" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">通道</th><th style="text-align:left">路径</th><th style="text-align:left">延迟</th><th style="text-align:left">典型用途</th></tr></thead><tbody><tr><td style="text-align:left"><strong>WebSocket</strong></td><td style="text-align:left">客户端 ↔ 服务器</td><td style="text-align:left">取决于服务器位置</td><td style="text-align:left">信令、聊天、推送</td></tr><tr><td style="text-align:left"><strong>HTTP/REST</strong></td><td style="text-align:left">客户端 ↔ 服务器</td><td style="text-align:left">高（请求-响应）</td><td style="text-align:left">文件上传、API</td></tr><tr><td style="text-align:left"><strong>Data Channel</strong></td><td style="text-align:left">P2P（或 TURN 中继）</td><td style="text-align:left"><strong>最低</strong></td><td style="text-align:left">游戏状态、白板、文件、传感器</td></tr></tbody></table>
<p>Data Channel 的核心价值：</p>
<ol>
<li class=""><strong>零服务器中转</strong>：数据直达对端，服务器带宽成本为零</li>
<li class=""><strong>与媒体同步</strong>：游戏/协作场景中，状态更新与音视频在同一连接上</li>
<li class=""><strong>灵活可靠性</strong>：可按消息配置有序/无序、可靠/部分可靠</li>
<li class=""><strong>多路复用</strong>：一个 PeerConnection 上可开多个 Data Channel（如 chat + file + control）</li>
</ol>
<!-- -->
<p><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 历史</a> 中 Ron Frederick 的 Spacewar 多播游戏是 Data Channel 的远古祖先——多个客户端广播飞船位置，所有人实时看到彼此。今天 Data Channel 用单播 SCTP 实现了类似效果，但不需要 MBONE 多播网络。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四rtcdatachannel-api-详解">四、RTCDataChannel API 详解<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E5%9B%9Brtcdatachannel-api-%E8%AF%A6%E8%A7%A3" class="hash-link" aria-label="Direct link to 四、RTCDataChannel API 详解" title="Direct link to 四、RTCDataChannel API 详解" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-发起方createdatachannel">4.1 发起方：createDataChannel<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#41-%E5%8F%91%E8%B5%B7%E6%96%B9createdatachannel" class="hash-link" aria-label="Direct link to 4.1 发起方：createDataChannel" title="Direct link to 4.1 发起方：createDataChannel" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">ICE_SERVERS</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> dc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createDataChannel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"chat"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">ordered</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">maxRetransmits</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword nil" style="color:#00009f">undefined</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">protocol</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">""</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">negotiated</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword nil" style="color:#00009f">undefined</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onopen</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Data Channel open, readyState:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">readyState</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Hello P2P!"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onclose</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Data Channel closed"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onerror</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Data Channel error:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onmessage</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Received:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">typeof</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-应答方ondatachannel">4.2 应答方：ondatachannel<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#42-%E5%BA%94%E7%AD%94%E6%96%B9ondatachannel" class="hash-link" aria-label="Direct link to 4.2 应答方：ondatachannel" title="Direct link to 4.2 应答方：ondatachannel" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">ondatachannel</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> channel </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">channel</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Incoming channel:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> channel</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">label</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> channel</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">id</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  channel</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onopen</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Remote-initiated channel open"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  channel</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onmessage</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Message:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="43-完整建立时序">4.3 完整建立时序<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#43-%E5%AE%8C%E6%95%B4%E5%BB%BA%E7%AB%8B%E6%97%B6%E5%BA%8F" class="hash-link" aria-label="Direct link to 4.3 完整建立时序" title="Direct link to 4.3 完整建立时序" translate="no">​</a></h3>
<!-- -->
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>时序陷阱</div><div class="admonitionContent_BuS1"><p>必须在 <code>createOffer()</code> <strong>之前</strong> 调用 <code>createDataChannel()</code>，否则 SDP 中不会包含 SCTP 协商信息，对端收不到 Data Channel。</p></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="44-在-ch02-基础上添加-data-channel">4.4 在 ch02 基础上添加 Data Channel<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#44-%E5%9C%A8-ch02-%E5%9F%BA%E7%A1%80%E4%B8%8A%E6%B7%BB%E5%8A%A0-data-channel" class="hash-link" aria-label="Direct link to 4.4 在 ch02 基础上添加 Data Channel" title="Direct link to 4.4 在 ch02 基础上添加 Data Channel" translate="no">​</a></h3>
<p>基于 <code>examples/webrtc-lab/client/ch02-p2p-basic/</code>，最小改动即可支持 Data Channel：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">ICE_SERVERS</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 仅 Caller 创建 Data Channel</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">role </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"caller"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    dc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createDataChannel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"chat"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onopen</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"DC open"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onmessage</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"DC msg:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">ondatachannel</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    dc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">channel</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onopen</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"DC open (remote-initiated)"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onmessage</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">ev</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"DC msg:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> ev</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// ... 其余 ICE / track 逻辑不变</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五传输模式有序无序与可靠性">五、传输模式：有序/无序与可靠性<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E4%BA%94%E4%BC%A0%E8%BE%93%E6%A8%A1%E5%BC%8F%E6%9C%89%E5%BA%8F%E6%97%A0%E5%BA%8F%E4%B8%8E%E5%8F%AF%E9%9D%A0%E6%80%A7" class="hash-link" aria-label="Direct link to 五、传输模式：有序/无序与可靠性" title="Direct link to 五、传输模式：有序/无序与可靠性" translate="no">​</a></h2>
<p>SCTP 的核心优势是<strong>按消息（message-oriented）</strong> 而非按字节流（TCP），且每个消息可独立配置可靠性：</p>
<!-- -->
<table><thead><tr><th style="text-align:left">配置</th><th style="text-align:left">行为</th><th style="text-align:left">类比</th><th style="text-align:left">适用场景</th></tr></thead><tbody><tr><td style="text-align:left"><code>ordered: true</code>（默认）</td><td style="text-align:left">保证顺序</td><td style="text-align:left">TCP</td><td style="text-align:left">聊天、文件传输</td></tr><tr><td style="text-align:left"><code>ordered: false</code></td><td style="text-align:left">不保证顺序</td><td style="text-align:left">—</td><td style="text-align:left">游戏位置更新（只要最新）</td></tr><tr><td style="text-align:left"><code>maxRetransmits: 0</code></td><td style="text-align:left">不重传</td><td style="text-align:left">UDP</td><td style="text-align:left">实时传感器、心跳</td></tr><tr><td style="text-align:left"><code>maxRetransmits: 3</code></td><td style="text-align:left">最多重传 3 次</td><td style="text-align:left">部分可靠</td><td style="text-align:left">可容忍偶尔丢包的状态</td></tr><tr><td style="text-align:left"><code>maxPacketLifeTime: 1000</code></td><td style="text-align:left">1 秒内未送达则丢弃</td><td style="text-align:left">—</td><td style="text-align:left">实时性优先于完整性</td></tr></tbody></table>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> chat </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createDataChannel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"chat"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ordered</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> position </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createDataChannel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"position"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">ordered</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">maxRetransmits</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> draw </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createDataChannel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"draw"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">ordered</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">maxRetransmits</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> metrics </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createDataChannel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"metrics"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">ordered</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">maxPacketLifeTime</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">500</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>maxRetransmits 与 maxPacketLifeTime 互斥</div><div class="admonitionContent_BuS1"><p><a href="https://w3c.github.io/webrtc-pc/#dom-rtcdatachannelinit" target="_blank" rel="noopener noreferrer" class="">W3C 规范</a> 规定两者不能同时设置。选择其一即可。</p></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-ordered-的行为细节">5.1 ordered<!-- -->:false<!-- --> 的行为细节<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#51-ordered-%E7%9A%84%E8%A1%8C%E4%B8%BA%E7%BB%86%E8%8A%82" class="hash-link" aria-label="Direct link to 51-ordered-的行为细节" title="Direct link to 51-ordered-的行为细节" translate="no">​</a></h3>
<p>当 <code>ordered: false</code> 时，SCTP 允许新消息「跳过」因丢包而卡住的重传队列：</p>
<!-- -->
<p>这在游戏位置同步中是期望行为——过时的位置数据没有价值，与其等待重传不如处理最新状态。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六binarytype-与消息类型">六、binaryType 与消息类型<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E5%85%ADbinarytype-%E4%B8%8E%E6%B6%88%E6%81%AF%E7%B1%BB%E5%9E%8B" class="hash-link" aria-label="Direct link to 六、binaryType 与消息类型" title="Direct link to 六、binaryType 与消息类型" translate="no">​</a></h2>
<p><code>RTCDataChannel.send()</code> 接受 <code>string</code>、<code>Blob</code>、<code>ArrayBuffer</code> 或 <code>ArrayBufferView</code>。接收端通过 <code>binaryType</code> 属性控制二进制消息的 JS 类型：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Hello, world!"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> buffer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Uint8Array</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0x48</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x65</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x6c</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x6c</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x6f</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">buffer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">binaryType</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"arraybuffer"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onmessage</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">typeof</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"string"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Text:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">else</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">instanceof</span><span class="token plain"> </span><span class="token class-name">ArrayBuffer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> view </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Uint8Array</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Binary:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> view</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">length</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"bytes"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">binaryType</th><th style="text-align:left">接收类型</th><th style="text-align:left">适用</th></tr></thead><tbody><tr><td style="text-align:left"><code>"blob"</code>（默认）</td><td style="text-align:left"><code>Blob</code></td><td style="text-align:left">大文件、图片，可延迟读取</td></tr><tr><td style="text-align:left"><code>"arraybuffer"</code></td><td style="text-align:left"><code>ArrayBuffer</code></td><td style="text-align:left">需要立即解析二进制协议</td></tr></tbody></table>
<p>SCTP 层通过 PPID 区分消息类型：</p>
<table><thead><tr><th style="text-align:left">PPID</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left">51</td><td style="text-align:left">UTF-8 字符串</td></tr><tr><td style="text-align:left">53</td><td style="text-align:left">二进制（部分可靠）</td></tr><tr><td style="text-align:left">54</td><td style="text-align:left">二进制（可靠）</td></tr><tr><td style="text-align:left">56</td><td style="text-align:left">字符串（部分可靠）</td></tr><tr><td style="text-align:left">57</td><td style="text-align:left">空消息</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="61-应用层协议设计建议">6.1 应用层协议设计建议<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#61-%E5%BA%94%E7%94%A8%E5%B1%82%E5%8D%8F%E8%AE%AE%E8%AE%BE%E8%AE%A1%E5%BB%BA%E8%AE%AE" class="hash-link" aria-label="Direct link to 6.1 应用层协议设计建议" title="Direct link to 6.1 应用层协议设计建议" translate="no">​</a></h3>
<p>Data Channel 没有内置的消息分帧——<code>send()</code> 的每次调用对应一条 SCTP 消息。设计二进制协议时建议：</p>
<!-- -->
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 推荐：长度前缀帧格式</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">encodeMessage</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">type</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> payload</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> header </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">ArrayBuffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> view </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">DataView</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">header</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  view</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setUint8</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  view</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setUint32</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> payload</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">byteLength</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> combined </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Uint8Array</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">5</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> payload</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">byteLength</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  combined</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">set</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Uint8Array</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">header</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  combined</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">set</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Uint8Array</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">payload</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">5</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> combined</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">buffer</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// type: 0x01=chat, 0x02=file-chunk, 0x03=control</span><br></div></code></pre></div></div></div>
<p>文本消息可直接 <code>JSON.stringify</code> + <code>send()</code>，二进制数据用 <code>ArrayBuffer</code>。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七背压控制bufferedamount">七、背压控制：bufferedAmount<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E4%B8%83%E8%83%8C%E5%8E%8B%E6%8E%A7%E5%88%B6bufferedamount" class="hash-link" aria-label="Direct link to 七、背压控制：bufferedAmount" title="Direct link to 七、背压控制：bufferedAmount" translate="no">​</a></h2>
<p><code>send()</code> 是<strong>异步非阻塞</strong>的——调用后数据进入 SCTP 发送缓冲区，而非立即发出。如果发送速度超过网络吞吐，<code>bufferedAmount</code> 会持续增长，可能导致内存溢出。</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="71-背压控制模式">7.1 背压控制模式<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#71-%E8%83%8C%E5%8E%8B%E6%8E%A7%E5%88%B6%E6%A8%A1%E5%BC%8F" class="hash-link" aria-label="Direct link to 7.1 背压控制模式" title="Direct link to 7.1 背压控制模式" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">CHUNK_SIZE</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">16</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1024</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">LOW_THRESHOLD</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">256</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1024</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bufferedAmountLowThreshold</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">LOW_THRESHOLD</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onbufferedamountlow</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">pumpSendQueue</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sendQueue </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">enqueueSend</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  sendQueue</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">pumpSendQueue</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">pumpSendQueue</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">while</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sendQueue</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">length</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">readyState</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"open"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bufferedAmount</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">LOW_THRESHOLD</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> chunk </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> sendQueue</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">shift</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">chunk</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="72-关键属性">7.2 关键属性<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#72-%E5%85%B3%E9%94%AE%E5%B1%9E%E6%80%A7" class="hash-link" aria-label="Direct link to 7.2 关键属性" title="Direct link to 7.2 关键属性" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">属性/事件</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left"><code>bufferedAmount</code></td><td style="text-align:left">当前缓冲区中未发送的字节数</td></tr><tr><td style="text-align:left"><code>bufferedAmountLowThreshold</code></td><td style="text-align:left">触发 <code>bufferedamountlow</code> 的阈值</td></tr><tr><td style="text-align:left"><code>bufferedamountlow</code></td><td style="text-align:left"><code>bufferedAmount</code> 降到阈值以下时触发</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="73-消息大小限制">7.3 消息大小限制<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#73-%E6%B6%88%E6%81%AF%E5%A4%A7%E5%B0%8F%E9%99%90%E5%88%B6" class="hash-link" aria-label="Direct link to 7.3 消息大小限制" title="Direct link to 7.3 消息大小限制" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">浏览器</th><th style="text-align:left">最大消息大小</th><th style="text-align:left">备注</th></tr></thead><tbody><tr><td style="text-align:left">Chrome</td><td style="text-align:left">256 KB</td><td style="text-align:left">SDP <code>a=max-message-size:262144</code></td></tr><tr><td style="text-align:left">Firefox</td><td style="text-align:left">256 KB</td><td style="text-align:left">同 Chrome</td></tr><tr><td style="text-align:left">Safari</td><td style="text-align:left">256 KB</td><td style="text-align:left">同 Chrome</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>常见陷阱</div><div class="admonitionContent_BuS1"><ol>
<li class=""><strong>不检查 bufferedAmount 就循环 send</strong> → 内存暴涨，Tab 崩溃</li>
<li class=""><strong>send 超过 256KB 的单条消息</strong> → 抛出异常，大文件必须分片</li>
<li class=""><strong>在 readyState !== "open" 时 send</strong> → 抛出 InvalidStateError</li>
<li class=""><strong>忽略 onerror</strong> → 静默丢数据</li>
<li class=""><strong>背压阈值设太大</strong> → 内存占用高；设太小 → 吞吐低</li>
</ol><p>生产文件传输务必分片（16 KB 是经验值），并配合 <code>bufferedAmount</code> 控制发送速率。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八文件传输完整示例">八、文件传输完整示例<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E5%85%AB%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%AE%8C%E6%95%B4%E7%A4%BA%E4%BE%8B" class="hash-link" aria-label="Direct link to 八、文件传输完整示例" title="Direct link to 八、文件传输完整示例" translate="no">​</a></h2>
<p>以下是一个带进度、背压、校验的生产级文件传输实现：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">P2PFileTransfer</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">constructor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">dataChannel</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dc</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> dataChannel</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">binaryType</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"arraybuffer"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bufferedAmountLowThreshold</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">256</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1024</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">CHUNK_SIZE</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">16</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">*</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1024</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sendQueue</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">metadata</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">receivedChunks</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">receivedBytes</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onbufferedamountlow</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">_pumpSend</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onmessage</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">_onMessage</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">sendFile</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">file</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> meta </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"file-start"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">name</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> file</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">name</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">size</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> file</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">size</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">mime</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> file</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">meta</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> buffer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> file</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">arrayBuffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> offset </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> offset </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain"> buffer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">byteLength</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> offset </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">CHUNK_SIZE</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sendQueue</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">buffer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">slice</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offset</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> offset </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">CHUNK_SIZE</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">_pumpSend</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">_pumpSend</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">while</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sendQueue</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">length</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">readyState</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"open"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bufferedAmount</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bufferedAmountLowThreshold</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sendQueue</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">shift</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sendQueue</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">length</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bufferedAmount</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"file-end"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onSendComplete</span><span class="token operator" style="color:#393A34">?.</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">_onMessage</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">typeof</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"string"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> msg </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">parse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"file-start"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">metadata</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> msg</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">receivedChunks</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">receivedBytes</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onReceiveStart</span><span class="token operator" style="color:#393A34">?.</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">else</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"file-end"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">_assembleFile</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">else</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">receivedChunks</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">receivedBytes</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+=</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">byteLength</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onProgress</span><span class="token operator" style="color:#393A34">?.</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">receivedBytes</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">metadata</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">size</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">_assembleFile</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> blob </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Blob</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">receivedChunks</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">metadata</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mime</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onReceiveComplete</span><span class="token operator" style="color:#393A34">?.</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">blob</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">metadata</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="81-传输时序">8.1 传输时序<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#81-%E4%BC%A0%E8%BE%93%E6%97%B6%E5%BA%8F" class="hash-link" aria-label="Direct link to 8.1 传输时序" title="Direct link to 8.1 传输时序" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="82-生产增强">8.2 生产增强<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#82-%E7%94%9F%E4%BA%A7%E5%A2%9E%E5%BC%BA" class="hash-link" aria-label="Direct link to 8.2 生产增强" title="Direct link to 8.2 生产增强" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">增强</th><th style="text-align:left">实现</th></tr></thead><tbody><tr><td style="text-align:left">校验</td><td style="text-align:left">发送前计算 SHA-256，file-end 携带 hash，接收方验证</td></tr><tr><td style="text-align:left">断点续传</td><td style="text-align:left">file-start 带 offset，接收方告知已收字节数</td></tr><tr><td style="text-align:left">取消</td><td style="text-align:left">发送 <code>file-abort</code> 控制消息，清空 sendQueue</td></tr><tr><td style="text-align:left">限速</td><td style="text-align:left">动态调整 CHUNK_SIZE 或 LOW_THRESHOLD</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九多-data-channel-与-negotiated-模式">九、多 Data Channel 与 negotiated 模式<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E4%B9%9D%E5%A4%9A-data-channel-%E4%B8%8E-negotiated-%E6%A8%A1%E5%BC%8F" class="hash-link" aria-label="Direct link to 九、多 Data Channel 与 negotiated 模式" title="Direct link to 九、多 Data Channel 与 negotiated 模式" translate="no">​</a></h2>
<p>一个 PeerConnection 可创建多个 Data Channel，SCTP 在底层做流多路复用：</p>
<!-- -->
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> chat </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createDataChannel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"chat"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ordered</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> file </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createDataChannel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"file"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ordered</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> control </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createDataChannel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"control"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">ordered</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">maxRetransmits</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">ondatachannel</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> label </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">channel</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">switch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">label</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"chat"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">setupChat</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">channel</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"file"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">setupFileTransfer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">channel</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"control"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">setupControl</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">channel</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<p><strong>negotiated 模式</strong>：双方各自调用 <code>createDataChannel</code> 并指定相同 <code>id</code>：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> dc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createDataChannel</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"sync"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">negotiated</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">id</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">ordered</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">模式</th><th style="text-align:left">谁创建</th><th style="text-align:left">适用</th></tr></thead><tbody><tr><td style="text-align:left">默认（negotiated: false）</td><td style="text-align:left">仅发起方 createDataChannel</td><td style="text-align:left">1v1 通话，Caller 决定开哪些通道</td></tr><tr><td style="text-align:left">negotiated: true</td><td style="text-align:left">双方都 createDataChannel</td><td style="text-align:left">SFU/Mesh，双方对称创建</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>negotiated 模式的 id 冲突</div><div class="admonitionContent_BuS1"><p>双方必须使用<strong>相同 id</strong> 且<strong>相同参数</strong>（ordered、maxRetransmits 等）。id 范围 0–65534，id 65535 保留给 SCTP 控制。重复 id 会导致关联失败。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十data-channel-vs-websocket">十、Data Channel vs WebSocket<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E5%8D%81data-channel-vs-websocket" class="hash-link" aria-label="Direct link to 十、Data Channel vs WebSocket" title="Direct link to 十、Data Channel vs WebSocket" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">维度</th><th style="text-align:left">Data Channel</th><th style="text-align:left">WebSocket</th></tr></thead><tbody><tr><td style="text-align:left"><strong>路径</strong></td><td style="text-align:left">P2P（或 TURN 中继）</td><td style="text-align:left">客户端 ↔ 服务器</td></tr><tr><td style="text-align:left"><strong>延迟</strong></td><td style="text-align:left">最低（直连）</td><td style="text-align:left">+1 跳服务器 RTT</td></tr><tr><td style="text-align:left"><strong>可靠性</strong></td><td style="text-align:left">可配置（有序/无序/部分可靠）</td><td style="text-align:left">始终可靠有序（TCP）</td></tr><tr><td style="text-align:left"><strong>服务端</strong></td><td style="text-align:left">不需要（信令除外）</td><td style="text-align:left">必须</td></tr><tr><td style="text-align:left"><strong>连接数扩展</strong></td><td style="text-align:left">每对 Peer 一个 PC</td><td style="text-align:left">服务器连接数 = 用户数</td></tr><tr><td style="text-align:left"><strong>防火墙</strong></td><td style="text-align:left">依赖 ICE/STUN/TURN</td><td style="text-align:left">标准 HTTPS 端口</td></tr><tr><td style="text-align:left"><strong>消息大小</strong></td><td style="text-align:left">~256 KB/消息</td><td style="text-align:left">无硬性限制</td></tr><tr><td style="text-align:left"><strong>广播</strong></td><td style="text-align:left">不支持（需 Mesh/SFU）</td><td style="text-align:left">服务器可广播</td></tr><tr><td style="text-align:left"><strong>持久化</strong></td><td style="text-align:left">无（P2P 断开即失）</td><td style="text-align:left">服务器可存储</td></tr></tbody></table>
<p><strong>最佳实践架构</strong>：</p>
<!-- -->
<ul>
<li class=""><strong>信令</strong> → WebSocket（必须经服务器）</li>
<li class=""><strong>媒体</strong> → SRTP（P2P 或 SFU）</li>
<li class=""><strong>大流量数据</strong> → Data Channel（P2P 优先）</li>
<li class=""><strong>需要持久化/广播的消息</strong> → WebSocket + 服务器</li>
</ul>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>混合策略</div><div class="admonitionContent_BuS1"><p>Slack/Discord 类应用：聊天消息走 WebSocket（持久化、搜索、离线推送），语音走 WebRTC SRTP，屏幕共享标注走 Data Channel（低延迟）。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十一data-channel-状态与生命周期">十一、Data Channel 状态与生命周期<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E5%8D%81%E4%B8%80data-channel-%E7%8A%B6%E6%80%81%E4%B8%8E%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F" class="hash-link" aria-label="Direct link to 十一、Data Channel 状态与生命周期" title="Direct link to 十一、Data Channel 状态与生命周期" translate="no">​</a></h2>
<!-- -->
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">readyState</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// "connecting" | "open" | "closing" | "closed"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">close</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">close</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 级联关闭所有 Data Channel</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">事件</th><th style="text-align:left">触发时机</th></tr></thead><tbody><tr><td style="text-align:left"><code>onopen</code></td><td style="text-align:left">SCTP 关联建立 + DCEP 完成，可以 send</td></tr><tr><td style="text-align:left"><code>onmessage</code></td><td style="text-align:left">收到对端消息</td></tr><tr><td style="text-align:left"><code>onclose</code></td><td style="text-align:left">通道关闭</td></tr><tr><td style="text-align:left"><code>onerror</code></td><td style="text-align:left">传输错误</td></tr><tr><td style="text-align:left"><code>onbufferedamountlow</code></td><td style="text-align:left">发送缓冲区降到阈值以下</td></tr></tbody></table>
<p>Data Channel <strong>不能</strong>在 <code>closed</code> 后重新 open——需要创建新的 Data Channel 或重建 PeerConnection。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十二sdp-中的-data-channel-协商">十二、SDP 中的 Data Channel 协商<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E5%8D%81%E4%BA%8Csdp-%E4%B8%AD%E7%9A%84-data-channel-%E5%8D%8F%E5%95%86" class="hash-link" aria-label="Direct link to 十二、SDP 中的 Data Channel 协商" title="Direct link to 十二、SDP 中的 Data Channel 协商" translate="no">​</a></h2>
<p>Data Channel 在 SDP 中通过 <code>m=application</code> 行描述：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">m=application 9 UDP/DTLS/SCTP webrtc-datachannel</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">c=IN IP4 0.0.0.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=ice-ufrag:...</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=ice-pwd:...</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=fingerprint:sha-256 ...</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=setup:actpass</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=mid:2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=sctp-port:5000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=max-message-size:262144</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">属性</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left"><code>m=application</code></td><td style="text-align:left">非媒体 m-line，标识 Data Channel</td></tr><tr><td style="text-align:left"><code>UDP/DTLS/SCTP</code></td><td style="text-align:left">协议栈</td></tr><tr><td style="text-align:left"><code>webrtc-datachannel</code></td><td style="text-align:left">SCTP 载荷格式</td></tr><tr><td style="text-align:left"><code>a=sctp-port:5000</code></td><td style="text-align:left">SCTP 端口号（占位，实际由 DTLS 承载）</td></tr><tr><td style="text-align:left"><code>a=max-message-size:262144</code></td><td style="text-align:left">最大消息 256 KB</td></tr></tbody></table>
<p>发起方 <code>createDataChannel</code> 后再 <code>createOffer</code>，浏览器自动在 SDP 中加入上述字段。应答方 <code>setRemoteDescription</code> 时触发 <code>ondatachannel</code>。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十三常见问题与排查">十三、常见问题与排查<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E5%8D%81%E4%B8%89%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E4%B8%8E%E6%8E%92%E6%9F%A5" class="hash-link" aria-label="Direct link to 十三、常见问题与排查" title="Direct link to 十三、常见问题与排查" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">问题</th><th style="text-align:left">原因</th><th style="text-align:left">解决</th></tr></thead><tbody><tr><td style="text-align:left">对端收不到 ondatachannel</td><td style="text-align:left">createDataChannel 在 createOffer 之后</td><td style="text-align:left">调整调用顺序</td></tr><tr><td style="text-align:left">dc.readyState 一直 connecting</td><td style="text-align:left">ICE/DTLS 未完成</td><td style="text-align:left">检查 Ch5 ICE 状态</td></tr><tr><td style="text-align:left">send 抛 InvalidStateError</td><td style="text-align:left">readyState 不是 open</td><td style="text-align:left">等 onopen 回调</td></tr><tr><td style="text-align:left">大文件传输 Tab 崩溃</td><td style="text-align:left">未做背压控制</td><td style="text-align:left">检查 bufferedAmount</td></tr><tr><td style="text-align:left">消息乱序</td><td style="text-align:left">ordered: false</td><td style="text-align:left">预期行为；需要则改 true</td></tr><tr><td style="text-align:left">二进制收到 Blob 而非 ArrayBuffer</td><td style="text-align:left">binaryType 默认 blob</td><td style="text-align:left">设 <code>binaryType = "arraybuffer"</code></td></tr><tr><td style="text-align:left">双向都需要发数据</td><td style="text-align:left">只有一方 createDataChannel</td><td style="text-align:left">用 negotiated 模式或双方都创建</td></tr><tr><td style="text-align:left">TURN 下传输慢</td><td style="text-align:left">中继带宽限制</td><td style="text-align:left">正常；对比 P2P 模式延迟</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="131-chromewebrtc-internals-调试">13.1 chrome://webrtc-internals 调试<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#131-chromewebrtc-internals-%E8%B0%83%E8%AF%95" class="hash-link" aria-label="Direct link to 13.1 chrome://webrtc-internals 调试" title="Direct link to 13.1 chrome://webrtc-internals 调试" translate="no">​</a></h3>
<ol>
<li class="">打开 <code>chrome://webrtc-internals</code></li>
<li class="">找到 PeerConnection → <strong>dataChannels</strong> 部分</li>
<li class="">查看每个 channel 的 <code>label</code>、<code>id</code>、<code>state</code>、<code>messagesSent/Received</code></li>
<li class="">在 <strong>Stats</strong> 中搜索 <code>sctp</code> 相关条目</li>
</ol>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">report</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"data-channel"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"DC:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">label</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token string" style="color:#e3116c">"sent:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">messagesSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token string" style="color:#e3116c">"received:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">messagesReceived</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token string" style="color:#e3116c">"bytes:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesSent</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"transport"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"SCTP state:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sctpState</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十四实战-lab">十四、实战 Lab<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E5%8D%81%E5%9B%9B%E5%AE%9E%E6%88%98-lab" class="hash-link" aria-label="Direct link to 十四、实战 Lab" title="Direct link to 十四、实战 Lab" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-1最小-data-channel-聊天">Lab 1：最小 Data Channel 聊天<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#lab-1%E6%9C%80%E5%B0%8F-data-channel-%E8%81%8A%E5%A4%A9" class="hash-link" aria-label="Direct link to Lab 1：最小 Data Channel 聊天" title="Direct link to Lab 1：最小 Data Channel 聊天" translate="no">​</a></h3>
<ol>
<li class="">启动 <code>examples/webrtc-lab/signaling/</code> 信令服务器</li>
<li class="">打开 <code>client/ch06-data-channel/</code>（或基于 ch02 添加 Data Channel，见 §4.4）</li>
<li class="">Caller 调用 <code>createDataChannel("chat")</code> 后 <code>createOffer</code></li>
<li class="">双方互发文本消息，确认 <code>onmessage</code> 触发</li>
<li class="">在 webrtc-internals 中确认 Data Channel state = open</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-2有序-vs-无序对比">Lab 2：有序 vs 无序对比<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#lab-2%E6%9C%89%E5%BA%8F-vs-%E6%97%A0%E5%BA%8F%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to Lab 2：有序 vs 无序对比" title="Direct link to Lab 2：有序 vs 无序对比" translate="no">​</a></h3>
<ol>
<li class="">创建两个 Channel：<code>ordered: true</code> 和 <code>ordered: false, maxRetransmits: 0</code></li>
<li class="">快速连续 send 100 条带序号的消息</li>
<li class="">在接收端对比：ordered 通道序号连续，unordered 可能乱序/丢包</li>
<li class="">用 Chrome DevTools → Network → Throttling 模拟 3G 丢包，效果更明显</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-3文件传输--背压">Lab 3：文件传输 + 背压<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#lab-3%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93--%E8%83%8C%E5%8E%8B" class="hash-link" aria-label="Direct link to Lab 3：文件传输 + 背压" title="Direct link to Lab 3：文件传输 + 背压" translate="no">​</a></h3>
<ol>
<li class="">选择 10 MB 以上文件</li>
<li class="">实现 §八 的 <code>P2PFileTransfer</code> 类</li>
<li class="">对比「有背压」vs「无背压」的内存占用（Chrome Task Manager）</li>
<li class="">添加进度条 UI</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-4binarytype-实验">Lab 4：binaryType 实验<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#lab-4binarytype-%E5%AE%9E%E9%AA%8C" class="hash-link" aria-label="Direct link to Lab 4：binaryType 实验" title="Direct link to Lab 4：binaryType 实验" translate="no">​</a></h3>
<ol>
<li class="">分别设置 <code>binaryType = "blob"</code> 和 <code>"arraybuffer"</code></li>
<li class="">发送 PNG 图片二进制</li>
<li class="">对比接收端处理方式的差异（Blob 需 <code>arrayBuffer()</code> 异步读取）</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-5多-data-channel">Lab 5：多 Data Channel<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#lab-5%E5%A4%9A-data-channel" class="hash-link" aria-label="Direct link to Lab 5：多 Data Channel" title="Direct link to Lab 5：多 Data Channel" translate="no">​</a></h3>
<ol>
<li class="">同时创建 chat（id=0）、file（id=1）、ping（id=2, unordered）</li>
<li class="">在 file 通道传大文件的同时，通过 chat 通道发消息——验证互不阻塞</li>
<li class="">在 webrtc-internals 中确认三个 data channel 条目</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-6turn-中继下的-data-channel">Lab 6：TURN 中继下的 Data Channel<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#lab-6turn-%E4%B8%AD%E7%BB%A7%E4%B8%8B%E7%9A%84-data-channel" class="hash-link" aria-label="Direct link to Lab 6：TURN 中继下的 Data Channel" title="Direct link to Lab 6：TURN 中继下的 Data Channel" translate="no">​</a></h3>
<ol>
<li class="">部署 coturn（<code>examples/webrtc-lab/docker/coturn/</code>）</li>
<li class="">设置 <code>iceTransportPolicy: "relay"</code> 强制 TURN</li>
<li class="">确认 Data Channel 仍正常工作（数据经 TURN 中继）</li>
<li class="">用 <code>performance.now()</code> 对比 P2P vs TURN 模式下 1MB 文件传输耗时</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-7sctp-统计与监控">Lab 7：SCTP 统计与监控<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#lab-7sctp-%E7%BB%9F%E8%AE%A1%E4%B8%8E%E7%9B%91%E6%8E%A7" class="hash-link" aria-label="Direct link to Lab 7：SCTP 统计与监控" title="Direct link to Lab 7：SCTP 统计与监控" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">setInterval</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"data-channel"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">table</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">label</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">label</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">state</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">messagesSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">messagesSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">messagesReceived</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">messagesReceived</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesReceived</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesReceived</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十五本章小结">十五、本章小结<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#%E5%8D%81%E4%BA%94%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="Direct link to 十五、本章小结" title="Direct link to 十五、本章小结" translate="no">​</a></h2>
<p>Phase 2（连接建立）到此完成：</p>
<table><thead><tr><th style="text-align:left">章节</th><th style="text-align:left">内容</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:left">Ch3</td><td style="text-align:left">信令服务器</td><td style="text-align:center">✅</td></tr><tr><td style="text-align:left">Ch4</td><td style="text-align:left">SDP 协商</td><td style="text-align:center">✅</td></tr><tr><td style="text-align:left">Ch5</td><td style="text-align:left">ICE/STUN/TURN</td><td style="text-align:center">✅</td></tr><tr><td style="text-align:left">Ch6</td><td style="text-align:left">Data Channel</td><td style="text-align:center">✅</td></tr></tbody></table>
<p>Phase 3 将深入媒体与安全内核，从 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密（Ch7）</a> 开始。</p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-data-channel#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8831" target="_blank" rel="noopener noreferrer" class="">RFC 8831 — WebRTC Data Channels</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8832" target="_blank" rel="noopener noreferrer" class="">RFC 8832 — SCTP-based Media Transport in the Datagram Transport Layer Security (DTLS) Protocol</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8834" target="_blank" rel="noopener noreferrer" class="">RFC 8834 — SCTP over DTLS for WebRTC</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc4960" target="_blank" rel="noopener noreferrer" class="">RFC 4960 — SCTP Specification</a></li>
<li class=""><a href="https://w3c.github.io/webrtc-pc/#rtcdatachannel" target="_blank" rel="noopener noreferrer" class="">W3C — RTCDataChannel</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel" target="_blank" rel="noopener noreferrer" class="">MDN — RTCDataChannel</a></li>
<li class=""><a href="https://webrtcforthecurious.com/docs/06-data-communication/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — Data Channel</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史 / 多播 vs 单播</a></li>
<li class=""><a href="https://html.spec.whatwg.org/multipage/webappapis.html#dom-datachannelinit" target="_blank" rel="noopener noreferrer" class="">HTML Spec — RTCDataChannelInit</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>实时通信</category>
            <category>教程</category>
            <category>Deep Dive</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (5)：ICE、STUN、TURN 与 NAT 穿透]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn</guid>
            <pubDate>Tue, 16 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[ICE 状态机、Candidate 类型、STUN/TURN 原理、NAT 穿透失败排查与多播到单播的设计转折]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"70% 的 WebRTC bug 与 NAT 有关。" — 业界经验总结</p>
</blockquote>
<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">Ch4 SDP</a> 中的 <code>a=ice-ufrag</code>、<code>a=ice-pwd</code> 只是凭证；真正让两个浏览器在 NAT 后面找到彼此、建立 UDP 通道的，是 <strong>ICE（Interactive Connectivity Establishment）</strong>——<a href="https://datatracker.ietf.org/doc/html/rfc8445" target="_blank" rel="noopener noreferrer" class="">RFC 8445</a> 定义的标准算法。</p>
<p>1990 年代 MBONE 多播时代，发送者只需向多播组发一次包，路由器负责复制给所有订阅者。Ron Frederick 在 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史</a> 中回忆：他和同事都是 IP 多播研究者，用 <code>nv</code> 工具向整个 Internet 广播会议视频——一份数据包，数百个子网同时接收。Serge Lachapelle 则描述了另一条演进路线：他创办的 Marratech 最初也依赖多播网络，「服务器可以非常简单，因为网络负责把视频包复制给通话中的每一个人」——但「必须设计网络以适配多播模式」这一致命缺点，最终推动行业从多播转向 SFU（packet shufflers）。</p>
<p>WebRTC 运行在<strong>没有多播的公网</strong>上。IPv4 地址耗尽催生了 NAT 的大规模部署，每个参与者必须找到与对端通信的<strong>具体 IP<!-- -->:Port<!-- --> 路径</strong>。从「一对多广播」到「点对点单播」的设计转折，是理解 ICE 存在原因的关键背景。</p>
<p>本章深入 ICE 状态机、Candidate 类型、STUN/TURN 协议细节、Trickle ICE 优化，以及生产环境中最常见的穿透失败排查路径。</p>
<p>配套 Lab：<code>examples/webrtc-lab/docker/coturn/</code> + <code>client/ch02-p2p-basic/</code></p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>ICE</strong></td><td style="text-align:left">Interactive Connectivity Establishment</td><td style="text-align:left">在多个 Candidate 地址之间做连通性检查，选出最优传输路径的标准算法</td></tr><tr><td style="text-align:left"><strong>Candidate</strong></td><td style="text-align:left">ICE Candidate</td><td style="text-align:left">一个可用的网络地址（IP<!-- -->:Port<!-- --> + 类型 + 优先级），供 ICE 检查</td></tr><tr><td style="text-align:left"><strong>host</strong></td><td style="text-align:left">Host Candidate</td><td style="text-align:left">本机网卡直接绑定的地址，如 <code>192.168.1.10:54321</code></td></tr><tr><td style="text-align:left"><strong>srflx</strong></td><td style="text-align:left">Server Reflexive</td><td style="text-align:left">经 STUN 服务器反射得到的公网映射地址</td></tr><tr><td style="text-align:left"><strong>prflx</strong></td><td style="text-align:left">Peer Reflexive</td><td style="text-align:left">连通性检查过程中动态发现的远端映射地址</td></tr><tr><td style="text-align:left"><strong>relay</strong></td><td style="text-align:left">Relay Candidate</td><td style="text-align:left">TURN 服务器分配的中继地址，P2P 失败时的兜底</td></tr><tr><td style="text-align:left"><strong>STUN</strong></td><td style="text-align:left">Session Traversal Utilities for NAT</td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc5389" target="_blank" rel="noopener noreferrer" class="">RFC 5389</a>，帮助客户端发现自己的公网映射</td></tr><tr><td style="text-align:left"><strong>TURN</strong></td><td style="text-align:left">Traversal Using Relays around NAT</td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc5766" target="_blank" rel="noopener noreferrer" class="">RFC 5766</a>，在服务端分配中继地址转发流量</td></tr><tr><td style="text-align:left"><strong>Connectivity Check</strong></td><td style="text-align:left">—</td><td style="text-align:left">ICE 用 STUN Binding Request 探测 Candidate Pair 是否可达</td></tr><tr><td style="text-align:left"><strong>Nomination</strong></td><td style="text-align:left">提名</td><td style="text-align:left">ICE 选出「最终使用」的 Candidate Pair 的过程</td></tr><tr><td style="text-align:left"><strong>Trickle ICE</strong></td><td style="text-align:left">—</td><td style="text-align:left">边收集 Candidate 边通过信令发送，而非等全部收集完</td></tr><tr><td style="text-align:left"><strong>NAT</strong></td><td style="text-align:left">Network Address Translation</td><td style="text-align:left">将私网地址映射到公网地址/端口的中间设备</td></tr><tr><td style="text-align:left"><strong>Symmetric NAT</strong></td><td style="text-align:left">—</td><td style="text-align:left">每个目标地址分配不同外部端口，P2P 几乎必然失败</td></tr><tr><td style="text-align:left"><strong>CGNAT</strong></td><td style="text-align:left">Carrier-Grade NAT</td><td style="text-align:left">运营商级 NAT，多层 NAT 嵌套，穿透难度更高</td></tr><tr><td style="text-align:left"><strong>ice-ufrag / ice-pwd</strong></td><td style="text-align:left">—</td><td style="text-align:left">SDP 中的 ICE 凭证，用于连通性检查的身份验证</td></tr><tr><td style="text-align:left"><strong>Candidate Pair</strong></td><td style="text-align:left">—</td><td style="text-align:left">本地 Candidate + 远端 Candidate 的组合，ICE 逐个检查</td></tr><tr><td style="text-align:left"><strong>iceTransportPolicy</strong></td><td style="text-align:left">—</td><td style="text-align:left"><code>all</code>（默认）或 <code>relay</code>（强制走 TURN）</td></tr><tr><td style="text-align:left"><strong>ICE-CONTROLLING</strong></td><td style="text-align:left">—</td><td style="text-align:left">连通性检查中拥有提名权的一方（Offer 方通常为 controlling）</td></tr><tr><td style="text-align:left"><strong>ICE-Lite</strong></td><td style="text-align:left">—</td><td style="text-align:left">简化版 ICE 实现，仅被动响应检查，不主动探测</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一从多播到单播为什么-ice-存在">一、从多播到单播：为什么 ICE 存在<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E4%B8%80%E4%BB%8E%E5%A4%9A%E6%92%AD%E5%88%B0%E5%8D%95%E6%92%AD%E4%B8%BA%E4%BB%80%E4%B9%88-ice-%E5%AD%98%E5%9C%A8" class="hash-link" aria-label="Direct link to 一、从多播到单播：为什么 ICE 存在" title="Direct link to 一、从多播到单播：为什么 ICE 存在" translate="no">​</a></h2>
<p><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 历史章节</a> 勾勒出一条清晰的技术演进线：</p>
<table><thead><tr><th style="text-align:left">时代</th><th style="text-align:left">模型</th><th style="text-align:left">带宽效率</th><th style="text-align:left">网络要求</th></tr></thead><tbody><tr><td style="text-align:left">MBONE / <code>nv</code> (1992)</td><td style="text-align:left">IP 多播</td><td style="text-align:left">发送方只发一份</td><td style="text-align:left">全网支持多播</td></tr><tr><td style="text-align:left">Marratech (2000s)</td><td style="text-align:left">多播 + 简单服务器</td><td style="text-align:left">高</td><td style="text-align:left">企业网多播</td></tr><tr><td style="text-align:left">SFU / WebRTC (2010+)</td><td style="text-align:left">单播 P2P 或 SFU 转发</td><td style="text-align:left">每跳一份</td><td style="text-align:left">仅需 UDP 出站</td></tr></tbody></table>
<p>Ron Frederick 坦言：「有时我希望我们之前能更加努力地推动 IP 多播的应用……如果我们这么做了，可能早就可以看到有线电视过渡到基于 Internet 的音频和视频。」但现实是公网 ISP 几乎不转发多播，IPv4 地址耗尽又催生了 NAT 的大规模部署。</p>
<!-- -->
<p>每个 WebRTC 参与者可能有多个网络接口（Wi-Fi、以太网、VPN、IPv6），每个接口经 NAT 映射后又有不同的公网地址。ICE 的任务就是：<strong>枚举所有可能的路径，逐个探测，选出延迟最低、可用的那条</strong>。</p>
<p>Serge Lachapelle 在 Google 收购 Global IP Solutions（GIPS）后，把 VoIP 技术栈搬进浏览器——GIPS 的 libjingle 提供了 ICE/STUN/TURN 的成熟实现，这正是今天 Chrome 内置 ICE Agent 的根基。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二ice-在连接建立中的位置">二、ICE 在连接建立中的位置<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E4%BA%8Cice-%E5%9C%A8%E8%BF%9E%E6%8E%A5%E5%BB%BA%E7%AB%8B%E4%B8%AD%E7%9A%84%E4%BD%8D%E7%BD%AE" class="hash-link" aria-label="Direct link to 二、ICE 在连接建立中的位置" title="Direct link to 二、ICE 在连接建立中的位置" translate="no">​</a></h2>
<p>WebRTC 连接建立是一个多层协议栈的叠加过程。ICE 位于 SDP 协商之后、DTLS 握手之前：</p>
<!-- -->
<table><thead><tr><th style="text-align:left">阶段</th><th style="text-align:left">协议</th><th style="text-align:center">本章是否涉及</th></tr></thead><tbody><tr><td style="text-align:left">信令</td><td style="text-align:left">WebSocket / 自定义</td><td style="text-align:center">间接（传递 Candidate）</td></tr><tr><td style="text-align:left">媒体协商</td><td style="text-align:left">SDP Offer/Answer</td><td style="text-align:center">Ch4</td></tr><tr><td style="text-align:left"><strong>地址发现与选路</strong></td><td style="text-align:left"><strong>ICE + STUN + TURN</strong></td><td style="text-align:center"><strong>本章</strong></td></tr><tr><td style="text-align:left">加密</td><td style="text-align:left">DTLS</td><td style="text-align:center">Ch7</td></tr><tr><td style="text-align:left">媒体传输</td><td style="text-align:left">SRTP / SCTP</td><td style="text-align:center">Ch6/Ch8</td></tr></tbody></table>
<p>ICE 不传输任何应用数据——它只负责在 UDP（偶尔 TCP）上找到一条可达的五元组 <code>(srcIP, srcPort, dstIP, dstPort, UDP)</code>。找到之后，DTLS 在同一五元组上握手，SRTP/SCTP 复用该通道。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三ice-状态机详解">三、ICE 状态机详解<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E4%B8%89ice-%E7%8A%B6%E6%80%81%E6%9C%BA%E8%AF%A6%E8%A7%A3" class="hash-link" aria-label="Direct link to 三、ICE 状态机详解" title="Direct link to 三、ICE 状态机详解" translate="no">​</a></h2>
<p>浏览器通过 <code>RTCPeerConnection.iceConnectionState</code> 暴露 ICE 连接状态：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-各状态含义">3.1 各状态含义<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#31-%E5%90%84%E7%8A%B6%E6%80%81%E5%90%AB%E4%B9%89" class="hash-link" aria-label="Direct link to 3.1 各状态含义" title="Direct link to 3.1 各状态含义" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">状态</th><th style="text-align:left">含义</th><th style="text-align:left">典型持续时间</th></tr></thead><tbody><tr><td style="text-align:left"><code>new</code></td><td style="text-align:left">尚未开始 ICE 检查</td><td style="text-align:left">—</td></tr><tr><td style="text-align:left"><code>checking</code></td><td style="text-align:left">正在对 Candidate Pair 做连通性检查</td><td style="text-align:left">数百 ms ~ 数秒</td></tr><tr><td style="text-align:left"><code>connected</code></td><td style="text-align:left">至少一个 Pair 成功，媒体可传输</td><td style="text-align:left">—</td></tr><tr><td style="text-align:left"><code>completed</code></td><td style="text-align:left">提名完成，不再切换 Pair</td><td style="text-align:left">稳定连接后</td></tr><tr><td style="text-align:left"><code>disconnected</code></td><td style="text-align:left">临时断连，可能自动恢复</td><td style="text-align:left">数秒</td></tr><tr><td style="text-align:left"><code>failed</code></td><td style="text-align:left">所有 Pair 失败，需人工介入</td><td style="text-align:left">终止</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-icegatheringstate-与-connectionstate">3.2 iceGatheringState 与 connectionState<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#32-icegatheringstate-%E4%B8%8E-connectionstate" class="hash-link" aria-label="Direct link to 3.2 iceGatheringState 与 connectionState" title="Direct link to 3.2 iceGatheringState 与 connectionState" translate="no">​</a></h3>
<p>ICE 涉及三个平行的状态维度，调试时缺一不可：</p>
<table><thead><tr><th style="text-align:left">属性</th><th style="text-align:left">反映层次</th><th style="text-align:left">关键值</th></tr></thead><tbody><tr><td style="text-align:left"><code>iceGatheringState</code></td><td style="text-align:left">Candidate 收集进度</td><td style="text-align:left"><code>new</code> → <code>gathering</code> → <code>complete</code></td></tr><tr><td style="text-align:left"><code>iceConnectionState</code></td><td style="text-align:left">ICE 连通性</td><td style="text-align:left"><code>checking</code> → <code>connected</code> / <code>failed</code></td></tr><tr><td style="text-align:left"><code>connectionState</code></td><td style="text-align:left">ICE + DTLS 整体</td><td style="text-align:left"><code>connecting</code> → <code>connected</code> / <code>failed</code></td></tr></tbody></table>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">ICE_SERVERS</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">oniceconnectionstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> state </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceConnectionState</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"[ICE]"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> state</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">switch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">state</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"checking"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"正在探测 Candidate Pair…"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"connected"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"completed"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ICE 连通，DTLS 应已开始"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"disconnected"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">warn</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"临时断连，等待恢复或触发 failed"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"failed"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ICE 失败 — 检查 TURN 配置、防火墙、Candidate 类型"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onicegatheringstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"[Gathering]"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceGatheringState</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onconnectionstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"[Connection]"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">connectionState</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// connectionState=failed 但 iceConnectionState=connected → DTLS 问题，见 Ch7</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onicecandidate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    signaling</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">candidate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">else</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ICE gathering complete (null candidate)"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>iceConnectionState vs connectionState</div><div class="admonitionContent_BuS1"><p><code>iceConnectionState</code> 只反映 ICE 层；<code>connectionState</code> 还包含 DTLS 状态。调试时两者都要看——可能出现 ICE <code>connected</code> 但 <code>connectionState=failed</code>（DTLS 证书/指纹不匹配）。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四candidate-类型与优先级">四、Candidate 类型与优先级<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E5%9B%9Bcandidate-%E7%B1%BB%E5%9E%8B%E4%B8%8E%E4%BC%98%E5%85%88%E7%BA%A7" class="hash-link" aria-label="Direct link to 四、Candidate 类型与优先级" title="Direct link to 四、Candidate 类型与优先级" translate="no">​</a></h2>
<p>ICE 为每个 Candidate 计算优先级，<strong>host &gt; srflx &gt; relay</strong>（同类型内还有协议、接口偏好等细粒度排序）：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-candidate-sdp-格式">4.1 Candidate SDP 格式<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#41-candidate-sdp-%E6%A0%BC%E5%BC%8F" class="hash-link" aria-label="Direct link to 4.1 Candidate SDP 格式" title="Direct link to 4.1 Candidate SDP 格式" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=candidate:842163049 1 udp 2130706431 192.168.1.10 54321 typ host</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=candidate:842163049 2 udp 2130706431 192.168.1.10 54321 typ host</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=candidate:1234567890 1 udp 1694498815 1.2.3.4 54321 typ srflx raddr 192.168.1.10 rport 54321</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=candidate:9876543210 1 udp 16777215 5.6.7.8 60000 typ relay raddr 1.2.3.4 rport 54321</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=candidate:1111111111 1 udp 2130706431 abcd1234-5678-90ab.local 54321 typ host</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">示例</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left">foundation</td><td style="text-align:left"><code>842163049</code></td><td style="text-align:left">相同类型+地址的 Candidate 共享 foundation</td></tr><tr><td style="text-align:left">component</td><td style="text-align:left"><code>1</code> / <code>2</code></td><td style="text-align:left">1=RTP，2=RTCP（WebRTC 用 rtcp-mux 时通常只有 1）</td></tr><tr><td style="text-align:left">transport</td><td style="text-align:left"><code>udp</code> / <code>tcp</code></td><td style="text-align:left">传输协议</td></tr><tr><td style="text-align:left">priority</td><td style="text-align:left"><code>2130706431</code></td><td style="text-align:left">优先级数值，越大越优先</td></tr><tr><td style="text-align:left">address</td><td style="text-align:left"><code>192.168.1.10</code></td><td style="text-align:left">IP 地址</td></tr><tr><td style="text-align:left">port</td><td style="text-align:left"><code>54321</code></td><td style="text-align:left">端口</td></tr><tr><td style="text-align:left">typ</td><td style="text-align:left"><code>host</code> / <code>srflx</code> / <code>relay</code> / <code>prflx</code></td><td style="text-align:left">Candidate 类型</td></tr><tr><td style="text-align:left">raddr / rport</td><td style="text-align:left">srflx/relay 附带</td><td style="text-align:left">本地映射前的地址</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-优先级计算公式rfc-8445">4.2 优先级计算公式（RFC 8445）<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#42-%E4%BC%98%E5%85%88%E7%BA%A7%E8%AE%A1%E7%AE%97%E5%85%AC%E5%BC%8Frfc-8445" class="hash-link" aria-label="Direct link to 4.2 优先级计算公式（RFC 8445）" title="Direct link to 4.2 优先级计算公式（RFC 8445）" translate="no">​</a></h3>
<p>优先级是一个 32 位无符号整数，公式为：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">priority = (2^24) × (126 - typePreference)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">         + (2^8)  × (256 - localPreference)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">         + (256 - componentId)</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">typePreference</th><th style="text-align:left">类型</th><th style="text-align:center">值</th></tr></thead><tbody><tr><td style="text-align:left">host</td><td style="text-align:left">本机</td><td style="text-align:center">126</td></tr><tr><td style="text-align:left">prflx</td><td style="text-align:left">对端反射</td><td style="text-align:center">110</td></tr><tr><td style="text-align:left">srflx</td><td style="text-align:left">服务器反射</td><td style="text-align:center">100</td></tr><tr><td style="text-align:left">relay</td><td style="text-align:left">中继</td><td style="text-align:center">0</td></tr></tbody></table>
<p>因此 host candidate 的 priority 天然高于 srflx，srflx 高于 relay。浏览器自动计算，开发者通常无需手动干预。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="43-mdns-host-candidate">4.3 mDNS Host Candidate<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#43-mdns-host-candidate" class="hash-link" aria-label="Direct link to 4.3 mDNS Host Candidate" title="Direct link to 4.3 mDNS Host Candidate" translate="no">​</a></h3>
<p>Chrome 从 M94 起默认用 <strong>mDNS</strong> 隐藏本地 IP：host candidate 的地址显示为 <code>xxxx-xxxx.local</code> 而非真实 <code>192.168.x.x</code>。这是隐私保护特性，不影响 srflx/relay 的收集和 ICE 选路。在 <code>chrome://webrtc-internals</code> 中你会看到这类 candidate，属于正常现象。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="44-配置-iceservers">4.4 配置 iceServers<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#44-%E9%85%8D%E7%BD%AE-iceservers" class="hash-link" aria-label="Direct link to 4.4 配置 iceServers" title="Direct link to 4.4 配置 iceServers" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">ICE_SERVERS</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"stun:stun.l.google.com:19302"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"stun:stun1.l.google.com:19302"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token string" style="color:#e3116c">"turn:turn.example.com:3478?transport=udp"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token string" style="color:#e3116c">"turn:turn.example.com:3478?transport=tcp"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token string" style="color:#e3116c">"turns:turn.example.com:5349?transport=tcp"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">username</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"webrtc-user"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">credential</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"secret-token"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">credentialType</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"password"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">ICE_SERVERS</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">iceCandidatePoolSize</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">iceTransportPolicy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"all"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">bundlePolicy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"max-bundle"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">配置项</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:left"><code>iceServers</code></td><td style="text-align:left">STUN/TURN 服务器列表</td></tr><tr><td style="text-align:left"><code>iceCandidatePoolSize</code></td><td style="text-align:left">在 <code>createOffer</code> 前预收集 N 个 Candidate，加速首连</td></tr><tr><td style="text-align:left"><code>iceTransportPolicy: "relay"</code></td><td style="text-align:left">隐私模式或对称 NAT 测试，禁用 host/srflx</td></tr><tr><td style="text-align:left"><code>bundlePolicy: "max-bundle"</code></td><td style="text-align:left">所有 m-line 复用同一 ICE 传输（WebRTC 默认行为）</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五stun-协议深度解析rfc-5389">五、STUN 协议深度解析（RFC 5389）<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E4%BA%94stun-%E5%8D%8F%E8%AE%AE%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90rfc-5389" class="hash-link" aria-label="Direct link to 五、STUN 协议深度解析（RFC 5389）" title="Direct link to 五、STUN 协议深度解析（RFC 5389）" translate="no">​</a></h2>
<p>STUN 的核心消息只有几种：<strong>Binding Request / Binding Response</strong>，用于地址发现。Allocate / Send / Data 等属于 TURN 扩展（RFC 5766）。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-binding-交互时序">5.1 Binding 交互时序<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#51-binding-%E4%BA%A4%E4%BA%92%E6%97%B6%E5%BA%8F" class="hash-link" aria-label="Direct link to 5.1 Binding 交互时序" title="Direct link to 5.1 Binding 交互时序" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-stun-消息结构简化">5.2 STUN 消息结构（简化）<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#52-stun-%E6%B6%88%E6%81%AF%E7%BB%93%E6%9E%84%E7%AE%80%E5%8C%96" class="hash-link" aria-label="Direct link to 5.2 STUN 消息结构（简化）" title="Direct link to 5.2 STUN 消息结构（简化）" translate="no">​</a></h3>
<p><a href="https://datatracker.ietf.org/doc/html/rfc5389" target="_blank" rel="noopener noreferrer" class="">RFC 5389</a> 固定头 20 字节，后跟可变长 Attributes（TURN Allocate 等复用同一头部格式）。首 16 bit 的 <strong>Message Type</strong> 按位域编码（<a href="https://datatracker.ietf.org/doc/html/rfc5389#section-6.1" target="_blank" rel="noopener noreferrer" class="">§6.1</a>）：</p>
<!-- -->
<table><thead><tr><th style="text-align:left">Class (C0C1)</th><th style="text-align:left">值</th><th style="text-align:left">典型消息</th></tr></thead><tbody><tr><td style="text-align:left"><code>00</code></td><td style="text-align:left">Request</td><td style="text-align:left">STUN Binding Request、TURN Allocate</td></tr><tr><td style="text-align:left"><code>01</code></td><td style="text-align:left">Indication</td><td style="text-align:left">TURN Send/Data Indication</td></tr><tr><td style="text-align:left"><code>10</code></td><td style="text-align:left">Success Response</td><td style="text-align:left">Binding Success Response</td></tr><tr><td style="text-align:left"><code>11</code></td><td style="text-align:left">Error Response</td><td style="text-align:left">401 Unauthorized 等</td></tr></tbody></table>
<p>Method 12 bit 标识具体操作，例如 <strong>Binding = 0x001</strong>、<strong>Allocate = 0x003</strong>（TURN）。Class + Method 组合成线上 16 bit Message Type 字段——前两 bit 恒为 0 是 STUN 与旧版 STUN 的兼容标记，不可省略。</p>
<p>常见 Attribute：</p>
<table><thead><tr><th style="text-align:left">Attribute</th><th style="text-align:left">用途</th></tr></thead><tbody><tr><td style="text-align:left"><code>XOR-MAPPED-ADDRESS</code></td><td style="text-align:left">客户端的公网映射地址（防 NAT 篡改）</td></tr><tr><td style="text-align:left"><code>USERNAME</code> / <code>MESSAGE-INTEGRITY</code></td><td style="text-align:left">ICE 连通性检查时的身份验证</td></tr><tr><td style="text-align:left"><code>PRIORITY</code> / <code>USE-CANDIDATE</code></td><td style="text-align:left">ICE 提名与优先级</td></tr><tr><td style="text-align:left"><code>ICE-CONTROLLED</code> / <code>ICE-CONTROLLING</code></td><td style="text-align:left">决定哪端执行提名</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="53-ice-连通性检查中的-stun">5.3 ICE 连通性检查中的 STUN<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#53-ice-%E8%BF%9E%E9%80%9A%E6%80%A7%E6%A3%80%E6%9F%A5%E4%B8%AD%E7%9A%84-stun" class="hash-link" aria-label="Direct link to 5.3 ICE 连通性检查中的 STUN" title="Direct link to 5.3 ICE 连通性检查中的 STUN" translate="no">​</a></h3>
<p>Candidate 收集阶段的 STUN Binding 不带 ICE 凭证；<strong>连通性检查</strong>阶段的 STUN Binding 则携带 SDP 中的 <code>ice-ufrag</code> 和 <code>ice-pwd</code>：</p>
<!-- -->
<ul>
<li class=""><strong>ICE-CONTROLLING</strong>（通常为 Offer 方）：有权发送 <code>USE-CANDIDATE</code> 提名</li>
<li class=""><strong>ICE-CONTROLLED</strong>（通常为 Answer 方）：被动响应，接受提名</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="54-公共-stun-服务器">5.4 公共 STUN 服务器<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#54-%E5%85%AC%E5%85%B1-stun-%E6%9C%8D%E5%8A%A1%E5%99%A8" class="hash-link" aria-label="Direct link to 5.4 公共 STUN 服务器" title="Direct link to 5.4 公共 STUN 服务器" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">提供者</th><th style="text-align:left">URL</th><th style="text-align:left">备注</th></tr></thead><tbody><tr><td style="text-align:left">Google</td><td style="text-align:left"><code>stun:stun.l.google.com:19302</code></td><td style="text-align:left">免费，无 SLA</td></tr><tr><td style="text-align:left">Cloudflare</td><td style="text-align:left"><code>stun:stun.cloudflare.com:3478</code></td><td style="text-align:left">免费</td></tr><tr><td style="text-align:left">自建</td><td style="text-align:left"><code>stun:your-domain:3478</code></td><td style="text-align:left">生产推荐（coturn 同时提供 STUN+TURN）</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>STUN 不转发媒体</div><div class="admonitionContent_BuS1"><p>STUN 只回答「你的公网地址是什么」。它不参与 RTP/SCTP 数据转发。如果 P2P 打洞失败，必须靠 TURN。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六turn-中继协议rfc-5766">六、TURN 中继协议（RFC 5766）<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E5%85%ADturn-%E4%B8%AD%E7%BB%A7%E5%8D%8F%E8%AE%AErfc-5766" class="hash-link" aria-label="Direct link to 六、TURN 中继协议（RFC 5766）" title="Direct link to 六、TURN 中继协议（RFC 5766）" translate="no">​</a></h2>
<p>当双方都是 Symmetric NAT，或企业防火墙只允许出站 UDP 时，P2P 必然失败。TURN 在服务端分配<strong>中继地址（relay candidate）</strong>，所有媒体经服务器转发。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="61-turn-allocate-时序">6.1 TURN Allocate 时序<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#61-turn-allocate-%E6%97%B6%E5%BA%8F" class="hash-link" aria-label="Direct link to 6.1 TURN Allocate 时序" title="Direct link to 6.1 TURN Allocate 时序" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="62-turn-认证机制">6.2 TURN 认证机制<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#62-turn-%E8%AE%A4%E8%AF%81%E6%9C%BA%E5%88%B6" class="hash-link" aria-label="Direct link to 6.2 TURN 认证机制" title="Direct link to 6.2 TURN 认证机制" translate="no">​</a></h3>
<p>生产 TURN 通常使用 <strong>长期凭证（long-term credentials）</strong> 或 <strong>REST API 临时凭证</strong>（Twilio、Xirsys 模式）：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 长期凭证（开发/内网）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"turn:turn.example.com:3478"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">username</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"test"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">credential</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"test123"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// REST 临时凭证（生产推荐）—— 服务端生成</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> crypto </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">require</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"crypto"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">generateTurnCredentials</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">secret</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> ttl </span><span class="token parameter operator" style="color:#393A34">=</span><span class="token parameter"> </span><span class="token parameter number" style="color:#36acaa">86400</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> timestamp </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">Math</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">floor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> ttl</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> username </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">timestamp</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">:userId123</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> hmac </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> crypto</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createHmac</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"sha1"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> secret</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  hmac</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">update</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">username</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> credential </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> hmac</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">digest</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"base64"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> username</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> credential </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 客户端消费</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> username</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> credential </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">fetch</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/api/turn-credentials"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">then</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">r</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">json</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"turn:turn.example.com:3478"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    username</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    credential</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<p>临时凭证的优势：即使泄露，凭证在 TTL 后自动失效；可按用户/会话隔离。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="63-coturn-docker-本地部署">6.3 coturn Docker 本地部署<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#63-coturn-docker-%E6%9C%AC%E5%9C%B0%E9%83%A8%E7%BD%B2" class="hash-link" aria-label="Direct link to 6.3 coturn Docker 本地部署" title="Direct link to 6.3 coturn Docker 本地部署" translate="no">​</a></h3>
<p><code>examples/webrtc-lab/docker/coturn/docker-compose.yml</code>：</p>
<div class="custom-code-block" data-language="yaml"><div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">services</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">coturn</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">image</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> coturn/coturn</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">latest</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">ports</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"3478:3478/udp"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"3478:3478/tcp"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"49152-49200:49152-49200/udp"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">command</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">&gt;</span><span class="token scalar string" style="color:#e3116c"></span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">      -n --log-file=stdout</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">      --lt-cred-mech</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">      --user=test:test123</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">      --realm=webrtc.lab</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">      --min-port=49152</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">      --max-port=49200</span><br></div></code></pre></div></div></div>
<p>启动与验证：</p>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token builtin class-name">cd</span><span class="token plain"> examples/webrtc-lab/docker/coturn</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">docker</span><span class="token plain"> compose up </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 验证 STUN 响应（需安装 stuntman）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">stunclient localhost </span><span class="token number" style="color:#36acaa">3478</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 查看 coturn 日志</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">docker</span><span class="token plain"> compose logs </span><span class="token parameter variable" style="color:#36acaa">-f</span><span class="token plain"> coturn</span><br></div></code></pre></div></div></div>
<p>浏览器端配置：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">LOCAL_TURN</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"turn:localhost:3478?transport=udp"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token string" style="color:#e3116c">"turn:localhost:3478?transport=tcp"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">username</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"test"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">credential</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"test123"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"stun:stun.l.google.com:19302"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token constant" style="color:#36acaa">LOCAL_TURN</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<p>部署成功后，在 <code>chrome://webrtc-internals</code> 中应能看到 <code>typ relay</code> 的 Candidate。生产集群扩展见 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">Ch14 TURN 集群</a>。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="64-turn-带宽成本">6.4 TURN 带宽成本<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#64-turn-%E5%B8%A6%E5%AE%BD%E6%88%90%E6%9C%AC" class="hash-link" aria-label="Direct link to 6.4 TURN 带宽成本" title="Direct link to 6.4 TURN 带宽成本" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">模式</th><th style="text-align:left">路径</th><th style="text-align:left">服务器带宽</th></tr></thead><tbody><tr><td style="text-align:left">P2P (srflx)</td><td style="text-align:left">A ↔ B 直连</td><td style="text-align:left">0</td></tr><tr><td style="text-align:left">TURN relay</td><td style="text-align:left">A → TURN → B</td><td style="text-align:left">发送量 × 2</td></tr><tr><td style="text-align:left">SFU</td><td style="text-align:left">每人 → SFU → 每人</td><td style="text-align:left">N × 发送量</td></tr></tbody></table>
<p>TURN 是兜底方案，但大流量场景（视频会议、文件传输）中 relay 比例直接影响成本。监控 relay 使用率是生产运维的关键指标。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七trickle-ice-vs-full-ice">七、Trickle ICE vs Full ICE<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E4%B8%83trickle-ice-vs-full-ice" class="hash-link" aria-label="Direct link to 七、Trickle ICE vs Full ICE" title="Direct link to 七、Trickle ICE vs Full ICE" translate="no">​</a></h2>
<p>早期 ICE 实现等所有 Candidate 收集完毕才交换 SDP，首连延迟高。<strong>Trickle ICE</strong>（<a href="https://datatracker.ietf.org/doc/html/rfc8838" target="_blank" rel="noopener noreferrer" class="">RFC 8838</a>）边收集边发送，是现代 WebRTC 的标准做法。</p>
<!-- -->
<table><thead><tr><th style="text-align:left">模式</th><th style="text-align:left">行为</th><th style="text-align:left">首连延迟</th><th style="text-align:left">信令复杂度</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Full ICE</strong></td><td style="text-align:left">等 <code>iceGatheringState=complete</code> 再发 SDP</td><td style="text-align:left">较慢（2~5s）</td><td style="text-align:left">低</td></tr><tr><td style="text-align:left"><strong>Trickle ICE</strong></td><td style="text-align:left"><code>onicecandidate</code> 实时转发</td><td style="text-align:left">快（&lt;1s 常见）</td><td style="text-align:left">中</td></tr><tr><td style="text-align:left"><strong>ICE Restart</strong></td><td style="text-align:left">断连后 <code>restartIce()</code> 重新收集</td><td style="text-align:left">取决于网络</td><td style="text-align:left">高</td></tr></tbody></table>
<p><code>examples/webrtc-lab/client/ch02-p2p-basic/</code> 使用 Full ICE（<code>waitIceGatheringComplete</code>），适合理解流程；生产信令服务器（Ch3）应实现 Trickle ICE。</p>
<p>SDP 中 <code>a=ice-options:trickle</code> 表示支持 Trickle ICE。信令层需处理 <code>candidate</code> 消息在 Offer/Answer 之前或之后到达的乱序情况：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pendingCandidates </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">onRemoteCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">remoteDescription</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addIceCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">else</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    pendingCandidates</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">onRemoteAnswer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">answer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setRemoteDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">answer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> c </span><span class="token keyword" style="color:#00009f">of</span><span class="token plain"> pendingCandidates</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addIceCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pendingCandidates</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">length</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>addIceCandidate 的 end-of-candidates</div><div class="admonitionContent_BuS1"><p>Trickle ICE 结束时，发起方会发送 <code>candidate: null</code>（即 <code>onicecandidate</code> 中 <code>event.candidate === null</code>）。现代浏览器不再需要显式调用 <code>addIceCandidate(null)</code>，但信令协议应能识别这一信号。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八nat-类型与穿透策略">八、NAT 类型与穿透策略<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E5%85%ABnat-%E7%B1%BB%E5%9E%8B%E4%B8%8E%E7%A9%BF%E9%80%8F%E7%AD%96%E7%95%A5" class="hash-link" aria-label="Direct link to 八、NAT 类型与穿透策略" title="Direct link to 八、NAT 类型与穿透策略" translate="no">​</a></h2>
<p><a href="https://datatracker.ietf.org/doc/html/rfc4787" target="_blank" rel="noopener noreferrer" class="">RFC 4787</a> 定义了 NAT 行为分类。理解 NAT 类型有助于预测 P2P 成功率：</p>
<!-- -->
<table><thead><tr><th style="text-align:left">NAT 类型</th><th style="text-align:left">映射行为</th><th style="text-align:left">过滤行为</th><th style="text-align:left">P2P 成功率</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Full Cone</strong></td><td style="text-align:left">固定映射</td><td style="text-align:left">任何源可入</td><td style="text-align:left">高</td></tr><tr><td style="text-align:left"><strong>Restricted Cone</strong></td><td style="text-align:left">固定映射</td><td style="text-align:left">仅曾发送过的 IP</td><td style="text-align:left">中</td></tr><tr><td style="text-align:left"><strong>Port Restricted</strong></td><td style="text-align:left">固定映射</td><td style="text-align:left">仅曾发送过的 IP<!-- -->:Port</td><td style="text-align:left">中低</td></tr><tr><td style="text-align:left"><strong>Symmetric</strong></td><td style="text-align:left">每个目标不同映射</td><td style="text-align:left">严格</td><td style="text-align:left"><strong>极低，必须 TURN</strong></td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="81-cgnat运营商级-nat">8.1 CGNAT（运营商级 NAT）<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#81-cgnat%E8%BF%90%E8%90%A5%E5%95%86%E7%BA%A7-nat" class="hash-link" aria-label="Direct link to 8.1 CGNAT（运营商级 NAT）" title="Direct link to 8.1 CGNAT（运营商级 NAT）" translate="no">​</a></h3>
<p>移动网络（4G/5G）和许多家庭宽带使用 <strong>CGNAT</strong>：你的「公网 IP」其实是运营商内网地址，外面还有一层 NAT。这意味着：</p>
<ul>
<li class="">srflx candidate 拿到的是运营商 NAT 的外部地址，不是真正的公网 IP</li>
<li class="">两层 NAT 叠加，打洞成功率进一步下降</li>
<li class=""><strong>始终配置 TURN</strong> 是移动场景的生产标配</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="82-现实建议">8.2 现实建议<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#82-%E7%8E%B0%E5%AE%9E%E5%BB%BA%E8%AE%AE" class="hash-link" aria-label="Direct link to 8.2 现实建议" title="Direct link to 8.2 现实建议" translate="no">​</a></h3>
<p>不要试图在客户端检测 NAT 类型——结果不可靠且各浏览器实现不同。生产环境的正确策略是：</p>
<ol>
<li class=""><strong>始终配置 STUN + TURN</strong></li>
<li class="">让 ICE 自动选路</li>
<li class="">监控 relay 使用率，优化 TURN 集群部署（Ch14）</li>
<li class="">对隐私敏感场景，可用 <code>iceTransportPolicy: "relay"</code> 隐藏真实 IP</li>
</ol>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九candidate-pair-状态机">九、Candidate Pair 状态机<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E4%B9%9Dcandidate-pair-%E7%8A%B6%E6%80%81%E6%9C%BA" class="hash-link" aria-label="Direct link to 九、Candidate Pair 状态机" title="Direct link to 九、Candidate Pair 状态机" translate="no">​</a></h2>
<p>ICE 为每对 (local, remote) candidate 维护独立状态：</p>
<!-- -->
<table><thead><tr><th style="text-align:left">Pair 状态</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left"><code>frozen</code></td><td style="text-align:left">初始状态，等待解冻</td></tr><tr><td style="text-align:left"><code>waiting</code></td><td style="text-align:left">已解冻，排队等待检查</td></tr><tr><td style="text-align:left"><code>in-progress</code></td><td style="text-align:left">正在发送 STUN Binding</td></tr><tr><td style="text-align:left"><code>succeeded</code></td><td style="text-align:left">检查成功，可被提名</td></tr><tr><td style="text-align:left"><code>failed</code></td><td style="text-align:left">检查失败</td></tr><tr><td style="text-align:left"><code>nominated</code></td><td style="text-align:left">被选为最终传输路径</td></tr></tbody></table>
<p>ICE Agent 按优先级从高到低逐个检查 Pair。第一个成功的 host-host 或 srflx-srflx Pair 通常延迟最低，会被优先提名。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十ice-失败诊断决策树">十、ICE 失败诊断决策树<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E5%8D%81ice-%E5%A4%B1%E8%B4%A5%E8%AF%8A%E6%96%AD%E5%86%B3%E7%AD%96%E6%A0%91" class="hash-link" aria-label="Direct link to 十、ICE 失败诊断决策树" title="Direct link to 十、ICE 失败诊断决策树" translate="no">​</a></h2>
<p>当 <code>iceConnectionState === "failed"</code> 时，按以下决策树排查：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="101-常见失败原因">10.1 常见失败原因<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#101-%E5%B8%B8%E8%A7%81%E5%A4%B1%E8%B4%A5%E5%8E%9F%E5%9B%A0" class="hash-link" aria-label="Direct link to 10.1 常见失败原因" title="Direct link to 10.1 常见失败原因" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">现象</th><th style="text-align:left">根因</th><th style="text-align:left">修复</th></tr></thead><tbody><tr><td style="text-align:left">只有 <code>host</code>，无 <code>srflx</code></td><td style="text-align:left">STUN 服务器不可达或被墙</td><td style="text-align:left">换 STUN / 检查出站 UDP</td></tr><tr><td style="text-align:left">有 <code>srflx</code> 无 <code>relay</code></td><td style="text-align:left">未配置 TURN 或认证失败</td><td style="text-align:left">检查 username/credential</td></tr><tr><td style="text-align:left">有 <code>relay</code> 但仍 failed</td><td style="text-align:left">TURN 端口范围被防火墙拦截</td><td style="text-align:left">开放 49152-65535/UDP</td></tr><tr><td style="text-align:left">本地通、跨网 failed</td><td style="text-align:left">对称 NAT 或运营商 CGNAT</td><td style="text-align:left">必须 TURN</td></tr><tr><td style="text-align:left">连上后频繁 disconnected</td><td style="text-align:left">Wi-Fi 切换 / 网络抖动</td><td style="text-align:left">ICE Restart + 断线重连</td></tr><tr><td style="text-align:left">mDNS host candidate</td><td style="text-align:left">Chrome 隐私特性 <code>.local</code></td><td style="text-align:left">正常，不影响 srflx/relay</td></tr><tr><td style="text-align:left">TURN 凭证过期</td><td style="text-align:left">REST 临时凭证 TTL 到期</td><td style="text-align:left">重新获取凭证 / 延长 TTL</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="102-chromewebrtc-internals-使用指南">10.2 chrome://webrtc-internals 使用指南<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#102-chromewebrtc-internals-%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97" class="hash-link" aria-label="Direct link to 10.2 chrome://webrtc-internals 使用指南" title="Direct link to 10.2 chrome://webrtc-internals 使用指南" translate="no">​</a></h3>
<ol>
<li class="">打开 <code>chrome://webrtc-internals</code>（连接建立前打开，可捕获完整过程）</li>
<li class="">建立 WebRTC 连接</li>
<li class="">选择对应的 PeerConnection</li>
<li class="">查看 <strong>Stats</strong> → <code>candidate-pair</code> → 找 <code>selected=true</code> 或 <code>state=succeeded</code> 的行</li>
<li class="">查看 <strong>ICE candidate grid</strong> → 确认本地/远端 Candidate 类型</li>
<li class="">点击 <strong>Create Dump</strong> 导出完整 JSON 用于离线分析</li>
</ol>
<p>关键字段：</p>
<table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left"><code>selected</code> / <code>nominated</code></td><td style="text-align:left">是否为当前选中 Pair</td></tr><tr><td style="text-align:left"><code>localCandidateType</code></td><td style="text-align:left">host / srflx / relay / prflx</td></tr><tr><td style="text-align:left"><code>remoteCandidateType</code></td><td style="text-align:left">对端 Candidate 类型</td></tr><tr><td style="text-align:left"><code>bytesSent</code> / <code>bytesReceived</code></td><td style="text-align:left">确认有实际流量</td></tr><tr><td style="text-align:left"><code>currentRoundTripTime</code></td><td style="text-align:left">RTT 估算</td></tr><tr><td style="text-align:left"><code>state</code></td><td style="text-align:left">Pair 状态（succeeded / failed / in-progress）</td></tr></tbody></table>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">exportIceDiagnostics</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stats </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getStats</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> report </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">iceConnectionState</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceConnectionState</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">iceGatheringState</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceGatheringState</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">connectionState</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">connectionState</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">candidates</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">local</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">remote</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">pairs</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">selectedPair</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  stats</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">s</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"local-candidate"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidates</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">local</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidateType</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">address</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">address</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ip</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">port</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">port</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">protocol</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">protocol</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"remote-candidate"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidates</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">remote</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidateType</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">address</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">address</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ip</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">port</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">port</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate-pair"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pairs</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">state</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">state</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">nominated</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">nominated</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesSent</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesSent</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">bytesReceived</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">bytesReceived</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">rtt</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">currentRoundTripTime</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">selected</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">nominated</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        report</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">selectedPair</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">report</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> report</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十一ice-restart-与断线恢复">十一、ICE Restart 与断线恢复<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E5%8D%81%E4%B8%80ice-restart-%E4%B8%8E%E6%96%AD%E7%BA%BF%E6%81%A2%E5%A4%8D" class="hash-link" aria-label="Direct link to 十一、ICE Restart 与断线恢复" title="Direct link to 十一、ICE Restart 与断线恢复" translate="no">​</a></h2>
<p>网络切换（Wi-Fi → 4G）或 NAT 映射过期时，ICE 可能从 <code>connected</code> 变为 <code>disconnected</code> 甚至 <code>failed</code>。WebRTC 支持 <strong>ICE Restart</strong> 在不重建整个 PeerConnection 的情况下重新收集 Candidate：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">handleIceFailure</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceConnectionState</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!==</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"failed"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> offer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">iceRestart</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  signaling</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"offer"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">sdp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> offer</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">iceRestart</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">onIceRestartOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">offer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setRemoteDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> answer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createAnswer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">answer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  signaling</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"answer"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">sdp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> answer </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<p>ICE Restart 会生成新的 <code>ice-ufrag</code> / <code>ice-pwd</code>，旧 Candidate 全部作废。信令层需重新交换 Candidate。配合 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">Ch3</a> 的重连状态机，这是生产系统断线恢复的标准手段。</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>disconnected 不等于 failed</div><div class="admonitionContent_BuS1"><p><code>disconnected</code> 状态可能持续数秒后自动恢复为 <code>connected</code>（如短暂网络抖动）。不要立即触发 ICE Restart——建议等待 5~10 秒，确认无法恢复后再重启。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十二实战-lab">十二、实战 Lab<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E5%8D%81%E4%BA%8C%E5%AE%9E%E6%88%98-lab" class="hash-link" aria-label="Direct link to 十二、实战 Lab" title="Direct link to 十二、实战 Lab" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-1仅-stun观察-candidate-类型">Lab 1：仅 STUN，观察 Candidate 类型<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#lab-1%E4%BB%85-stun%E8%A7%82%E5%AF%9F-candidate-%E7%B1%BB%E5%9E%8B" class="hash-link" aria-label="Direct link to Lab 1：仅 STUN，观察 Candidate 类型" title="Direct link to Lab 1：仅 STUN，观察 Candidate 类型" translate="no">​</a></h3>
<ol>
<li class="">启动 <code>examples/webrtc-lab/signaling/</code> 信令服务器（或直接用 ch02 手动交换 SDP）</li>
<li class="">打开两个 Tab 运行 <code>client/ch02-p2p-basic/</code></li>
<li class=""><code>iceServers</code> 只配 Google STUN，不配 TURN</li>
<li class="">打开 <code>chrome://webrtc-internals</code>，确认出现 <code>host</code> + <code>srflx</code>，<strong>无</strong> <code>relay</code></li>
<li class="">记录 selected pair 的类型和 RTT</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-2部署-coturn验证-relay">Lab 2：部署 coturn，验证 relay<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#lab-2%E9%83%A8%E7%BD%B2-coturn%E9%AA%8C%E8%AF%81-relay" class="hash-link" aria-label="Direct link to Lab 2：部署 coturn，验证 relay" title="Direct link to Lab 2：部署 coturn，验证 relay" translate="no">​</a></h3>
<ol>
<li class=""><code>cd examples/webrtc-lab/docker/coturn &amp;&amp; docker compose up -d</code></li>
<li class="">客户端添加 localhost TURN 配置（见第六节）</li>
<li class="">重新建立连接，确认出现 <code>typ relay</code> Candidate</li>
<li class="">对比 selected pair：P2P 成功时通常选 srflx</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-3强制-turn-中继">Lab 3：强制 TURN 中继<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#lab-3%E5%BC%BA%E5%88%B6-turn-%E4%B8%AD%E7%BB%A7" class="hash-link" aria-label="Direct link to Lab 3：强制 TURN 中继" title="Direct link to Lab 3：强制 TURN 中继" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token constant" style="color:#36acaa">LOCAL_TURN</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">iceTransportPolicy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"relay"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<ol>
<li class="">设置 <code>iceTransportPolicy: "relay"</code></li>
<li class="">建立连接，确认 selected pair 的 local/remote 类型均为 <code>relay</code></li>
<li class="">测量 RTT，对比 P2P 模式下的差异</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-4trickle-ice-时序观察">Lab 4：Trickle ICE 时序观察<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#lab-4trickle-ice-%E6%97%B6%E5%BA%8F%E8%A7%82%E5%AF%9F" class="hash-link" aria-label="Direct link to Lab 4：Trickle ICE 时序观察" title="Direct link to Lab 4：Trickle ICE 时序观察" translate="no">​</a></h3>
<ol>
<li class="">在 <code>onicecandidate</code> 中加 <code>performance.now()</code> 时间戳日志</li>
<li class="">对比各 Candidate 到达顺序 vs <code>iceConnectionState</code> 变化时间</li>
<li class="">验证：第一个 srflx candidate 到达后，<code>checking</code> 状态是否很快出现</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-5故障注入">Lab 5：故障注入<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#lab-5%E6%95%85%E9%9A%9C%E6%B3%A8%E5%85%A5" class="hash-link" aria-label="Direct link to Lab 5：故障注入" title="Direct link to Lab 5：故障注入" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">实验</th><th style="text-align:left">操作</th><th style="text-align:left">预期</th></tr></thead><tbody><tr><td style="text-align:left">无 TURN</td><td style="text-align:left">移除 TURN 配置</td><td style="text-align:left">大多数家庭网络仍可 P2P</td></tr><tr><td style="text-align:left">错误 TURN 密码</td><td style="text-align:left">credential 故意写错</td><td style="text-align:left">无 relay candidate，coturn 日志显示认证失败</td></tr><tr><td style="text-align:left">阻断 UDP</td><td style="text-align:left">防火墙禁出站 UDP</td><td style="text-align:left">ICE failed</td></tr><tr><td style="text-align:left">强制 relay</td><td style="text-align:left"><code>iceTransportPolicy: "relay"</code></td><td style="text-align:left">只有 relay pair 成功</td></tr><tr><td style="text-align:left">错误 STUN 地址</td><td style="text-align:left">urls 指向不存在的主机</td><td style="text-align:left">无 srflx，仅 host candidate</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="lab-6导出诊断报告">Lab 6：导出诊断报告<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#lab-6%E5%AF%BC%E5%87%BA%E8%AF%8A%E6%96%AD%E6%8A%A5%E5%91%8A" class="hash-link" aria-label="Direct link to Lab 6：导出诊断报告" title="Direct link to Lab 6：导出诊断报告" translate="no">​</a></h3>
<ol>
<li class="">在连接失败时调用 <code>exportIceDiagnostics(pc)</code></li>
<li class="">检查 <code>candidates.local</code> 中是否有 relay 类型</li>
<li class="">检查 <code>pairs</code> 中是否有 <code>state: "succeeded"</code> 的条目</li>
<li class="">将 JSON 报告与 <code>chrome://webrtc-internals</code> 的 Dump 交叉验证</li>
</ol>
<p><strong>下一篇（Ch6）</strong>：<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a>——ICE 连通后，如何在 DTLS 之上传输任意 P2P 数据。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十三本章小结">十三、本章小结<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#%E5%8D%81%E4%B8%89%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="Direct link to 十三、本章小结" title="Direct link to 十三、本章小结" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">概念</th><th style="text-align:left">要点</th></tr></thead><tbody><tr><td style="text-align:left">ICE</td><td style="text-align:left">枚举 + 检查 + 提名，RFC 8445</td></tr><tr><td style="text-align:left">STUN</td><td style="text-align:left">发现公网映射，RFC 5389，不转发数据</td></tr><tr><td style="text-align:left">TURN</td><td style="text-align:left">中继兜底，RFC 5766，带宽成本 ×2</td></tr><tr><td style="text-align:left">Trickle ICE</td><td style="text-align:left">边收集边发送，降低首连延迟</td></tr><tr><td style="text-align:left">生产策略</td><td style="text-align:left">始终 STUN + TURN，监控 relay 比例</td></tr></tbody></table>
<p>Phase 2（连接建立）继续深入：<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Ch6 Data Channel</a> 将在 ICE 选出的路径上传输任意 P2P 数据。</p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8445" target="_blank" rel="noopener noreferrer" class="">RFC 8445 — ICE: Interactive Connectivity Establishment</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc5389" target="_blank" rel="noopener noreferrer" class="">RFC 5389 — STUN: Session Traversal Utilities for NAT</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc5766" target="_blank" rel="noopener noreferrer" class="">RFC 5766 — TURN: Traversal Using Relays around NAT</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc4787" target="_blank" rel="noopener noreferrer" class="">RFC 4787 — NAT Behavioral Requirements for Unicast UDP</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8838" target="_blank" rel="noopener noreferrer" class="">RFC 8838 — Trickle ICE</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8839" target="_blank" rel="noopener noreferrer" class="">RFC 8839 — ICE in WebRTC</a></li>
<li class=""><a href="https://webrtcforthecurious.com/docs/04-ice/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — ICE</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史 / 多播 vs 单播</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState" target="_blank" rel="noopener noreferrer" class="">MDN — RTCPeerConnection.iceConnectionState</a></li>
<li class=""><a href="https://github.com/coturn/coturn" target="_blank" rel="noopener noreferrer" class="">coturn — GitHub</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>ICE</category>
            <category>STUN/TURN</category>
            <category>实时通信</category>
            <category>Deep Dive</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (4)：SDP 解剖与媒体协商]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive</guid>
            <pubDate>Mon, 15 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[SDP 逐行解剖、Unified Plan、RTCRtpTransceiver、编解码器协商与 BUNDLE/rtcp-mux 深度解析]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"Offer/Answer 里那一大段文本，就是整个 WebRTC 会话的「合同」。"</p>
</blockquote>
<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">Ch3 信令</a> 负责传递 SDP，但 SDP 本身是什么？<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">Ch2</a> 中 <code>createOffer()</code> 产出的字符串，遵循 <a href="https://datatracker.ietf.org/doc/html/rfc8866" target="_blank" rel="noopener noreferrer" class="">RFC 8866 SDP</a> 格式。它描述了：<strong>用什么编解码器、用什么 ICE 凭证、用什么 DTLS 指纹、媒体方向是什么</strong>。</p>
<p>本章逐行解剖 WebRTC 中的 SDP，理解 <strong>Unified Plan</strong>、<strong>BUNDLE</strong>、<strong>rtcp-mux</strong>，以及 <code>RTCRtpTransceiver</code> 与 m-line 的对应关系。</p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>SDP</strong></td><td style="text-align:left">Session Description Protocol</td><td style="text-align:left">文本格式的会话描述协议，<strong>不是</strong>传输协议，只描述「会话参数」</td></tr><tr><td style="text-align:left"><strong>Offer</strong></td><td style="text-align:left">—</td><td style="text-align:left">发起方生成的 SDP，声明「我能提供什么」</td></tr><tr><td style="text-align:left"><strong>Answer</strong></td><td style="text-align:left">—</td><td style="text-align:left">应答方生成的 SDP，声明「我接受什么」</td></tr><tr><td style="text-align:left"><strong>m-line</strong></td><td style="text-align:left">Media Description Line</td><td style="text-align:left"><code>m=</code> 开头的行，描述一条媒体流（audio/video/application）</td></tr><tr><td style="text-align:left"><strong>mid</strong></td><td style="text-align:left">Media Identification</td><td style="text-align:left">m-line 的唯一标识符，Unified Plan 下每个 track 一个 mid</td></tr><tr><td style="text-align:left"><strong>Payload Type (PT)</strong></td><td style="text-align:left">—</td><td style="text-align:left">RTP 包头中的 7bit 字段，映射到具体编解码器</td></tr><tr><td style="text-align:left"><strong>rtpmap</strong></td><td style="text-align:left">—</td><td style="text-align:left">SDP 属性，定义 PT 与编解码器的映射关系</td></tr><tr><td style="text-align:left"><strong>fmtp</strong></td><td style="text-align:left">Format Parameters</td><td style="text-align:left">编解码器的额外参数，如 H.264 的 profile-level-id</td></tr><tr><td style="text-align:left"><strong>rtcp-fb</strong></td><td style="text-align:left">RTCP Feedback</td><td style="text-align:left">声明支持的 RTCP 反馈机制（NACK/PLI/TWCC 等）</td></tr><tr><td style="text-align:left"><strong>BUNDLE</strong></td><td style="text-align:left">—</td><td style="text-align:left">将多条 m-line 的多路 RTP 复用到<strong>同一个 UDP 五元组</strong></td></tr><tr><td style="text-align:left"><strong>rtcp-mux</strong></td><td style="text-align:left">RTCP Multiplexing</td><td style="text-align:left">RTP 与 RTCP 共用同一端口，而非各用独立端口</td></tr><tr><td style="text-align:left"><strong>Unified Plan</strong></td><td style="text-align:left">—</td><td style="text-align:left">现行 SDP 语义：每个 m-line 对应一个 Transceiver</td></tr><tr><td style="text-align:left"><strong>Plan B</strong></td><td style="text-align:left">—</td><td style="text-align:left">已废弃：多个 track 共用一个 m-line</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一sdp-在-webrtc-中的角色">一、SDP 在 WebRTC 中的角色<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#%E4%B8%80sdp-%E5%9C%A8-webrtc-%E4%B8%AD%E7%9A%84%E8%A7%92%E8%89%B2" class="hash-link" aria-label="Direct link to 一、SDP 在 WebRTC 中的角色" title="Direct link to 一、SDP 在 WebRTC 中的角色" translate="no">​</a></h2>
<!-- -->
<p>SDP 是 <strong>声明式</strong> 的：它不建立连接，只描述能力。真正的连通由 ICE 完成，加密由 DTLS 完成，媒体由 RTP 承载。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二完整-sdp-示例与逐行解读">二、完整 SDP 示例与逐行解读<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#%E4%BA%8C%E5%AE%8C%E6%95%B4-sdp-%E7%A4%BA%E4%BE%8B%E4%B8%8E%E9%80%90%E8%A1%8C%E8%A7%A3%E8%AF%BB" class="hash-link" aria-label="Direct link to 二、完整 SDP 示例与逐行解读" title="Direct link to 二、完整 SDP 示例与逐行解读" translate="no">​</a></h2>
<p>以下是一段真实的 WebRTC Offer SDP（简化版）：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">v=0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">o=- 4611731400430051336 2 IN IP4 127.0.0.1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">s=-</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">t=0 0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=group:BUNDLE 0 1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=extmap-allow-mixed</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">c=IN IP4 0.0.0.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp:9 IN IP4 0.0.0.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=ice-ufrag:FPlu</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=ice-pwd:2/1muCWoOi3J5Wfu+86J7GqJ</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=ice-options:trickle</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=fingerprint:sha-256 4A:AD:BA:62:79:...</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=setup:actpass</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=mid:0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=sendrecv</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=msid:stream-id track-audio-id</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-mux</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:111 opus/48000/2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=fmtp:111 minptime=10;useinbandfec=1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:63 red/48000/2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:111 transport-cc</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 103 104 107 108</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">c=IN IP4 0.0.0.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp:9 IN IP4 0.0.0.0</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=ice-ufrag:FPlu</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=ice-pwd:2/1muCWoOi3J5Wfu+86J7GqJ</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=fingerprint:sha-256 4A:AD:BA:62:79:...</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=setup:actpass</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=mid:1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=sendrecv</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=msid:stream-id track-video-id</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-mux</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-rsize</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:96 VP8/90000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:96 goog-remb</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:96 transport-cc</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:96 ccm fir</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:96 nack</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:96 nack pli</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:97 rtx/90000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=fmtp:97 apt=96</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-会话级字段">2.1 会话级字段<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#21-%E4%BC%9A%E8%AF%9D%E7%BA%A7%E5%AD%97%E6%AE%B5" class="hash-link" aria-label="Direct link to 2.1 会话级字段" title="Direct link to 2.1 会话级字段" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">行</th><th style="text-align:left">字段名</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left"><code>v=0</code></td><td style="text-align:left">Version</td><td style="text-align:left">SDP 版本，固定为 0</td></tr><tr><td style="text-align:left"><code>o=- 4611731400430051336 2 IN IP4 127.0.0.1</code></td><td style="text-align:left">Origin</td><td style="text-align:left">会话创建者 ID、版本号、地址</td></tr><tr><td style="text-align:left"><code>s=-</code></td><td style="text-align:left">Session Name</td><td style="text-align:left">会话名，WebRTC 中通常为 <code>-</code></td></tr><tr><td style="text-align:left"><code>t=0 0</code></td><td style="text-align:left">Timing</td><td style="text-align:left">会话时间，<code>0 0</code> 表示永久会话</td></tr><tr><td style="text-align:left"><code>a=group:BUNDLE 0 1</code></td><td style="text-align:left">BUNDLE Group</td><td style="text-align:left">mid <code>0</code> 和 <code>1</code> 共用同一传输通道</td></tr><tr><td style="text-align:left"><code>a=extmap-allow-mixed</code></td><td style="text-align:left">—</td><td style="text-align:left">允许不同 m-line 使用不同 RTP 扩展</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="22-m-line-结构">2.2 m-line 结构<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#22-m-line-%E7%BB%93%E6%9E%84" class="hash-link" aria-label="Direct link to 2.2 m-line 结构" title="Direct link to 2.2 m-line 结构" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">m=&lt;media&gt; &lt;port&gt; &lt;proto&gt; &lt;fmt列表&gt;</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9 ...</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">部分</th><th style="text-align:left">示例</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left">media</td><td style="text-align:left"><code>audio</code> / <code>video</code></td><td style="text-align:left">媒体类型</td></tr><tr><td style="text-align:left">port</td><td style="text-align:left"><code>9</code></td><td style="text-align:left">占位端口（ICE 实际决定端口，<code>9</code> 为 discard port）</td></tr><tr><td style="text-align:left">proto</td><td style="text-align:left"><code>UDP/TLS/RTP/SAVPF</code></td><td style="text-align:left">协议栈：UDP + DTLS + SRTP + AVPF（反馈 profile）</td></tr><tr><td style="text-align:left">fmt</td><td style="text-align:left"><code>111 63 9 ...</code></td><td style="text-align:left">支持的 Payload Type 列表，<strong>按优先级排序</strong></td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="23-ice-与-dtls-属性">2.3 ICE 与 DTLS 属性<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#23-ice-%E4%B8%8E-dtls-%E5%B1%9E%E6%80%A7" class="hash-link" aria-label="Direct link to 2.3 ICE 与 DTLS 属性" title="Direct link to 2.3 ICE 与 DTLS 属性" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">属性</th><th style="text-align:left">示例</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:left"><code>a=ice-ufrag</code></td><td style="text-align:left"><code>FPlu</code></td><td style="text-align:left">ICE 用户名片段，连通性检查凭证</td></tr><tr><td style="text-align:left"><code>a=ice-pwd</code></td><td style="text-align:left"><code>2/1muCWo...</code></td><td style="text-align:left">ICE 密码</td></tr><tr><td style="text-align:left"><code>a=ice-options:trickle</code></td><td style="text-align:left">—</td><td style="text-align:left">支持 Trickle ICE</td></tr><tr><td style="text-align:left"><code>a=fingerprint:sha-256 ...</code></td><td style="text-align:left">—</td><td style="text-align:left">DTLS 证书 SHA-256 指纹，<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">Ch7</a></td></tr><tr><td style="text-align:left"><code>a=setup:actpass</code></td><td style="text-align:left">—</td><td style="text-align:left">DTLS 角色：可主动可被动</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="24-媒体方向与-track-标识">2.4 媒体方向与 Track 标识<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#24-%E5%AA%92%E4%BD%93%E6%96%B9%E5%90%91%E4%B8%8E-track-%E6%A0%87%E8%AF%86" class="hash-link" aria-label="Direct link to 2.4 媒体方向与 Track 标识" title="Direct link to 2.4 媒体方向与 Track 标识" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">属性</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left"><code>a=mid:0</code></td><td style="text-align:left">此 m-line 的 ID，BUNDLE 组内引用</td></tr><tr><td style="text-align:left"><code>a=sendrecv</code></td><td style="text-align:left">双向收发（还有 <code>sendonly</code>/<code>recvonly</code>/<code>inactive</code>）</td></tr><tr><td style="text-align:left"><code>a=msid:stream-id track-id</code></td><td style="text-align:left">MediaStream ID + Track ID，关联 <code>addTrack()</code></td></tr><tr><td style="text-align:left"><code>a=rtcp-mux</code></td><td style="text-align:left">RTP 与 RTCP 同端口</td></tr><tr><td style="text-align:left"><code>a=rtcp-rsize</code></td><td style="text-align:left">使用 Reduced-Size RTCP</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="25-编解码器映射">2.5 编解码器映射<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#25-%E7%BC%96%E8%A7%A3%E7%A0%81%E5%99%A8%E6%98%A0%E5%B0%84" class="hash-link" aria-label="Direct link to 2.5 编解码器映射" title="Direct link to 2.5 编解码器映射" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:111 opus/48000/2</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=fmtp:111 minptime=10;useinbandfec=1</span><br></div></code></pre></div></div></div>
<ul>
<li class=""><strong>PT 111</strong> → Opus，48kHz，2 声道</li>
<li class=""><strong>fmtp</strong> → 最小时长 10ms，启用带内 FEC</li>
</ul>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:96 VP8/90000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:96 nack</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:96 nack pli</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=rtcp-fb:96 transport-cc</span><br></div></code></pre></div></div></div>
<ul>
<li class=""><strong>PT 96</strong> → VP8，90kHz 时钟</li>
<li class="">支持 NACK 重传、PLI 关键帧请求、TWCC 拥塞控制</li>
</ul>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">a=rtpmap:97 rtx/90000</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">a=fmtp:97 apt=96</span><br></div></code></pre></div></div></div>
<ul>
<li class=""><strong>PT 97</strong> → RTX（重传）流，关联主 PT 96</li>
</ul>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三bundle-与-rtcp-mux-深度解析">三、BUNDLE 与 rtcp-mux 深度解析<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#%E4%B8%89bundle-%E4%B8%8E-rtcp-mux-%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90" class="hash-link" aria-label="Direct link to 三、BUNDLE 与 rtcp-mux 深度解析" title="Direct link to 三、BUNDLE 与 rtcp-mux 深度解析" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-为什么需要-bundle">3.1 为什么需要 BUNDLE？<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#31-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-bundle" class="hash-link" aria-label="Direct link to 3.1 为什么需要 BUNDLE？" title="Direct link to 3.1 为什么需要 BUNDLE？" translate="no">​</a></h3>
<p>没有 BUNDLE 时，audio 和 video 各占用独立 UDP 端口 → 更多 ICE Candidate → 更慢连通、更多 NAT 打洞失败。</p>
<!-- -->
<p><code>a=group:BUNDLE 0 1</code> 表示 mid <code>0</code> 和 <code>1</code> 的 RTP/RTCP 都走<strong>第一个 m-line 协商出的传输通道</strong>。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-rtcp-mux">3.2 rtcp-mux<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#32-rtcp-mux" class="hash-link" aria-label="Direct link to 3.2 rtcp-mux" title="Direct link to 3.2 rtcp-mux" translate="no">​</a></h3>
<p>传统 RTP：RTP 端口 N，RTCP 端口 N+1。WebRTC 强制 <strong>rtcp-mux</strong>——RTP 和 RTCP 在同一端口，通过包类型区分。这减少了一半的 Candidate 数量。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四unified-plan-vs-plan-b">四、Unified Plan vs Plan B<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#%E5%9B%9Bunified-plan-vs-plan-b" class="hash-link" aria-label="Direct link to 四、Unified Plan vs Plan B" title="Direct link to 四、Unified Plan vs Plan B" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">特性</th><th style="text-align:left">Plan B</th><th style="text-align:left">Unified Plan</th></tr></thead><tbody><tr><td style="text-align:left">m-line 与 track</td><td style="text-align:left">多 track 共一 m-line</td><td style="text-align:left">一 track 一 m-line</td></tr><tr><td style="text-align:left">API</td><td style="text-align:left"><code>addStream()</code></td><td style="text-align:left"><code>addTransceiver()</code> / <code>addTrack()</code></td></tr><tr><td style="text-align:left">Simulcast</td><td style="text-align:left">支持差</td><td style="text-align:left">原生 <code>rid</code> 支持</td></tr><tr><td style="text-align:left">浏览器</td><td style="text-align:left">已移除</td><td style="text-align:left">唯一支持</td></tr></tbody></table>
<p><a href="https://blogwebrtc.org/" target="_blank" rel="noopener noreferrer" class="">Fippo — Exploring RTCRtpTransceiver</a> 详细记录了 Unified Plan 成为唯一标准的设计决策过程。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五rtcrtptransceiver-详解">五、RTCRtpTransceiver 详解<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#%E4%BA%94rtcrtptransceiver-%E8%AF%A6%E8%A7%A3" class="hash-link" aria-label="Direct link to 五、RTCRtpTransceiver 详解" title="Direct link to 五、RTCRtpTransceiver 详解" translate="no">​</a></h2>
<!-- -->
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 添加只发送视频的 Transceiver</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> transceiver </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTransceiver</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">direction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"sendonly"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">sendEncodings</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"h"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1_500_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">scaleResolutionDownBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"m"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">500_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">scaleResolutionDownBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">rid</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"l"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">maxBitrate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">150_000</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">scaleResolutionDownBy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">4</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// direction 可选值</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// "sendrecv" | "sendonly" | "recvonly" | "inactive"</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">direction</th><th style="text-align:left">含义</th><th style="text-align:left">SDP 中</th></tr></thead><tbody><tr><td style="text-align:left"><code>sendrecv</code></td><td style="text-align:left">双向</td><td style="text-align:left"><code>a=sendrecv</code></td></tr><tr><td style="text-align:left"><code>sendonly</code></td><td style="text-align:left">只发送</td><td style="text-align:left"><code>a=sendonly</code></td></tr><tr><td style="text-align:left"><code>recvonly</code></td><td style="text-align:left">只接收</td><td style="text-align:left"><code>a=recvonly</code></td></tr><tr><td style="text-align:left"><code>inactive</code></td><td style="text-align:left">暂停</td><td style="text-align:left"><code>a=inactive</code></td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六编解码器协商过程">六、编解码器协商过程<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#%E5%85%AD%E7%BC%96%E8%A7%A3%E7%A0%81%E5%99%A8%E5%8D%8F%E5%95%86%E8%BF%87%E7%A8%8B" class="hash-link" aria-label="Direct link to 六、编解码器协商过程" title="Direct link to 六、编解码器协商过程" translate="no">​</a></h2>
<!-- -->
<p>Answer 方<strong>不能</strong>添加 Offer 中未出现的 Codec。协商结果 = 双方 fmt 列表的<strong>交集</strong>，按 Offer 中的顺序取最优。</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 查看浏览器支持的所有 Codec</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> audioCodecs </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token maybe-class-name">RTCRtpSender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getCapabilities</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"audio"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">codecs</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> videoCodecs </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token maybe-class-name">RTCRtpSender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getCapabilities</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">codecs</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">videoCodecs</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">c</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">c</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">mimeType</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c"> pt=</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">c</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">preferredPayloadType</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七手工调试-sdp">七、手工调试 SDP<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#%E4%B8%83%E6%89%8B%E5%B7%A5%E8%B0%83%E8%AF%95-sdp" class="hash-link" aria-label="Direct link to 七、手工调试 SDP" title="Direct link to 七、手工调试 SDP" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="71-打印与分析">7.1 打印与分析<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#71-%E6%89%93%E5%8D%B0%E4%B8%8E%E5%88%86%E6%9E%90" class="hash-link" aria-label="Direct link to 7.1 打印与分析" title="Direct link to 7.1 打印与分析" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> offer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 按 m-line 分段</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sections </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> offer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">split</span><span class="token punctuation" style="color:#393A34">(</span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token regex regex-source language-regex anchor function" style="color:#d73a49">^</span><span class="token regex regex-source language-regex" style="color:#36acaa">m=</span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token regex regex-flags" style="color:#36acaa">m</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">sections</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">s</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"--- m="</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">slice</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">20</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">+</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"..."</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="72-限制带宽调试用">7.2 限制带宽（调试用）<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#72-%E9%99%90%E5%88%B6%E5%B8%A6%E5%AE%BD%E8%B0%83%E8%AF%95%E7%94%A8" class="hash-link" aria-label="Direct link to 7.2 限制带宽（调试用）" title="Direct link to 7.2 限制带宽（调试用）" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 不推荐生产使用，优先用 setParameters</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> offer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">replace</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">(</span><span class="token regex regex-source language-regex" style="color:#36acaa">a=mid:1</span><span class="token regex regex-source language-regex escape" style="color:#36acaa">\r</span><span class="token regex regex-source language-regex escape" style="color:#36acaa">\n</span><span class="token regex regex-source language-regex group punctuation" style="color:#393A34">)</span><span class="token regex regex-delimiter" style="color:#36acaa">/</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token string" style="color:#e3116c">"$1b=AS:500\r\n"</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 视频 mid=1 限制 500 kbps</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="73-强制-h264调试用">7.3 强制 H.264（调试用）<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#73-%E5%BC%BA%E5%88%B6-h264%E8%B0%83%E8%AF%95%E7%94%A8" class="hash-link" aria-label="Direct link to 7.3 强制 H.264（调试用）" title="Direct link to 7.3 强制 H.264（调试用）" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> transceiver </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTransceiver</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">direction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"sendrecv"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> caps </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token maybe-class-name">RTCRtpSender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getCapabilities</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> h264 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> caps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">codecs</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">c</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mimeType</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video/H264"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">transceiver</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setCodecPreferences</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">h264</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八常见问题">八、常见问题<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#%E5%85%AB%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98" class="hash-link" aria-label="Direct link to 八、常见问题" title="Direct link to 八、常见问题" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">问题</th><th style="text-align:left">原因</th><th style="text-align:left">解决</th></tr></thead><tbody><tr><td style="text-align:left">有 Offer 无 Answer</td><td style="text-align:left">对端 Codec 无交集</td><td style="text-align:left">检查 fmt 列表</td></tr><tr><td style="text-align:left">单向视频</td><td style="text-align:left">direction 设为 sendonly</td><td style="text-align:left">改 sendrecv</td></tr><tr><td style="text-align:left">Simulcast 不生效</td><td style="text-align:left">SDP 无 rid</td><td style="text-align:left">用 Unified Plan + addTransceiver</td></tr><tr><td style="text-align:left">BUNDLE 失败</td><td style="text-align:left">一端不支持 BUNDLE</td><td style="text-align:left">检查 <code>a=group:BUNDLE</code></td></tr><tr><td style="text-align:left">fingerprint 不匹配</td><td style="text-align:left">SDP 被中间人修改</td><td style="text-align:left">信令必须 WSS</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九实战-lab">九、实战 Lab<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#%E4%B9%9D%E5%AE%9E%E6%88%98-lab" class="hash-link" aria-label="Direct link to 九、实战 Lab" title="Direct link to 九、实战 Lab" translate="no">​</a></h2>
<ol>
<li class=""><code>createOffer()</code> 后完整打印 SDP，标出每个 <code>a=</code> 行的含义</li>
<li class="">对比 Offer 与 Answer 的 fmt 列表差异</li>
<li class="">添加第二个 video Transceiver（屏幕共享），观察新 m-line</li>
<li class="">用 <code>setCodecPreferences</code> 强制 Opus + VP8</li>
</ol>
<p><strong>下一篇（Ch5）</strong>：<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN</a>——SDP 中 <code>ice-ufrag</code> 如何驱动 NAT 穿透。</p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8866" target="_blank" rel="noopener noreferrer" class="">RFC 8866 — SDP: Session Description Protocol</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8829" target="_blank" rel="noopener noreferrer" class="">RFC 8829 — JSEP</a></li>
<li class=""><a href="https://blogwebrtc.org/" target="_blank" rel="noopener noreferrer" class="">Advancing WebRTC — Exploring RTCRtpTransceiver</a></li>
<li class=""><a href="https://webrtc.org/getting-started/unified-plan-transition-guide" target="_blank" rel="noopener noreferrer" class="">Unified Plan Transition Guide</a></li>
<li class=""><a href="https://webrtcforthecurious.com/docs/03-connecting/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — SDP</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>SDP</category>
            <category>实时通信</category>
            <category>Deep Dive</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (3)：信令服务器设计与会话状态机]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-signaling</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-signaling</guid>
            <pubDate>Sun, 14 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[WebSocket 信令服务器、Room 状态机、SDP/ICE 转发、安全鉴权与生产级信令架构设计]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"WebRTC 只标准化媒体通道，信令 deliberately 留给应用层。" — <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious</a></p>
</blockquote>
<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">Ch2</a> 我们用手工复制 JSON 完成了 Offer/Answer 和 ICE Candidate 交换——这足以理解原理，但无法支撑生产。Serge Lachapelle 在 Curious 历史访谈中明确提到：<strong>IETF 刻意不对信令重新标准化</strong>，因为 SIP 等方案已存在，重新标准化只会引发政治斗争且「不会创造有价值贡献」。</p>
<p>因此，<strong>每一个 WebRTC 应用都必须实现自己的信令层</strong>。本章从设计原则出发，构建一套可扩展的 WebSocket 信令服务器，并讨论生产环境的安全、重连与 Room 状态管理。</p>
<p>配套代码：<code>examples/webrtc-lab/signaling/</code></p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>信令</strong></td><td style="text-align:left">Signaling</td><td style="text-align:left">在 PeerConnection 建立<strong>之前/之中</strong>，交换 SDP、ICE Candidate、Room 元数据的<strong>带外（Out-of-band）控制通道</strong></td></tr><tr><td style="text-align:left"><strong>SDP</strong></td><td style="text-align:left">Session Description Protocol</td><td style="text-align:left">描述会话媒体能力（编解码器、ICE 参数、DTLS 指纹）的文本格式，见 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">Ch4</a></td></tr><tr><td style="text-align:left"><strong>Offer/Answer</strong></td><td style="text-align:left">—</td><td style="text-align:left">JSEP 模型下的 SDP 协商：一方 Offer，另一方 Answer</td></tr><tr><td style="text-align:left"><strong>ICE Candidate</strong></td><td style="text-align:left">—</td><td style="text-align:left">一个可用的网络地址（IP<!-- -->:Port<!-- --> + 类型），供 ICE 连通性检查使用</td></tr><tr><td style="text-align:left"><strong>Room</strong></td><td style="text-align:left">房间</td><td style="text-align:left">逻辑上的会话容器，同一 Room 内的参与者交换媒体</td></tr><tr><td style="text-align:left"><strong>Peer</strong></td><td style="text-align:left">对等端</td><td style="text-align:left">Room 内的一个参与者实例，通常对应一个浏览器 Tab 或一个设备</td></tr><tr><td style="text-align:left"><strong>Trickle ICE</strong></td><td style="text-align:left">—</td><td style="text-align:left">边收集 ICE Candidate 边通过信令发送，而非等全部收集完</td></tr><tr><td style="text-align:left"><strong>控制面</strong></td><td style="text-align:left">Control Plane</td><td style="text-align:left">信令、鉴权、Room 管理——<strong>不承载媒体</strong></td></tr><tr><td style="text-align:left"><strong>数据面</strong></td><td style="text-align:left">Data Plane</td><td style="text-align:left">SRTP 媒体流、SCTP Data Channel——<strong>走 UDP，不经过信令服务器</strong></td></tr><tr><td style="text-align:left"><strong>WSS</strong></td><td style="text-align:left">WebSocket Secure</td><td style="text-align:left">TLS 加密的 WebSocket，生产信令必须使用</td></tr><tr><td style="text-align:left"><strong>JWT</strong></td><td style="text-align:left">JSON Web Token</td><td style="text-align:left">常用于编码 Room 权限、Identity、过期时间的鉴权令牌</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一为什么-webrtc-不内置信令">一、为什么 WebRTC 不内置信令？<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#%E4%B8%80%E4%B8%BA%E4%BB%80%E4%B9%88-webrtc-%E4%B8%8D%E5%86%85%E7%BD%AE%E4%BF%A1%E4%BB%A4" class="hash-link" aria-label="Direct link to 一、为什么 WebRTC 不内置信令？" title="Direct link to 一、为什么 WebRTC 不内置信令？" translate="no">​</a></h2>
<!-- -->
<p>WebRTC 的设计哲学是 <strong>「Bring Your Own Signaling」</strong>：</p>
<ol>
<li class=""><strong>灵活性</strong>：1v1 通话只需转发 Offer/Answer；大型会议需要 Room 状态、权限、录制控制——需求差异太大</li>
<li class=""><strong>避免重复</strong>：SIP、XMPP、MQTT 等已有成熟信令协议，WebRTC 不应重复造轮子</li>
<li class=""><strong>解耦</strong>：媒体走 UDP P2P 或 SFU，信令走 TCP WebSocket——路径、扩容策略完全不同</li>
</ol>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>关键认知</div><div class="admonitionContent_BuS1"><p>信令服务器<strong>永远看不到 SRTP 媒体内容</strong>（除非你是 SFU）。它只传递 SDP 文本和 ICE Candidate JSON。媒体带宽不经过信令服务器。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二信令消息协议设计">二、信令消息协议设计<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#%E4%BA%8C%E4%BF%A1%E4%BB%A4%E6%B6%88%E6%81%AF%E5%8D%8F%E8%AE%AE%E8%AE%BE%E8%AE%A1" class="hash-link" aria-label="Direct link to 二、信令消息协议设计" title="Direct link to 二、信令消息协议设计" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-最小消息集1v1-p2p">2.1 最小消息集（1v1 P2P）<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#21-%E6%9C%80%E5%B0%8F%E6%B6%88%E6%81%AF%E9%9B%861v1-p2p" class="hash-link" aria-label="Direct link to 2.1 最小消息集（1v1 P2P）" title="Direct link to 2.1 最小消息集（1v1 P2P）" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">消息类型</th><th style="text-align:left">方向</th><th style="text-align:left">载荷</th><th style="text-align:left">触发时机</th></tr></thead><tbody><tr><td style="text-align:left"><code>join</code></td><td style="text-align:left">C→S</td><td style="text-align:left"><code>{ roomId, identity }</code></td><td style="text-align:left">用户进入 Room</td></tr><tr><td style="text-align:left"><code>joined</code></td><td style="text-align:left">S→C</td><td style="text-align:left"><code>{ peerId, peers[] }</code></td><td style="text-align:left">加入成功，返回已有 peer</td></tr><tr><td style="text-align:left"><code>offer</code></td><td style="text-align:left">C→S→C</td><td style="text-align:left"><code>{ sdp, from, to? }</code></td><td style="text-align:left"><code>createOffer</code> + <code>setLocalDescription</code> 后</td></tr><tr><td style="text-align:left"><code>answer</code></td><td style="text-align:left">C→S→C</td><td style="text-align:left"><code>{ sdp, from, to }</code></td><td style="text-align:left">收到 Offer 并 <code>createAnswer</code> 后</td></tr><tr><td style="text-align:left"><code>candidate</code></td><td style="text-align:left">C→S→C</td><td style="text-align:left"><code>{ candidate, from, to? }</code></td><td style="text-align:left"><code>onicecandidate</code> 回调</td></tr><tr><td style="text-align:left"><code>peer-joined</code></td><td style="text-align:left">S→C</td><td style="text-align:left"><code>{ peerId }</code></td><td style="text-align:left">新 peer 加入 Room</td></tr><tr><td style="text-align:left"><code>peer-left</code></td><td style="text-align:left">S→C</td><td style="text-align:left"><code>{ peerId }</code></td><td style="text-align:left">peer 断开连接</td></tr><tr><td style="text-align:left"><code>leave</code></td><td style="text-align:left">C→S</td><td style="text-align:left">—</td><td style="text-align:left">用户主动离开</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="22-完整交互时序1v1">2.2 完整交互时序（1v1）<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#22-%E5%AE%8C%E6%95%B4%E4%BA%A4%E4%BA%92%E6%97%B6%E5%BA%8F1v1" class="hash-link" aria-label="Direct link to 2.2 完整交互时序（1v1）" title="Direct link to 2.2 完整交互时序（1v1）" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="23-多人会议的消息扩展">2.3 多人会议的消息扩展<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#23-%E5%A4%9A%E4%BA%BA%E4%BC%9A%E8%AE%AE%E7%9A%84%E6%B6%88%E6%81%AF%E6%89%A9%E5%B1%95" class="hash-link" aria-label="Direct link to 2.3 多人会议的消息扩展" title="Direct link to 2.3 多人会议的消息扩展" translate="no">​</a></h3>
<p>当 Room 内 N &gt; 2 时，P2P Mesh 需要 <strong>N×(N-1)/2</strong> 条 PeerConnection。信令消息需扩展：</p>
<table><thead><tr><th style="text-align:left">额外消息</th><th style="text-align:left">用途</th></tr></thead><tbody><tr><td style="text-align:left"><code>publish</code></td><td style="text-align:left">通知「我要发送媒体」，触发向所有其他 peer 发 Offer</td></tr><tr><td style="text-align:left"><code>subscribe</code></td><td style="text-align:left">SFU 模式下请求订阅某 Track（Ch12）</td></tr><tr><td style="text-align:left"><code>mute</code> / <code>unmute</code></td><td style="text-align:left">通知静音状态（可选，也可仅本地处理）</td></tr><tr><td style="text-align:left"><code>kick</code></td><td style="text-align:left">管理员踢人（服务端下发 disconnect）</td></tr></tbody></table>
<p>生产环境更推荐使用 <strong>SFU（如 LiveKit）</strong>，此时每个客户端只需 <strong>1 条</strong> PeerConnection 到 SFU，信令大幅简化——LiveKit 内置 Room 管理，你的信令层主要负责 <strong>Token 下发</strong>。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三room-与会话状态机">三、Room 与会话状态机<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#%E4%B8%89room-%E4%B8%8E%E4%BC%9A%E8%AF%9D%E7%8A%B6%E6%80%81%E6%9C%BA" class="hash-link" aria-label="Direct link to 三、Room 与会话状态机" title="Direct link to 三、Room 与会话状态机" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-单个-peer-的状态机">3.1 单个 Peer 的状态机<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#31-%E5%8D%95%E4%B8%AA-peer-%E7%9A%84%E7%8A%B6%E6%80%81%E6%9C%BA" class="hash-link" aria-label="Direct link to 3.1 单个 Peer 的状态机" title="Direct link to 3.1 单个 Peer 的状态机" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-服务端-room-数据结构">3.2 服务端 Room 数据结构<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#32-%E6%9C%8D%E5%8A%A1%E7%AB%AF-room-%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84" class="hash-link" aria-label="Direct link to 3.2 服务端 Room 数据结构" title="Direct link to 3.2 服务端 Room 数据结构" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * Room 内存模型（生产环境应持久化到 Redis）</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * roomId -&gt; Map&lt;peerId, </span><span class="token doc-comment comment punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> ws, identity, joinedAt </span><span class="token doc-comment comment punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic">&gt;</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> rooms </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getRoom</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">rooms</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">has</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> rooms</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">set</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> rooms</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">类型</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left"><code>roomId</code></td><td style="text-align:left">string</td><td style="text-align:left">房间唯一标识，如 UUID 或用户自定义 slug</td></tr><tr><td style="text-align:left"><code>peerId</code></td><td style="text-align:left">string</td><td style="text-align:left">服务端分配的连接 ID（每次连接不同）</td></tr><tr><td style="text-align:left"><code>identity</code></td><td style="text-align:left">string</td><td style="text-align:left">业务层用户标识（同一用户重连可相同）</td></tr><tr><td style="text-align:left"><code>ws</code></td><td style="text-align:left">WebSocket</td><td style="text-align:left">活跃连接句柄</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>Identity vs PeerId</div><div class="admonitionContent_BuS1"><ul>
<li class=""><strong>Identity</strong>：业务语义，如 <code>"alice@company.com"</code>，可重复（多端登录）</li>
<li class=""><strong>PeerId</strong>：连接语义，如 UUID，每次 WebSocket 连接唯一</li>
</ul><p>混淆两者会导致重连时无法正确清理旧连接。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四websocket-信令服务器完整实现">四、WebSocket 信令服务器完整实现<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#%E5%9B%9Bwebsocket-%E4%BF%A1%E4%BB%A4%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%AE%8C%E6%95%B4%E5%AE%9E%E7%8E%B0" class="hash-link" aria-label="Direct link to 四、WebSocket 信令服务器完整实现" title="Direct link to 四、WebSocket 信令服务器完整实现" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-服务端核心代码">4.1 服务端核心代码<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#41-%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%A0%B8%E5%BF%83%E4%BB%A3%E7%A0%81" class="hash-link" aria-label="Direct link to 4.1 服务端核心代码" title="Direct link to 4.1 服务端核心代码" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// examples/webrtc-lab/signaling/server.js</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">WebSocketServer</span><span class="token imports"> </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ws"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:#393A34">{</span><span class="token imports"> randomUUID </span><span class="token imports punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"crypto"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">PORT</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> process</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">env</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">PORT</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">8080</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> rooms </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getRoom</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">rooms</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">has</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> rooms</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">set</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> rooms</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">broadcast</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">roomId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> senderId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> payload</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> room </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getRoom</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">peerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> peer</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">of</span><span class="token plain"> room</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">peerId </span><span class="token operator" style="color:#393A34">!==</span><span class="token plain"> senderId </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> peer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">readyState</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> peer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">OPEN</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      peer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token spread operator" style="color:#393A34">...</span><span class="token plain">payload</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> senderId </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">sendTo</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">roomId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> targetPeerId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> payload</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> peer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getRoom</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">get</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">targetPeerId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">peer</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">readyState</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> peer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">OPEN</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    peer</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">payload</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> wss </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">WebSocketServer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">port</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">PORT</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">wss</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"connection"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">ws</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> peerId </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">randomUUID</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> roomId </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> identity </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"message"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">raw</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> msg</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">try</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      msg </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">parse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">raw</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">toString</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"error"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">message</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Invalid JSON"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">switch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"join"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        roomId </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">roomId</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"default"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        identity </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">identity</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> peerId</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> room </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getRoom</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">set</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">peerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> ws</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">joinedAt</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token known-class-name class-name">Date</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">now</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> peers </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token spread operator" style="color:#393A34">...</span><span class="token plain">room</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">entries</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter punctuation" style="color:#393A34">[</span><span class="token parameter">id</span><span class="token parameter punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> id </span><span class="token operator" style="color:#393A34">!==</span><span class="token plain"> peerId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter punctuation" style="color:#393A34">[</span><span class="token parameter">id</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> p</span><span class="token parameter punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">peerId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">identity</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> p</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">identity</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"joined"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> peerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> peers </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token function" style="color:#d73a49">broadcast</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> peerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"peer-joined"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> peerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"offer"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"answer"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">to</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token function" style="color:#d73a49">sendTo</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">to</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token spread operator" style="color:#393A34">...</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword module" style="color:#00009f">from</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> peerId </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">else</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token function" style="color:#d73a49">broadcast</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> peerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"leave"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token function" style="color:#d73a49">getRoom</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">delete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">peerId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token function" style="color:#d73a49">broadcast</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> peerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"peer-left"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> peerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword module" style="color:#00009f">default</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"error"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">message</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">Unknown type: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">msg</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">type</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">on</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"close"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">getRoom</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">delete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">peerId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">broadcast</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> peerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"peer-left"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> peerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> identity </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">Signaling server: ws://localhost:</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation constant" style="color:#36acaa">PORT</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-启动与验证">4.2 启动与验证<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#42-%E5%90%AF%E5%8A%A8%E4%B8%8E%E9%AA%8C%E8%AF%81" class="hash-link" aria-label="Direct link to 4.2 启动与验证" title="Direct link to 4.2 启动与验证" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token builtin class-name">cd</span><span class="token plain"> examples/webrtc-lab/signaling</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">npm</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">install</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">npm</span><span class="token plain"> start</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 输出: Signaling server: ws://localhost:8080</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五客户端信令集成">五、客户端信令集成<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#%E4%BA%94%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%BF%A1%E4%BB%A4%E9%9B%86%E6%88%90" class="hash-link" aria-label="Direct link to 五、客户端信令集成" title="Direct link to 五、客户端信令集成" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-信令管理类">5.1 信令管理类<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#51-%E4%BF%A1%E4%BB%A4%E7%AE%A1%E7%90%86%E7%B1%BB" class="hash-link" aria-label="Direct link to 5.1 信令管理类" title="Direct link to 5.1 信令管理类" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">SignalingClient</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">constructor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">url</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> roomId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> identity</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">url</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> url</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">roomId</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> roomId</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">identity</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> identity</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">peerId</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">peers</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Map</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ws</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onOffer</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onAnswer</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onCandidate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onPeerJoined</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onPeerLeft</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">connect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Promise</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">resolve</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> reject</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ws</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">WebSocket</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">url</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onopen</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"join"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">roomId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">roomId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">identity</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">identity</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onmessage</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">handleMessage</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">parse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">data</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onerror</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> reject</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onclose</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">reconnect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">_resolveConnect</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> resolve</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">handleMessage</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">switch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">type</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"joined"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">peerId</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">peerId</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">peers</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">p</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">peers</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">set</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">p</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">peerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> p</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">_resolveConnect</span><span class="token operator" style="color:#393A34">?.</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"peer-joined"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">peers</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">set</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">peerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onPeerJoined</span><span class="token operator" style="color:#393A34">?.</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"peer-left"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">peers</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">delete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">peerId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onPeerLeft</span><span class="token operator" style="color:#393A34">?.</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"offer"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onOffer</span><span class="token operator" style="color:#393A34">?.</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"answer"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onAnswer</span><span class="token operator" style="color:#393A34">?.</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onCandidate</span><span class="token operator" style="color:#393A34">?.</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ws</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">readyState </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token maybe-class-name">WebSocket</span><span class="token punctuation" style="color:#393A34">.</span><span class="token constant" style="color:#36acaa">OPEN</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">ws</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">sendOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">targetPeerId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> sdp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"offer"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sdp</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">to</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> targetPeerId </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">sendAnswer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">targetPeerId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> sdp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"answer"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sdp</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">to</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> targetPeerId </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">sendCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">targetPeerId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> candidate</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">to</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> targetPeerId </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">reconnect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">setTimeout</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">connect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2000</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-与-rtcpeerconnection-绑定">5.2 与 RTCPeerConnection 绑定<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#52-%E4%B8%8E-rtcpeerconnection-%E7%BB%91%E5%AE%9A" class="hash-link" aria-label="Direct link to 5.2 与 RTCPeerConnection 绑定" title="Direct link to 5.2 与 RTCPeerConnection 绑定" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sig </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">SignalingClient</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ws://localhost:8080"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"demo-room"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"alice"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> sig</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">connect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"stun:stun.l.google.com:19302"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 媒体</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stream </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getUserMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">audio</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">stream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">t</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">t</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> stream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">ontrack</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> remoteVideo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">srcObject</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">streams</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// ICE → 信令</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onicecandidate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> sig</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">sendCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">remotePeerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 信令 → PeerConnection</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">sig</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onOffer</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setRemoteDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> answer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createAnswer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">answer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  sig</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">sendAnswer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token keyword module" style="color:#00009f">from</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">sig</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onAnswer</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setRemoteDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">sig</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onCandidate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addIceCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 新 peer 加入 → 发起 Offer</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">sig</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onPeerJoined</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  remotePeerId </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">peerId</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> offer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  sig</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">sendOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">peerId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六生产级安全设计">六、生产级安全设计<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#%E5%85%AD%E7%94%9F%E4%BA%A7%E7%BA%A7%E5%AE%89%E5%85%A8%E8%AE%BE%E8%AE%A1" class="hash-link" aria-label="Direct link to 六、生产级安全设计" title="Direct link to 六、生产级安全设计" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">机制</th><th style="text-align:left">说明</th><th style="text-align:left">参考</th></tr></thead><tbody><tr><td style="text-align:left"><strong>WSS</strong></td><td style="text-align:left">信令必须 TLS，防 SDP/ICE 被窃听</td><td style="text-align:left">Let's Encrypt</td></tr><tr><td style="text-align:left"><strong>JWT 鉴权</strong></td><td style="text-align:left">连接时 <code>?token=xxx</code>，服务端验证 Room 权限</td><td style="text-align:left"><a href="https://docs.livekit.io/home/get-started/authentication/" target="_blank" rel="noopener noreferrer" class="">LiveKit Token</a></td></tr><tr><td style="text-align:left"><strong>TURN 短期凭证</strong></td><td style="text-align:left">通过 API 下发，防 TURN 被滥用</td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8656" target="_blank" rel="noopener noreferrer" class="">RFC 8656 REST API</a></td></tr><tr><td style="text-align:left"><strong>Room 隔离</strong></td><td style="text-align:left">不同 Room 消息不可互串</td><td style="text-align:left">服务端路由校验</td></tr><tr><td style="text-align:left"><strong>Rate Limiting</strong></td><td style="text-align:left">防 Candidate 洪水攻击</td><td style="text-align:left">每 peer 每秒 ≤50 条</td></tr><tr><td style="text-align:left"><strong>Identity 校验</strong></td><td style="text-align:left">防止冒充他人 Identity</td><td style="text-align:left">JWT <code>sub</code> 字段绑定</td></tr></tbody></table>
<p>LiveKit Token 示例结构：</p>
<div class="custom-code-block" data-language="json"><div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"sub"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"user-123"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"video"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"roomJoin"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"room"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"my-room"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"canPublish"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"canSubscribe"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"exp"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1718000000</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七常见问题与排查">七、常见问题与排查<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#%E4%B8%83%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E4%B8%8E%E6%8E%92%E6%9F%A5" class="hash-link" aria-label="Direct link to 七、常见问题与排查" title="Direct link to 七、常见问题与排查" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">现象</th><th style="text-align:left">可能原因</th><th style="text-align:left">排查</th></tr></thead><tbody><tr><td style="text-align:left">Offer 发出无 Answer</td><td style="text-align:left">对端未监听 <code>onOffer</code></td><td style="text-align:left">检查信令消息日志</td></tr><tr><td style="text-align:left">ICE 永远 checking</td><td style="text-align:left">Candidate 未转发</td><td style="text-align:left">确认 <code>onicecandidate</code> → 信令 → <code>addIceCandidate</code></td></tr><tr><td style="text-align:left">重复连接</td><td style="text-align:left">重连未清理旧 peerId</td><td style="text-align:left">服务端 <code>close</code> 事件删除 peer</td></tr><tr><td style="text-align:left">跨 Room 串流</td><td style="text-align:left">roomId 路由错误</td><td style="text-align:left">日志打印 roomId + peerId</td></tr><tr><td style="text-align:left">信令通但无媒体</td><td style="text-align:left">媒体不走信令</td><td style="text-align:left">查 ICE/DTLS 状态，非信令问题</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八实战-lab">八、实战 Lab<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#%E5%85%AB%E5%AE%9E%E6%88%98-lab" class="hash-link" aria-label="Direct link to 八、实战 Lab" title="Direct link to 八、实战 Lab" translate="no">​</a></h2>
<ol>
<li class="">启动 <code>signaling/server.js</code>，两个 Tab 加入同一 Room</li>
<li class="">在服务端打印所有消息，观察 Offer → Answer → Candidate 顺序</li>
<li class="">故意断开一个 Tab 的 WebSocket，观察 <code>peer-left</code> 事件</li>
<li class="">对比：手工信令（Ch2）vs WebSocket 信令的时序差异</li>
<li class="">（进阶）为 <code>join</code> 增加 JWT 验证中间件</li>
</ol>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九本章小结与下一篇预告">九、本章小结与下一篇预告<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#%E4%B9%9D%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93%E4%B8%8E%E4%B8%8B%E4%B8%80%E7%AF%87%E9%A2%84%E5%91%8A" class="hash-link" aria-label="Direct link to 九、本章小结与下一篇预告" title="Direct link to 九、本章小结与下一篇预告" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">要点</th><th style="text-align:left">内容</th></tr></thead><tbody><tr><td style="text-align:left">信令定位</td><td style="text-align:left">WebRTC 标准外，应用自定义控制面</td></tr><tr><td style="text-align:left">核心消息</td><td style="text-align:left">join / offer / answer / candidate</td></tr><tr><td style="text-align:left">传输</td><td style="text-align:left">WebSocket（生产用 WSS）</td></tr><tr><td style="text-align:left">安全</td><td style="text-align:left">JWT + TURN 短期凭证 + Room 隔离</td></tr><tr><td style="text-align:left">生产选型</td><td style="text-align:left">小规模自建；大规模用 LiveKit 内置 Room</td></tr></tbody></table>
<p><strong>下一篇（Ch4）</strong> 深入 Offer/Answer 里的 SDP 文本结构：<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a>。</p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-signaling#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://webrtcforthecurious.com/docs/03-connecting/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — Signaling</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史 / 为何不标准化信令</a></li>
<li class=""><a href="https://docs.livekit.io/home/get-started/authentication/" target="_blank" rel="noopener noreferrer" class="">LiveKit — Authentication</a></li>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8656" target="_blank" rel="noopener noreferrer" class="">RFC 8656 — TURN REST API</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>实时通信</category>
            <category>教程</category>
            <category>Deep Dive</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (2)：第一个 P2P 视频通话]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call</guid>
            <pubDate>Sat, 13 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[RTCPeerConnection 完整生命周期、JSEP Offer/Answer 模型、ICE Candidate 交换、Perfect Negotiation 与连接状态机，用手工信令跑通第一个端到端 P2P 视频通话]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"跑通第一个通话，比读完十页 RFC 更有说服力。"</p>
</blockquote>
<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">Ch1</a> 我们学会了采集媒体。本章用 <code>RTCPeerConnection</code> 把媒体发给另一个浏览器——<strong>第一个完整 P2P 视频通话</strong>。</p>
<p>Serge Lachapelle 在 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史</a> 中解释了一个关键设计决策：<strong>IETF 刻意不对信令（Signaling）重新标准化</strong>，因为 SIP 等方案已存在，"重新标准化只会引发政治斗争"。这意味着 WebRTC 只标准化了媒体通道（SDP、ICE、DTLS、SRTP），而 <strong>Offer/Answer 的传递方式完全由应用决定</strong>——本章先用「复制粘贴 JSON」理解原理，<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">Ch3</a> 再替换为 WebSocket 信令服务器。</p>
<p>配套代码：<a href="https://github.com/rainlib/idea_visual/tree/main/examples/webrtc-lab/client/ch02-p2p-basic" target="_blank" rel="noopener noreferrer" class=""><code>examples/webrtc-lab/client/ch02-p2p-basic/</code></a></p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>对等连接</strong></td><td style="text-align:left">RTCPeerConnection</td><td style="text-align:left">浏览器 WebRTC 的核心对象，管理 SDP 协商、ICE、DTLS、媒体收发</td></tr><tr><td style="text-align:left"><strong>JSEP</strong></td><td style="text-align:left">JavaScript Session Establishment Protocol</td><td style="text-align:left">RFC 8829，定义浏览器如何用 Offer/Answer 交换 SDP</td></tr><tr><td style="text-align:left"><strong>Offer</strong></td><td style="text-align:left">—</td><td style="text-align:left">发起方的会话描述，包含媒体能力、ICE 参数、DTLS 指纹</td></tr><tr><td style="text-align:left"><strong>Answer</strong></td><td style="text-align:left">—</td><td style="text-align:left">应答方的会话描述，确认选择的编解码器与网络参数</td></tr><tr><td style="text-align:left"><strong>SDP</strong></td><td style="text-align:left">Session Description Protocol</td><td style="text-align:left">文本格式的会话描述，详见 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">Ch4</a></td></tr><tr><td style="text-align:left"><strong>ICE</strong></td><td style="text-align:left">Interactive Connectivity Establishment</td><td style="text-align:left">NAT 穿透协议，通过候选地址配对找到最优路径，详见 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">Ch5</a></td></tr><tr><td style="text-align:left"><strong>ICE Candidate</strong></td><td style="text-align:left">—</td><td style="text-align:left">一个可用的网络地址（IP<!-- -->:Port<!-- --> + 类型：host/srflx/relay）</td></tr><tr><td style="text-align:left"><strong>信令</strong></td><td style="text-align:left">Signaling</td><td style="text-align:left">在 PeerConnection 之外传递 SDP 和 ICE Candidate 的控制通道</td></tr><tr><td style="text-align:left"><strong>Trickle ICE</strong></td><td style="text-align:left">—</td><td style="text-align:left">边收集 ICE Candidate 边发送，而非等全部收集完毕</td></tr><tr><td style="text-align:left"><strong>Glare</strong></td><td style="text-align:left">冲突</td><td style="text-align:left">双方同时发送 Offer 导致的协商冲突</td></tr><tr><td style="text-align:left"><strong>Perfect Negotiation</strong></td><td style="text-align:left">—</td><td style="text-align:left">W3C 推荐的无冲突协商模式，通过 polite/impolite 角色解决 glare</td></tr><tr><td style="text-align:left"><strong>信令状态</strong></td><td style="text-align:left">signalingState</td><td style="text-align:left"><code>stable</code> → <code>have-local-offer</code> → <code>have-remote-offer</code> → <code>closed</code></td></tr><tr><td style="text-align:left"><strong>ICE 连接状态</strong></td><td style="text-align:left">iceConnectionState</td><td style="text-align:left"><code>new</code> → <code>checking</code> → <code>connected</code> → <code>completed</code> / <code>failed</code></td></tr><tr><td style="text-align:left"><strong>连接状态</strong></td><td style="text-align:left">connectionState</td><td style="text-align:left">综合 ICE + DTLS 的整体状态：<code>connecting</code> → <code>connected</code> → <code>closed</code></td></tr><tr><td style="text-align:left"><strong>Unified Plan</strong></td><td style="text-align:left">—</td><td style="text-align:left">现代 SDP 语义，每个 m-line 对应一个 transceiver（Plan B 已废弃）</td></tr><tr><td style="text-align:left"><strong>Transceiver</strong></td><td style="text-align:left">RTCRtpTransceiver</td><td style="text-align:left">绑定 sender + receiver 的媒体通道，Unified Plan 的核心单元</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一rtcpeerconnection-在协议栈中的位置">一、RTCPeerConnection 在协议栈中的位置<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#%E4%B8%80rtcpeerconnection-%E5%9C%A8%E5%8D%8F%E8%AE%AE%E6%A0%88%E4%B8%AD%E7%9A%84%E4%BD%8D%E7%BD%AE" class="hash-link" aria-label="Direct link to 一、RTCPeerConnection 在协议栈中的位置" title="Direct link to 一、RTCPeerConnection 在协议栈中的位置" translate="no">​</a></h2>
<!-- -->
<p><code>RTCPeerConnection</code> 是浏览器暴露给开发者的「黑盒」——你负责交换 SDP 和 ICE Candidate（信令），浏览器自动完成 ICE 连通性检查、DTLS 握手、SRTP 密钥协商和媒体传输。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="11-开发者职责-vs-浏览器职责">1.1 开发者职责 vs 浏览器职责<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#11-%E5%BC%80%E5%8F%91%E8%80%85%E8%81%8C%E8%B4%A3-vs-%E6%B5%8F%E8%A7%88%E5%99%A8%E8%81%8C%E8%B4%A3" class="hash-link" aria-label="Direct link to 1.1 开发者职责 vs 浏览器职责" title="Direct link to 1.1 开发者职责 vs 浏览器职责" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">开发者负责（信令层）</th><th style="text-align:left">浏览器自动完成（媒体层）</th></tr></thead><tbody><tr><td style="text-align:left">创建 / 转发 Offer / Answer</td><td style="text-align:left">ICE 候选收集与连通性检查</td></tr><tr><td style="text-align:left">转发 ICE Candidate</td><td style="text-align:left">STUN Binding Request</td></tr><tr><td style="text-align:left">选择 Caller / Callee 角色</td><td style="text-align:left">DTLS 握手与证书验证</td></tr><tr><td style="text-align:left">实现信令通道（Ch3 WebSocket）</td><td style="text-align:left">SRTP 密钥导出与加解密</td></tr><tr><td style="text-align:left">错误处理与重连策略</td><td style="text-align:left">编解码器选择与 RTP 打包</td></tr><tr><td style="text-align:left">Perfect Negotiation 角色分配</td><td style="text-align:left">RTCP 反馈与拥塞控制（Ch10）</td></tr></tbody></table>
<p><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 历史</a> 中强调：WebRTC 的设计哲学是 <strong>「Bring Your Own Signaling」</strong>——媒体标准化，信令留给应用。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="12-rtcpeerconnection-构造配置">1.2 RTCPeerConnection 构造配置<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#12-rtcpeerconnection-%E6%9E%84%E9%80%A0%E9%85%8D%E7%BD%AE" class="hash-link" aria-label="Direct link to 1.2 RTCPeerConnection 构造配置" title="Direct link to 1.2 RTCPeerConnection 构造配置" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// ICE 服务器 — STUN 发现公网地址，TURN 中继（Ch5 / Ch14）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"stun:stun.l.google.com:19302"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// { urls: "turn:turn.example.com:3478", username: "user", credential: "pass" },</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// ICE 传输策略</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// "all"（默认）：尝试 host + srflx + relay</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// "relay"：仅使用 TURN 中继（企业防火墙场景）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">iceTransportPolicy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"all"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// SDP 打包策略</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// "balanced"（默认）：音频和视频分别打包</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// "max-compat"：每个 track 独立 m-line</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// "max-bundle"：所有媒体打包到一条传输</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">bundlePolicy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"balanced"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// RTCP 复用 — 现代浏览器固定 true</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">rtcpMuxPolicy</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"require"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 预收集 ICE 候选 — 加速首次连接</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">iceCandidatePoolSize</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 设为 2-10 可预热，但消耗 STUN 资源</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二jsep-offeranswer-模型">二、JSEP Offer/Answer 模型<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#%E4%BA%8Cjsep-offeranswer-%E6%A8%A1%E5%9E%8B" class="hash-link" aria-label="Direct link to 二、JSEP Offer/Answer 模型" title="Direct link to 二、JSEP Offer/Answer 模型" translate="no">​</a></h2>
<p><a href="https://datatracker.ietf.org/doc/html/rfc8829" target="_blank" rel="noopener noreferrer" class="">JSEP（RFC 8829）</a> 定义了 WebRTC 的会话建立流程。核心思想：<strong>一方创建 Offer 描述自己的媒体能力，另一方创建 Answer 确认选择</strong>。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-完整信令时序">2.1 完整信令时序<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#21-%E5%AE%8C%E6%95%B4%E4%BF%A1%E4%BB%A4%E6%97%B6%E5%BA%8F" class="hash-link" aria-label="Direct link to 2.1 完整信令时序" title="Direct link to 2.1 完整信令时序" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="22-信令状态机">2.2 信令状态机<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#22-%E4%BF%A1%E4%BB%A4%E7%8A%B6%E6%80%81%E6%9C%BA" class="hash-link" aria-label="Direct link to 2.2 信令状态机" title="Direct link to 2.2 信令状态机" translate="no">​</a></h3>
<p>每次 <code>setLocalDescription</code> / <code>setRemoteDescription</code> 都会推进 <code>signalingState</code>：</p>
<!-- -->
<table><thead><tr><th style="text-align:left">signalingState</th><th style="text-align:left">含义</th><th style="text-align:left">下一步操作</th></tr></thead><tbody><tr><td style="text-align:left"><code>stable</code></td><td style="text-align:left">无进行中的 Offer/Answer</td><td style="text-align:left">可以 <code>createOffer</code></td></tr><tr><td style="text-align:left"><code>have-local-offer</code></td><td style="text-align:left">已发出 Offer，等待 Answer</td><td style="text-align:left">对端应 <code>createAnswer</code></td></tr><tr><td style="text-align:left"><code>have-remote-offer</code></td><td style="text-align:left">收到 Offer，等待本地 Answer</td><td style="text-align:left">本地应 <code>createAnswer</code></td></tr><tr><td style="text-align:left"><code>closed</code></td><td style="text-align:left">连接已关闭</td><td style="text-align:left">不可恢复</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="23-addtrack-vs-addtransceiver">2.3 addTrack vs addTransceiver<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#23-addtrack-vs-addtransceiver" class="hash-link" aria-label="Direct link to 2.3 addTrack vs addTransceiver" title="Direct link to 2.3 addTrack vs addTransceiver" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 方式 1：addTrack — 简单场景首选</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">videoTrack</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> localStream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">audioTrack</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> localStream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 方式 2：addTransceiver — 需要精细控制方向时</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTransceiver</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">direction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"sendrecv"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// "sendonly" | "recvonly" | "inactive"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">streams</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">localStream</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTransceiver</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"audio"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">direction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"sendrecv"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 仅接收（不发送摄像头，但想看对方画面）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTransceiver</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">direction</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"recvonly"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>Unified Plan</div><div class="admonitionContent_BuS1"><p>现代浏览器均使用 Unified Plan SDP 语义。每个 transceiver 对应 SDP 中一条 m-line。旧的 Plan B（Chrome 63 前）已废弃，无需了解。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三最小可运行代码1v1-视频通话">三、最小可运行代码：1v1 视频通话<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#%E4%B8%89%E6%9C%80%E5%B0%8F%E5%8F%AF%E8%BF%90%E8%A1%8C%E4%BB%A3%E7%A0%811v1-%E8%A7%86%E9%A2%91%E9%80%9A%E8%AF%9D" class="hash-link" aria-label="Direct link to 三、最小可运行代码：1v1 视频通话" title="Direct link to 三、最小可运行代码：1v1 视频通话" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-共享配置">3.1 共享配置<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#31-%E5%85%B1%E4%BA%AB%E9%85%8D%E7%BD%AE" class="hash-link" aria-label="Direct link to 3.1 共享配置" title="Direct link to 3.1 共享配置" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// STUN 服务器 — 帮助发现公网地址（Ch5 深入）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token constant" style="color:#36acaa">ICE_SERVERS</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">iceServers</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"stun:stun.l.google.com:19302"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">urls</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"stun:stun1.l.google.com:19302"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-通用-peerconnection-工厂">3.2 通用 PeerConnection 工厂<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#32-%E9%80%9A%E7%94%A8-peerconnection-%E5%B7%A5%E5%8E%82" class="hash-link" aria-label="Direct link to 3.2 通用 PeerConnection 工厂" title="Direct link to 3.2 通用 PeerConnection 工厂" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 创建 RTCPeerConnection 并绑定事件</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@param</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">MediaStream</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment parameter" style="color:#999988;font-style:italic">localStream</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> - Ch1 采集的本地流</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@param</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">HTMLVideoElement</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment parameter" style="color:#999988;font-style:italic">remoteVideo</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> - 远端画面元素</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@param</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">Function</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment parameter" style="color:#999988;font-style:italic">onIceCandidate</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> - ICE Candidate 回调（发给信令）</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">localStream</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> remoteVideo</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> onIceCandidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token constant" style="color:#36acaa">ICE_SERVERS</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 添加本地轨道 — 每个 track 关联一个 stream</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> track </span><span class="token keyword" style="color:#00009f">of</span><span class="token plain"> localStream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">track</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> localStream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 接收远端轨道</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">ontrack</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// event.streams[0] 是远端 MediaStream</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// event.track 是单个 MediaStreamTrack</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// event.transceiver 是 RTCRtpTransceiver</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">streams</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      remoteVideo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">srcObject</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">streams</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">收到远端 </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">event</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">track</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">kind</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c"> track</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// ICE Candidate 产出 — 通过信令发给对端</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onicecandidate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">onIceCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// event.candidate === null 表示 ICE Gathering 完成</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 需要重新协商时触发（addTrack / removeTrack / ICE Restart）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onnegotiationneeded</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"negotiationneeded — 需要重新 Offer/Answer"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 1v1 首次连接手动 createOffer，此处通常不处理</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// Ch3 多人场景 + Perfect Negotiation 会用到</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 状态监控（见第六节）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">oniceconnectionstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ICE 状态:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceConnectionState</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onconnectionstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"连接状态:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">connectionState</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onsignalingstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"信令状态:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">signalingState</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="33-caller呼叫方流程">3.3 Caller（呼叫方）流程<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#33-caller%E5%91%BC%E5%8F%AB%E6%96%B9%E6%B5%81%E7%A8%8B" class="hash-link" aria-label="Direct link to 3.3 Caller（呼叫方）流程" title="Direct link to 3.3 Caller（呼叫方）流程" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">callerFlow</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">localStream</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> remoteVideo</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> sendSignal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">localStream</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> remoteVideo</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">sendSignal</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> candidate </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 1. 创建 Offer</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> offer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// createOffer 可选参数：</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// { offerToReceiveAudio: true, offerToReceiveVideo: true }</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 现代浏览器通过 addTrack 已隐含，通常不需要</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 2. 设置本地描述 — 触发 ICE Gathering</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 3. 通过信令发送 Offer</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">sendSignal</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"offer"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">sdp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 4. 等待对端 Answer（在信令回调中处理）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 收到 Answer 时</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">handleAnswer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> answerSdp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setRemoteDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">answerSdp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 此时 ICE 连通性检查自动开始</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="34-callee应答方流程">3.4 Callee（应答方）流程<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#34-callee%E5%BA%94%E7%AD%94%E6%96%B9%E6%B5%81%E7%A8%8B" class="hash-link" aria-label="Direct link to 3.4 Callee（应答方）流程" title="Direct link to 3.4 Callee（应答方）流程" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">calleeFlow</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">localStream</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> remoteVideo</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> offerSdp</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> sendSignal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">localStream</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> remoteVideo</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">sendSignal</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> candidate </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 1. 设置远端 Offer</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setRemoteDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offerSdp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 2. 创建 Answer</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> answer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createAnswer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">answer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 3. 通过信令发送 Answer</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">sendSignal</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"answer"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">sdp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="35-完整交互流程图">3.5 完整交互流程图<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#35-%E5%AE%8C%E6%95%B4%E4%BA%A4%E4%BA%92%E6%B5%81%E7%A8%8B%E5%9B%BE" class="hash-link" aria-label="Direct link to 3.5 完整交互流程图" title="Direct link to 3.5 完整交互流程图" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="36-完整端到端示例类">3.6 完整端到端示例类<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#36-%E5%AE%8C%E6%95%B4%E7%AB%AF%E5%88%B0%E7%AB%AF%E7%A4%BA%E4%BE%8B%E7%B1%BB" class="hash-link" aria-label="Direct link to 3.6 完整端到端示例类" title="Direct link to 3.6 完整端到端示例类" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 1v1 通话管理器 — 整合 Caller / Callee 逻辑</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * Ch2 Lab 的面向对象版本</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">class</span><span class="token plain"> </span><span class="token class-name">P2PCall</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token doc-comment comment" style="color:#999988;font-style:italic">/** </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@type</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">RTCPeerConnection </span><span class="token doc-comment comment class-name operator" style="color:#393A34;font-style:italic">|</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name keyword" style="color:#00009f;font-style:italic">null</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token doc-comment comment" style="color:#999988;font-style:italic">/** </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@type</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">MediaStream </span><span class="token doc-comment comment class-name operator" style="color:#393A34;font-style:italic">|</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name keyword" style="color:#00009f;font-style:italic">null</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  localStream </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token doc-comment comment" style="color:#999988;font-style:italic">/** </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@type</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">RTCIceCandidate</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">[</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">]</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pendingCandidates </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">constructor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter punctuation" style="color:#393A34">{</span><span class="token parameter"> role</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> localVideo</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> remoteVideo</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> onSignal </span><span class="token parameter punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">role</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> role</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// "caller" | "callee"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localVideo</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> localVideo</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">remoteVideo</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> remoteVideo</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onSignal</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> onSignal</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">startLocalMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localStream</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getUserMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">audio</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localVideo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">srcObject</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localStream</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localVideo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">muted</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">createConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">RTCPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token constant" style="color:#36acaa">ICE_SERVERS</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> track </span><span class="token keyword" style="color:#00009f">of</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localStream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">track</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localStream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">ontrack</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">remoteVideo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">srcObject</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">streams</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onicecandidate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">e</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pendingCandidates</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">e</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onconnectionstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">[</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation keyword" style="color:#00009f">this</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">role</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">] connection:</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">connectionState</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createOfferBundle</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> offer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">waitIceGatheringComplete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">sdp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">candidates</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pendingCandidates</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">createAnswerBundle</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">offerBundle</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setRemoteDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offerBundle</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> c </span><span class="token keyword" style="color:#00009f">of</span><span class="token plain"> offerBundle</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidates</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addIceCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> answer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createAnswer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">answer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">waitIceGatheringComplete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">sdp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">candidates</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pendingCandidates</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">completeConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">answerBundle</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setRemoteDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">answerBundle</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> c </span><span class="token keyword" style="color:#00009f">of</span><span class="token plain"> answerBundle</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidates</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addIceCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">hangup</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token operator" style="color:#393A34">?.</span><span class="token method function property-access" style="color:#d73a49">close</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localStream</span><span class="token operator" style="color:#393A34">?.</span><span class="token method function property-access" style="color:#d73a49">getTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">t</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> t</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stop</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localVideo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">srcObject</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">remoteVideo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">srcObject</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">pc</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localStream</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四ice-candidate-交换">四、ICE Candidate 交换<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#%E5%9B%9Bice-candidate-%E4%BA%A4%E6%8D%A2" class="hash-link" aria-label="Direct link to 四、ICE Candidate 交换" title="Direct link to 四、ICE Candidate 交换" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-ice-gathering-过程">4.1 ICE Gathering 过程<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#41-ice-gathering-%E8%BF%87%E7%A8%8B" class="hash-link" aria-label="Direct link to 4.1 ICE Gathering 过程" title="Direct link to 4.1 ICE Gathering 过程" translate="no">​</a></h3>
<p><code>setLocalDescription()</code> 调用后，浏览器开始 <strong>ICE Gathering</strong>——收集所有可用的网络候选地址：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-ice-candidate-格式解析">4.2 ICE Candidate 格式解析<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#42-ice-candidate-%E6%A0%BC%E5%BC%8F%E8%A7%A3%E6%9E%90" class="hash-link" aria-label="Direct link to 4.2 ICE Candidate 格式解析" title="Direct link to 4.2 ICE Candidate 格式解析" translate="no">​</a></h3>
<p>一条 ICE Candidate 字符串包含丰富的网络信息：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">candidate:842163049 1 udp 1677729535 192.168.1.100 54321 typ host</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">         │           │ │   │           │               │      │</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">         │           │ │   │           │               │      └─ 类型</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">         │           │ │   │           │               └─ 端口</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">         │           │ │   │           └─ IP 地址</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">         │           │ │   └─ 优先级</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">         │           │ └─ 传输协议 (udp/tcp)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">         │           └─ 组件 ID (1=RTP, 2=RTCP)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">         └─ foundation (候选唯一标识)</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">类型</th><th style="text-align:left">含义</th><th style="text-align:left">示例场景</th></tr></thead><tbody><tr><td style="text-align:left"><code>host</code></td><td style="text-align:left">本机网卡地址</td><td style="text-align:left">同一局域网直连</td></tr><tr><td style="text-align:left"><code>srflx</code></td><td style="text-align:left">STUN 反射的公网地址</td><td style="text-align:left">跨 NAT 但可穿透</td></tr><tr><td style="text-align:left"><code>relay</code></td><td style="text-align:left">TURN 中继地址</td><td style="text-align:left">对称 NAT / 企业防火墙</td></tr><tr><td style="text-align:left"><code>prflx</code></td><td style="text-align:left">对端反射（连通性检查中发现）</td><td style="text-align:left">浏览器自动产生</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="43-收集并发送-candidate">4.3 收集并发送 Candidate<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#43-%E6%94%B6%E9%9B%86%E5%B9%B6%E5%8F%91%E9%80%81-candidate" class="hash-link" aria-label="Direct link to 4.3 收集并发送 Candidate" title="Direct link to 4.3 收集并发送 Candidate" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 收集并缓存 ICE Candidate</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> pendingCandidates </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onicecandidate</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">event</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 通过信令发送给对端</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    pendingCandidates</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    signaling</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">send</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">candidate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidate</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">toJSON</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">else</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ICE Gathering 完成，共"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> pendingCandidates</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">length</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"个候选"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="44-接收远端-candidate">4.4 接收远端 Candidate<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#44-%E6%8E%A5%E6%94%B6%E8%BF%9C%E7%AB%AF-candidate" class="hash-link" aria-label="Direct link to 4.4 接收远端 Candidate" title="Direct link to 4.4 接收远端 Candidate" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 对端收到 ICE Candidate 后添加</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 注意：可以在 setRemoteDescription 之前或之后到达</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">handleRemoteCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">try</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addIceCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">err</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 常见：在 setRemoteDescription 之前收到 candidate</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 解决：缓存到一个队列，setRemoteDescription 后批量添加</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">warn</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"addIceCandidate 失败，可能需缓存:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> err</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">message</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    pendingRemoteCandidates</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">candidate</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// setRemoteDescription 后，处理缓存的 candidate</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">flushPendingCandidates</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> candidates</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> c </span><span class="token keyword" style="color:#00009f">of</span><span class="token plain"> candidates</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addIceCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  candidates</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">length</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="45-非-trickle-模式等待-gathering-完成">4.5 非 Trickle 模式：等待 Gathering 完成<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#45-%E9%9D%9E-trickle-%E6%A8%A1%E5%BC%8F%E7%AD%89%E5%BE%85-gathering-%E5%AE%8C%E6%88%90" class="hash-link" aria-label="Direct link to 4.5 非 Trickle 模式：等待 Gathering 完成" title="Direct link to 4.5 非 Trickle 模式：等待 Gathering 完成" translate="no">​</a></h3>
<p>Ch2 的 Lab Demo 使用 <strong>非 Trickle</strong> 模式——等所有 Candidate 收集完毕后，一次性打包发送：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 等待 ICE Gathering 完成</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 适用于手工复制 JSON 的学习场景</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">waitIceGatheringComplete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceGatheringState</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"complete"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token known-class-name class-name">Promise</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">resolve</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Promise</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">resolve</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token function-variable function" style="color:#d73a49">check</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceGatheringState</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"complete"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">removeEventListener</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"icegatheringstatechange"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> check</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token function" style="color:#d73a49">resolve</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addEventListener</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"icegatheringstatechange"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> check</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Caller 侧：打包 Offer + 所有 Candidate</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> offer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">waitIceGatheringComplete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> bundle </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">sdp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">candidates</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pendingCandidates</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// → 复制到 Callee 的文本框</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>Trickle vs 非 Trickle</div><div class="admonitionContent_BuS1"><ul>
<li class=""><strong>Trickle ICE</strong>（生产推荐）：每个 Candidate 立即发送，连接更快建立</li>
<li class=""><strong>非 Trickle</strong>（学习用）：等 Gathering 完成后打包，适合手工复制 JSON</li>
</ul><p>Ch3 的 WebSocket 信令服务器实现 Trickle ICE。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五手工信令ch2-lab-的复制粘贴方案">五、手工信令：Ch2 Lab 的复制粘贴方案<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#%E4%BA%94%E6%89%8B%E5%B7%A5%E4%BF%A1%E4%BB%A4ch2-lab-%E7%9A%84%E5%A4%8D%E5%88%B6%E7%B2%98%E8%B4%B4%E6%96%B9%E6%A1%88" class="hash-link" aria-label="Direct link to 五、手工信令：Ch2 Lab 的复制粘贴方案" title="Direct link to 五、手工信令：Ch2 Lab 的复制粘贴方案" translate="no">​</a></h2>
<p>Ch2 不依赖服务器，用 <strong>两个浏览器 Tab + 一个文本框</strong> 模拟信令通道：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-lab-消息格式">5.1 Lab 消息格式<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#51-lab-%E6%B6%88%E6%81%AF%E6%A0%BC%E5%BC%8F" class="hash-link" aria-label="Direct link to 5.1 Lab 消息格式" title="Direct link to 5.1 Lab 消息格式" translate="no">​</a></h3>
<div class="custom-code-block" data-language="json"><div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"sdp"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"offer"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">"sdp"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"v=0\r\no=- 123456789 2 IN IP4 127.0.0.1\r\n..."</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">"candidates"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token property" style="color:#36acaa">"candidate"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"candidate:1 1 udp 2130706431 192.168.1.100 54321 typ host"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token property" style="color:#36acaa">"sdpMid"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"0"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token property" style="color:#36acaa">"sdpMLineIndex"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-lab-操作步骤">5.2 Lab 操作步骤<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#52-lab-%E6%93%8D%E4%BD%9C%E6%AD%A5%E9%AA%A4" class="hash-link" aria-label="Direct link to 5.2 Lab 操作步骤" title="Direct link to 5.2 Lab 操作步骤" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:center">步骤</th><th style="text-align:left">Tab A (Caller)</th><th style="text-align:left">Tab B (Callee)</th></tr></thead><tbody><tr><td style="text-align:center">1</td><td style="text-align:left">选择角色「Caller」</td><td style="text-align:left">选择角色「Callee」</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left">点击「创建 Offer」</td><td style="text-align:left">—</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left">复制文本框 JSON</td><td style="text-align:left">粘贴到文本框</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left">—</td><td style="text-align:left">点击「创建 Answer」</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left">粘贴 Answer JSON</td><td style="text-align:left">复制文本框 JSON</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left">点击「完成连接」</td><td style="text-align:left">—</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left">双方看到远端画面</td><td style="text-align:left">双方看到远端画面</td></tr></tbody></table>
<p>这种手工方式足以理解 Offer/Answer + ICE 的完整流程。<strong><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">Ch3</a></strong> 将同样的消息通过 WebSocket 自动转发，不再需要复制粘贴。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="53-信令消息与-ch3-的对应关系">5.3 信令消息与 Ch3 的对应关系<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#53-%E4%BF%A1%E4%BB%A4%E6%B6%88%E6%81%AF%E4%B8%8E-ch3-%E7%9A%84%E5%AF%B9%E5%BA%94%E5%85%B3%E7%B3%BB" class="hash-link" aria-label="Direct link to 5.3 信令消息与 Ch3 的对应关系" title="Direct link to 5.3 信令消息与 Ch3 的对应关系" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">Ch2 手工操作</th><th style="text-align:left">Ch3 WebSocket 消息</th><th style="text-align:left">载荷</th></tr></thead><tbody><tr><td style="text-align:left">复制 Offer JSON</td><td style="text-align:left"><code>{ type: "offer", sdp }</code></td><td style="text-align:left">SDP + candidates</td></tr><tr><td style="text-align:left">复制 Answer JSON</td><td style="text-align:left"><code>{ type: "answer", sdp }</code></td><td style="text-align:left">SDP + candidates</td></tr><tr><td style="text-align:left">（打包在 bundle 中）</td><td style="text-align:left"><code>{ type: "candidate", candidate }</code></td><td style="text-align:left">单个 ICE Candidate</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六连接状态监控">六、连接状态监控<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#%E5%85%AD%E8%BF%9E%E6%8E%A5%E7%8A%B6%E6%80%81%E7%9B%91%E6%8E%A7" class="hash-link" aria-label="Direct link to 六、连接状态监控" title="Direct link to 六、连接状态监控" translate="no">​</a></h2>
<p>WebRTC 提供了多层状态，从外到内：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="61-ice-连接状态">6.1 ICE 连接状态<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#61-ice-%E8%BF%9E%E6%8E%A5%E7%8A%B6%E6%80%81" class="hash-link" aria-label="Direct link to 6.1 ICE 连接状态" title="Direct link to 6.1 ICE 连接状态" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">oniceconnectionstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> state </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceConnectionState</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ICE:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> state</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">switch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">state</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"checking"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// 正在尝试候选地址配对</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">showUI</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"正在连接…"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"connected"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// 找到可用路径，媒体开始传输</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">showUI</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"已连接"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"completed"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// 所有候选检查完毕（可能切换更优路径）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">showUI</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"连接稳定"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"disconnected"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// 暂时断开，可能自动恢复</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">showUI</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"连接中断，尝试恢复…"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"failed"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// 所有候选都失败，需要 TURN 或重试</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">showUI</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"连接失败"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"closed"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">showUI</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"连接已关闭"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">break</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="62-整体连接状态">6.2 整体连接状态<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#62-%E6%95%B4%E4%BD%93%E8%BF%9E%E6%8E%A5%E7%8A%B6%E6%80%81" class="hash-link" aria-label="Direct link to 6.2 整体连接状态" title="Direct link to 6.2 整体连接状态" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onconnectionstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> state </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">connectionState</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// new → connecting → connected → closed</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 或 new → connecting → failed → closed</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">state </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"connected"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"P2P 连接完全建立（ICE + DTLS）"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">state </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"failed"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"连接失败，建议: 检查 TURN 配置 / 网络 / 防火墙"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// Ch5 将深入排查</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="63-状态监控最佳实践">6.3 状态监控最佳实践<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#63-%E7%8A%B6%E6%80%81%E7%9B%91%E6%8E%A7%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5" class="hash-link" aria-label="Direct link to 6.3 状态监控最佳实践" title="Direct link to 6.3 状态监控最佳实践" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">attachStateMonitor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> onStateChange</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> states </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> </span><span class="token function-variable function" style="color:#d73a49">update</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    states</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">signaling</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">signalingState</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    states</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceGathering</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceGatheringState</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    states</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceConnection</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceConnectionState</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    states</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">connection</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">connectionState</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">onStateChange</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">states</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onsignalingstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> update</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onicegatheringstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> update</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">oniceconnectionstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> update</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">onconnectionstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> update</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">update</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 初始状态</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Lab 中的状态面板</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">attachStateMonitor</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">states</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  stateEl</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">textContent</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">signaling: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">states</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">signaling</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">gathering: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">states</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">iceGathering</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">ice: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">states</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">iceConnection</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">connection: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">states</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">connection</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">join</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"\n"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="64-连接建立后的时间线">6.4 连接建立后的时间线<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#64-%E8%BF%9E%E6%8E%A5%E5%BB%BA%E7%AB%8B%E5%90%8E%E7%9A%84%E6%97%B6%E9%97%B4%E7%BA%BF" class="hash-link" aria-label="Direct link to 6.4 连接建立后的时间线" title="Direct link to 6.4 连接建立后的时间线" translate="no">​</a></h3>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七perfect-negotiation-模式">七、Perfect Negotiation 模式<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#%E4%B8%83perfect-negotiation-%E6%A8%A1%E5%BC%8F" class="hash-link" aria-label="Direct link to 七、Perfect Negotiation 模式" title="Direct link to 七、Perfect Negotiation 模式" translate="no">​</a></h2>
<p>当双方同时调用 <code>createOffer()</code> 时，会产生 <strong>Glare（冲突）</strong>——两个 Offer 互相到达，<code>signalingState</code> 陷入混乱。</p>
<p>W3C 在 <a href="https://www.w3.org/TR/webrtc/#perfect-negotiation" target="_blank" rel="noopener noreferrer" class="">WebRTC 1.0 规范</a> 中定义了 <strong>Perfect Negotiation</strong> 模式，通过角色分工解决冲突：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="71-完整实现">7.1 完整实现<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#71-%E5%AE%8C%E6%95%B4%E5%AE%9E%E7%8E%B0" class="hash-link" aria-label="Direct link to 7.1 完整实现" title="Direct link to 7.1 完整实现" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * Perfect Negotiation 协商管理器</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@param</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">RTCPeerConnection</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment parameter" style="color:#999988;font-style:italic">pc</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"></span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@param</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">boolean</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment parameter" style="color:#999988;font-style:italic">isPolite</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> - 是否为 polite 角色</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@param</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">Function</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment parameter" style="color:#999988;font-style:italic">signal</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> - 发送信令消息的函数</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">setupPerfectNegotiation</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> isPolite</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> signal</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> makingOffer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> ignoreOffer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">let</span><span class="token plain"> isSettingRemoteAnswerPending </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 当需要重新协商时触发（如 addTrack、removeTrack）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onnegotiationneeded</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">try</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      makingOffer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">signal</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">sdp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">err</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"negotiationneeded 失败:"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> err</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">finally</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      makingOffer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 处理收到的信令消息</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">handleSignalMessage</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">msg</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> readyForOffer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">makingOffer </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">signalingState</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"stable"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> isSettingRemoteAnswerPending</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> offerCollision </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"offer"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">readyForOffer</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    ignoreOffer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token plain">isPolite </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> offerCollision</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">ignoreOffer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"Impolite peer 忽略冲突 Offer"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    isSettingRemoteAnswerPending </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"answer"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// Polite peer 遇到冲突时需要 rollback</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offerCollision </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> isPolite</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"rollback"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setRemoteDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">type </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"offer"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createAnswer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token function" style="color:#d73a49">signal</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">sdp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    isSettingRemoteAnswerPending </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> handleSignalMessage </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="72-角色分配策略">7.2 角色分配策略<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#72-%E8%A7%92%E8%89%B2%E5%88%86%E9%85%8D%E7%AD%96%E7%95%A5" class="hash-link" aria-label="Direct link to 7.2 角色分配策略" title="Direct link to 7.2 角色分配策略" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">场景</th><th style="text-align:left">Polite</th><th style="text-align:left">Impolite</th></tr></thead><tbody><tr><td style="text-align:left">1v1 通话</td><td style="text-align:left">Callee（后加入方）</td><td style="text-align:left">Caller（先发起方）</td></tr><tr><td style="text-align:left">多人会议</td><td style="text-align:left">后加入 Room 的 peer</td><td style="text-align:left">先发布媒体的 peer</td></tr><tr><td style="text-align:left">通用规则</td><td style="text-align:left"><code>peerId</code> 较大者</td><td style="text-align:left"><code>peerId</code> 较小者</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>Ch2 不需要 Perfect Negotiation</div><div class="admonitionContent_BuS1"><p>1v1 通话中 Caller 和 Callee 角色明确，不会同时发 Offer。Perfect Negotiation 在 Ch3 多人场景和 <code>onnegotiationneeded</code> 自动重协商时才必需。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八资源清理与错误恢复">八、资源清理与错误恢复<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#%E5%85%AB%E8%B5%84%E6%BA%90%E6%B8%85%E7%90%86%E4%B8%8E%E9%94%99%E8%AF%AF%E6%81%A2%E5%A4%8D" class="hash-link" aria-label="Direct link to 八、资源清理与错误恢复" title="Direct link to 八、资源清理与错误恢复" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="81-正确关闭-peerconnection">8.1 正确关闭 PeerConnection<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#81-%E6%AD%A3%E7%A1%AE%E5%85%B3%E9%97%AD-peerconnection" class="hash-link" aria-label="Direct link to 8.1 正确关闭 PeerConnection" title="Direct link to 8.1 正确关闭 PeerConnection" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 完整清理 — 挂断通话时调用</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">hangup</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> localStream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 1. 关闭 PeerConnection（触发 closed 状态）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token operator" style="color:#393A34">?.</span><span class="token method function property-access" style="color:#d73a49">close</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 2. 停止所有本地轨道（释放摄像头/麦克风）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  localStream</span><span class="token operator" style="color:#393A34">?.</span><span class="token method function property-access" style="color:#d73a49">getTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">track</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stop</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 3. 清空视频元素</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  localVideo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">srcObject</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  remoteVideo</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">srcObject</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 页面关闭时自动清理</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token dom variable" style="color:#36acaa">window</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addEventListener</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"beforeunload"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">hangup</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> localStream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="82-连接失败重试与-ice-restart">8.2 连接失败重试与 ICE Restart<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#82-%E8%BF%9E%E6%8E%A5%E5%A4%B1%E8%B4%A5%E9%87%8D%E8%AF%95%E4%B8%8E-ice-restart" class="hash-link" aria-label="Direct link to 8.2 连接失败重试与 ICE Restart" title="Direct link to 8.2 连接失败重试与 ICE Restart" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">oniceconnectionstatechange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">iceConnectionState</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"failed"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"ICE 失败，尝试 ICE Restart"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">restartIce</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">restartIce</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> offer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">iceRestart</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 通过信令发送新 Offer — 对端 setRemoteDescription 后重新添加 candidate</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">signal</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">type</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"offer"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">sdp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<p>ICE Restart 会重新收集候选地址，但 <strong>保留 DTLS 会话</strong>（不断开加密通道）。适用于网络切换（WiFi → 4G）场景。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九常见问题与踩坑指南">九、常见问题与踩坑指南<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#%E4%B9%9D%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E4%B8%8E%E8%B8%A9%E5%9D%91%E6%8C%87%E5%8D%97" class="hash-link" aria-label="Direct link to 九、常见问题与踩坑指南" title="Direct link to 九、常见问题与踩坑指南" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q1-setremotedescription-报错-invalidstateerror">Q1: setRemoteDescription 报错 "InvalidStateError"<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#q1-setremotedescription-%E6%8A%A5%E9%94%99-invalidstateerror" class="hash-link" aria-label="Direct link to Q1: setRemoteDescription 报错 &quot;InvalidStateError&quot;" title="Direct link to Q1: setRemoteDescription 报错 &quot;InvalidStateError&quot;" translate="no">​</a></h3>
<p><strong>A</strong>: <code>signalingState</code> 不匹配。检查是否在 <code>have-local-offer</code> 时收到了第二个 Offer（glare）。使用 Perfect Negotiation 或确保只有 Caller 发 Offer。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q2-能看到本地画面但看不到远端画面">Q2: 能看到本地画面但看不到远端画面<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#q2-%E8%83%BD%E7%9C%8B%E5%88%B0%E6%9C%AC%E5%9C%B0%E7%94%BB%E9%9D%A2%E4%BD%86%E7%9C%8B%E4%B8%8D%E5%88%B0%E8%BF%9C%E7%AB%AF%E7%94%BB%E9%9D%A2" class="hash-link" aria-label="Direct link to Q2: 能看到本地画面但看不到远端画面" title="Direct link to Q2: 能看到本地画面但看不到远端画面" translate="no">​</a></h3>
<p><strong>A</strong>: 三个排查点：</p>
<ol>
<li class="">对端是否成功 <code>addTrack</code> 并在 <code>setLocalDescription</code> 之前完成</li>
<li class=""><code>ontrack</code> 回调是否正确设置 <code>remoteVideo.srcObject</code></li>
<li class="">ICE 是否连通——检查 <code>iceConnectionState</code> 是否为 <code>connected</code></li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q3-addicecandidate-报错-error-processing-ice-candidate">Q3: addIceCandidate 报错 "Error processing ICE candidate"<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#q3-addicecandidate-%E6%8A%A5%E9%94%99-error-processing-ice-candidate" class="hash-link" aria-label="Direct link to Q3: addIceCandidate 报错 &quot;Error processing ICE candidate&quot;" title="Direct link to Q3: addIceCandidate 报错 &quot;Error processing ICE candidate&quot;" translate="no">​</a></h3>
<p><strong>A</strong>: 常见原因：</p>
<ul>
<li class="">在 <code>setRemoteDescription</code> <strong>之前</strong>收到 Candidate → 缓存后批量添加</li>
<li class="">Candidate 格式被 JSON 序列化破坏 → 使用 <code>candidate.toJSON()</code> 发送，原样还原</li>
<li class="">连接已 <code>closed</code> → 忽略迟到的 Candidate</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q4-同一台机器两个-tab-能通不同机器不通">Q4: 同一台机器两个 Tab 能通，不同机器不通<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#q4-%E5%90%8C%E4%B8%80%E5%8F%B0%E6%9C%BA%E5%99%A8%E4%B8%A4%E4%B8%AA-tab-%E8%83%BD%E9%80%9A%E4%B8%8D%E5%90%8C%E6%9C%BA%E5%99%A8%E4%B8%8D%E9%80%9A" class="hash-link" aria-label="Direct link to Q4: 同一台机器两个 Tab 能通，不同机器不通" title="Direct link to Q4: 同一台机器两个 Tab 能通，不同机器不通" translate="no">​</a></h3>
<p><strong>A</strong>: 同一台机器时 ICE 使用 host candidate（本机 IP）直连。不同机器需要 STUN 发现公网地址，如果双方都在对称型 NAT 后面，host + srflx 都不够，需要 TURN 中继（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">Ch5</a>）。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q5-createoffer-和-addtrack-的顺序重要吗">Q5: createOffer 和 addTrack 的顺序重要吗？<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#q5-createoffer-%E5%92%8C-addtrack-%E7%9A%84%E9%A1%BA%E5%BA%8F%E9%87%8D%E8%A6%81%E5%90%97" class="hash-link" aria-label="Direct link to Q5: createOffer 和 addTrack 的顺序重要吗？" title="Direct link to Q5: createOffer 和 addTrack 的顺序重要吗？" translate="no">​</a></h3>
<p><strong>A</strong>: 必须先 <code>addTrack</code> 再 <code>createOffer</code>，否则 Offer 中不包含媒体描述，对端无法协商编解码器。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q6-为什么需要-stun-服务器">Q6: 为什么需要 STUN 服务器？<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#q6-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-stun-%E6%9C%8D%E5%8A%A1%E5%99%A8" class="hash-link" aria-label="Direct link to Q6: 为什么需要 STUN 服务器？" title="Direct link to Q6: 为什么需要 STUN 服务器？" translate="no">​</a></h3>
<p><strong>A</strong>: STUN 让浏览器发现自己的公网 IP<!-- -->:Port<!-- -->（Server Reflexive Candidate）。没有 STUN，跨 NAT 时双方只有私有地址，无法直连。STUN 是免费的；TURN 是付费中继（Ch5 / Ch14）。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q7-onnegotiationneeded-什么时候触发">Q7: onnegotiationneeded 什么时候触发？<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#q7-onnegotiationneeded-%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E8%A7%A6%E5%8F%91" class="hash-link" aria-label="Direct link to Q7: onnegotiationneeded 什么时候触发？" title="Direct link to Q7: onnegotiationneeded 什么时候触发？" translate="no">​</a></h3>
<p><strong>A</strong>: 添加/移除 Track、修改 transceiver 方向、ICE Restart 时。1v1 首次连接不会触发（因为手动 createOffer），但添加屏幕共享（Ch1 §5.3）时会触发。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q8-localdescription-和-currentlocaldescription-有什么区别">Q8: localDescription 和 currentLocalDescription 有什么区别？<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#q8-localdescription-%E5%92%8C-currentlocaldescription-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB" class="hash-link" aria-label="Direct link to Q8: localDescription 和 currentLocalDescription 有什么区别？" title="Direct link to Q8: localDescription 和 currentLocalDescription 有什么区别？" translate="no">​</a></h3>
<p><strong>A</strong>: <code>localDescription</code> 是最后一次成功 <code>setLocalDescription</code> 的值。<code>currentLocalDescription</code> 是浏览器当前实际使用的描述（rollback 后会不同）。通常使用 <code>localDescription</code> 即可。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q9-如何确认-dtls-握手成功">Q9: 如何确认 DTLS 握手成功？<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#q9-%E5%A6%82%E4%BD%95%E7%A1%AE%E8%AE%A4-dtls-%E6%8F%A1%E6%89%8B%E6%88%90%E5%8A%9F" class="hash-link" aria-label="Direct link to Q9: 如何确认 DTLS 握手成功？" title="Direct link to Q9: 如何确认 DTLS 握手成功？" translate="no">​</a></h3>
<p><strong>A</strong>: 当 <code>connectionState</code> 变为 <code>connected</code> 时，ICE 和 DTLS 都已成功。也可在 <code>chrome://webrtc-internals</code> 查看 DTLS 状态和 SRTP 密钥信息。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十实战-lab">十、实战 Lab<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#%E5%8D%81%E5%AE%9E%E6%88%98-lab" class="hash-link" aria-label="Direct link to 十、实战 Lab" title="Direct link to 十、实战 Lab" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="101-运行-ch02-demo">10.1 运行 Ch02 Demo<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#101-%E8%BF%90%E8%A1%8C-ch02-demo" class="hash-link" aria-label="Direct link to 10.1 运行 Ch02 Demo" title="Direct link to 10.1 运行 Ch02 Demo" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token builtin class-name">cd</span><span class="token plain"> examples/webrtc-lab/client/ch02-p2p-basic</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">npx serve </span><span class="token builtin class-name">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 打开 http://localhost:3000</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="102-练习清单">10.2 练习清单<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#102-%E7%BB%83%E4%B9%A0%E6%B8%85%E5%8D%95" class="hash-link" aria-label="Direct link to 10.2 练习清单" title="Direct link to 10.2 练习清单" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:center">#</th><th style="text-align:left">练习</th><th style="text-align:left">预期观察</th></tr></thead><tbody><tr><td style="text-align:center">1</td><td style="text-align:left">两个 Tab 分别选 Caller / Callee</td><td style="text-align:left">状态面板显示角色</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left">Caller 点击「创建 Offer」</td><td style="text-align:left">文本框出现 JSON bundle；<code>signalingState</code> = <code>have-local-offer</code></td></tr><tr><td style="text-align:center">3</td><td style="text-align:left">复制 JSON 到 Callee，点击「创建 Answer」</td><td style="text-align:left">Callee 出现 Answer JSON；<code>signalingState</code> = <code>stable</code></td></tr><tr><td style="text-align:center">4</td><td style="text-align:left">复制 Answer 回 Caller，点击「完成连接」</td><td style="text-align:left">双方 <code>iceConnectionState</code> → <code>connected</code></td></tr><tr><td style="text-align:center">5</td><td style="text-align:left">双方看到远端视频画面</td><td style="text-align:left"><code>ontrack</code> 触发，<code>remoteVideo</code> 有画面</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left">打开 <code>chrome://webrtc-internals</code></td><td style="text-align:left">观察 ICE pair 选中过程、DTLS 握手、SRTP 统计</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="103-扩展挑战">10.3 扩展挑战<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#103-%E6%89%A9%E5%B1%95%E6%8C%91%E6%88%98" class="hash-link" aria-label="Direct link to 10.3 扩展挑战" title="Direct link to 10.3 扩展挑战" translate="no">​</a></h3>
<ol>
<li class=""><strong>Trickle ICE</strong>：改造 Demo，每个 <code>onicecandidate</code> 立即显示，不等 Gathering 完成</li>
<li class=""><strong>ICE 状态面板</strong>：实时展示四维状态（signaling / gathering / ice / connection）</li>
<li class=""><strong>自动重连</strong>：<code>iceConnectionState === "failed"</code> 时自动 <code>restartIce</code></li>
<li class=""><strong>准备 Ch3</strong>：将 <code>sendSignal</code> 抽象为接口，为 WebSocket 接入做准备</li>
<li class=""><strong>跨网络测试</strong>：两台不同设备通过 STUN 连接，观察 candidate 类型变化</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="104-demo-核心代码导读">10.4 Demo 核心代码导读<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#104-demo-%E6%A0%B8%E5%BF%83%E4%BB%A3%E7%A0%81%E5%AF%BC%E8%AF%BB" class="hash-link" aria-label="Direct link to 10.4 Demo 核心代码导读" title="Direct link to 10.4 Demo 核心代码导读" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// examples/webrtc-lab/client/ch02-p2p-basic/main.js（节选）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 非 Trickle：等 ICE Gathering 完成后打包</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token dom variable" style="color:#36acaa">document</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getElementById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"startBtn"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onclick</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">createPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> offer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createOffer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">offer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">waitIceGatheringComplete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 关键：等待所有 candidate</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  sdpBox</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">value</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stringify</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">sdp</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">localDescription</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">candidates</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> pendingCandidates</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 一次性打包</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword null nil" style="color:#00009f">null</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Callee 侧：解析 bundle，设置远端描述 + 添加 candidates</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token dom variable" style="color:#36acaa">document</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getElementById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"answerBtn"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onclick</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> msg </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">parse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sdpBox</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">value</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">createPeerConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setRemoteDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> c </span><span class="token keyword" style="color:#00009f">of</span><span class="token plain"> msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidates</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addIceCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> answer </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">createAnswer</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setLocalDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">answer</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">waitIceGatheringComplete</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// ... 输出 Answer bundle</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// Caller 侧：收到 Answer 后完成连接</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token dom variable" style="color:#36acaa">document</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getElementById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"connectBtn"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onclick</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> msg </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">JSON</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">parse</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sdpBox</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">value</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setRemoteDescription</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">sdp</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> c </span><span class="token keyword" style="color:#00009f">of</span><span class="token plain"> msg</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">candidates</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addIceCandidate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十一从手工信令到-websocketch3-预告">十一、从手工信令到 WebSocket：Ch3 预告<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#%E5%8D%81%E4%B8%80%E4%BB%8E%E6%89%8B%E5%B7%A5%E4%BF%A1%E4%BB%A4%E5%88%B0-websocketch3-%E9%A2%84%E5%91%8A" class="hash-link" aria-label="Direct link to 十一、从手工信令到 WebSocket：Ch3 预告" title="Direct link to 十一、从手工信令到 WebSocket：Ch3 预告" translate="no">​</a></h2>
<p>本章用复制粘贴完成了信令交换的全流程。生产中不可能让用户手动复制 JSON——你需要一个 <strong>信令服务器</strong>。</p>
<!-- -->
<p><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">Ch3</a> 将实现：</p>
<ul>
<li class="">WebSocket 信令服务器（<a href="https://github.com/rainlib/idea_visual/tree/main/examples/webrtc-lab/signaling" target="_blank" rel="noopener noreferrer" class=""><code>examples/webrtc-lab/signaling/</code></a>）</li>
<li class="">Room 加入/离开与 peer 管理</li>
<li class="">Trickle ICE 实时转发</li>
<li class="">信令消息协议与安全考量</li>
</ul>
<p>信令服务器 <strong>永远看不到 SRTP 媒体内容</strong>——它只传递 SDP 文本和 ICE Candidate JSON。媒体带宽不经过信令服务器。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十二本章小结">十二、本章小结<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#%E5%8D%81%E4%BA%8C%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="Direct link to 十二、本章小结" title="Direct link to 十二、本章小结" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">要点</th><th style="text-align:left">内容</th></tr></thead><tbody><tr><td style="text-align:left">核心 API</td><td style="text-align:left"><code>RTCPeerConnection</code> + <code>addTrack</code> + <code>ontrack</code></td></tr><tr><td style="text-align:left">协商模型</td><td style="text-align:left">JSEP Offer/Answer：<code>createOffer</code> → <code>setLocalDescription</code> → 信令 → <code>setRemoteDescription</code></td></tr><tr><td style="text-align:left">ICE</td><td style="text-align:left"><code>onicecandidate</code> 产出 → 信令 → <code>addIceCandidate</code> 消费</td></tr><tr><td style="text-align:left">状态监控</td><td style="text-align:left"><code>signalingState</code> / <code>iceGatheringState</code> / <code>iceConnectionState</code> / <code>connectionState</code> 四层</td></tr><tr><td style="text-align:left">冲突处理</td><td style="text-align:left">Perfect Negotiation：polite 让出，impolite 忽略</td></tr><tr><td style="text-align:left">信令</td><td style="text-align:left">WebRTC 标准外，必须自行实现——<a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 历史</a> 解释了原因</td></tr><tr><td style="text-align:left">下一步</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">Ch3 WebSocket 信令服务器</a> 替代手工复制</td></tr></tbody></table>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8829" target="_blank" rel="noopener noreferrer" class="">RFC 8829 — JSEP</a></li>
<li class=""><a href="https://www.w3.org/TR/webrtc/" target="_blank" rel="noopener noreferrer" class="">W3C WebRTC 1.0</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection" target="_blank" rel="noopener noreferrer" class="">MDN — RTCPeerConnection</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation" target="_blank" rel="noopener noreferrer" class="">MDN — Perfect Negotiation</a></li>
<li class=""><a href="https://webrtc.github.io/samples/src/content/peerconnection/pc1/" target="_blank" rel="noopener noreferrer" class="">WebRTC Samples — PeerConnection</a></li>
<li class=""><a href="https://webrtcforthecurious.com/docs/03-connecting/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — Connecting</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史</a></li>
<li class=""><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">Ch1 浏览器媒体 API</a></li>
<li class=""><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">Ch3 信令服务器设计</a></li>
<li class=""><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">Ch4 SDP 解剖</a></li>
<li class=""><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">Ch5 ICE/STUN/TURN</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>实时通信</category>
            <category>教程</category>
            <category>Deep Dive</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (1)：浏览器媒体 API 与设备管理]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-browser-api</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-browser-api</guid>
            <pubDate>Fri, 12 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[从 getUserMedia 到 MediaStreamTrack 生命周期：Constraints 语义、设备枚举与热切换、屏幕共享、权限模型、adapter.js 与 devicechange 事件，掌握 WebRTC 媒体采集的完整第一站]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"在建立任何 P2P 连接之前，你得先拿到媒体。"</p>
</blockquote>
<p>上一篇 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">Ch0 架构全景</a> 我们画了协议栈地图。本章从栈顶最直观的入口开始：<strong>如何把摄像头、麦克风和屏幕变成浏览器里的 <code>MediaStream</code></strong>。</p>
<p>Serge Lachapelle 在 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史</a> 中回忆：Gmail 语音视频聊天的前身需要分别授权 GIPS 音频、Vidyo 视频、libjingle 网络三个子系统，"每个子系统都有完全不同的 API"。WebRTC 标准化工作的核心目标之一，就是把 <strong>媒体采集</strong> 这一层统一成开发者今天使用的 <code>navigator.mediaDevices</code> API——让你不必再为每个浏览器插件写一套集成代码。</p>
<p>配套代码：<a href="https://github.com/rainlib/idea_visual/tree/main/examples/webrtc-lab/client/ch01-media-devices" target="_blank" rel="noopener noreferrer" class=""><code>examples/webrtc-lab/client/ch01-media-devices/</code></a></p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表">本篇术语表<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8" class="hash-link" aria-label="Direct link to 本篇术语表" title="Direct link to 本篇术语表" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>媒体采集</strong></td><td style="text-align:left">Media Capture</td><td style="text-align:left">通过浏览器 API 从摄像头、麦克风或屏幕获取原始音视频数据的过程</td></tr><tr><td style="text-align:left"><strong>媒体流</strong></td><td style="text-align:left">MediaStream</td><td style="text-align:left">一个或多个 <code>MediaStreamTrack</code> 的容器，是 <code>getUserMedia</code> / <code>getDisplayMedia</code> 的返回值</td></tr><tr><td style="text-align:left"><strong>媒体轨道</strong></td><td style="text-align:left">MediaStreamTrack</td><td style="text-align:left">单路音频或视频数据通道，拥有独立的生命周期与 <code>readyState</code></td></tr><tr><td style="text-align:left"><strong>约束</strong></td><td style="text-align:left">Constraints</td><td style="text-align:left">向浏览器声明期望分辨率、帧率、设备 ID 等参数的键值对象</td></tr><tr><td style="text-align:left"><strong>设备枚举</strong></td><td style="text-align:left">enumerateDevices</td><td style="text-align:left">列出当前系统可用的输入/输出设备，返回 <code>deviceId</code>、<code>kind</code>、<code>label</code></td></tr><tr><td style="text-align:left"><strong>安全上下文</strong></td><td style="text-align:left">Secure Context</td><td style="text-align:left">HTTPS 或 <code>localhost</code> 环境；非安全上下文下 <code>getUserMedia</code> 会被拒绝</td></tr><tr><td style="text-align:left"><strong>权限提示</strong></td><td style="text-align:left">Permission Prompt</td><td style="text-align:left">浏览器弹出的摄像头/麦克风授权对话框，必须由用户手势触发</td></tr><tr><td style="text-align:left"><strong>轨道替换</strong></td><td style="text-align:left">replaceTrack</td><td style="text-align:left"><code>RTCRtpSender.replaceTrack()</code> 在不停 PeerConnection 的情况下切换发送轨道</td></tr><tr><td style="text-align:left"><strong>屏幕共享</strong></td><td style="text-align:left">Display Capture</td><td style="text-align:left"><code>getDisplayMedia()</code> 采集屏幕、窗口或浏览器 Tab 的视频（及可选系统音频）</td></tr><tr><td style="text-align:left"><strong>设备变更</strong></td><td style="text-align:left">devicechange</td><td style="text-align:left"><code>navigator.mediaDevices</code> 上的事件，USB 设备插拔时触发，需重新 <code>enumerateDevices</code></td></tr><tr><td style="text-align:left"><strong>适配层</strong></td><td style="text-align:left">adapter.js</td><td style="text-align:left">WebRTC 官方维护的跨浏览器 shim，抹平 API 前缀与行为差异</td></tr><tr><td style="text-align:left"><strong>采集设置</strong></td><td style="text-align:left">getSettings()</td><td style="text-align:left">轨道实际生效的参数（分辨率、帧率、deviceId），可能与 Constraints 不同</td></tr><tr><td style="text-align:left"><strong>能力查询</strong></td><td style="text-align:left">getCapabilities()</td><td style="text-align:left">设备/轨道支持的参数范围，用于构建合理的 Constraints</td></tr><tr><td style="text-align:left"><strong>权限策略</strong></td><td style="text-align:left">Permissions-Policy</td><td style="text-align:left">HTTP 响应头，控制 iframe 和页面是否允许调用摄像头/麦克风</td></tr><tr><td style="text-align:left"><strong>内容提示</strong></td><td style="text-align:left">Content Hint</td><td style="text-align:left">通过 <code>track.contentHint</code> 告知编码器内容类型（运动/细节/文本），影响码率分配</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一media-capture-api-在协议栈中的位置">一、Media Capture API 在协议栈中的位置<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#%E4%B8%80media-capture-api-%E5%9C%A8%E5%8D%8F%E8%AE%AE%E6%A0%88%E4%B8%AD%E7%9A%84%E4%BD%8D%E7%BD%AE" class="hash-link" aria-label="Direct link to 一、Media Capture API 在协议栈中的位置" title="Direct link to 一、Media Capture API 在协议栈中的位置" translate="no">​</a></h2>
<p>WebRTC 媒体采集由 W3C <a href="https://www.w3.org/TR/mediacapture-streams/" target="_blank" rel="noopener noreferrer" class="">Media Capture and Streams</a> 规范定义。它位于整个 WebRTC 栈的最顶端——<strong>在 SDP 协商、ICE 穿透、DTLS 握手之前</strong>，你必须先拿到 <code>MediaStream</code>。</p>
<!-- -->
<table><thead><tr><th style="text-align:left">API</th><th style="text-align:left">规范入口</th><th style="text-align:left">用途</th></tr></thead><tbody><tr><td style="text-align:left"><code>navigator.mediaDevices.getUserMedia()</code></td><td style="text-align:left">Media Capture §5.1</td><td style="text-align:left">摄像头 + 麦克风</td></tr><tr><td style="text-align:left"><code>navigator.mediaDevices.getDisplayMedia()</code></td><td style="text-align:left">Screen Capture §3</td><td style="text-align:left">屏幕 / 窗口 / Tab 共享</td></tr><tr><td style="text-align:left"><code>navigator.mediaDevices.enumerateDevices()</code></td><td style="text-align:left">Media Capture §4.3</td><td style="text-align:left">枚举设备列表</td></tr><tr><td style="text-align:left"><code>navigator.mediaDevices.getSupportedConstraints()</code></td><td style="text-align:left">Media Capture §4.4</td><td style="text-align:left">查询当前浏览器支持的约束键</td></tr><tr><td style="text-align:left"><code>MediaStream</code></td><td style="text-align:left">Media Capture §2.1</td><td style="text-align:left">媒体流容器</td></tr><tr><td style="text-align:left"><code>MediaStreamTrack</code></td><td style="text-align:left">Media Capture §2.3</td><td style="text-align:left">单路音频或视频轨道</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="11-从三个子系统到一个-api">1.1 从三个子系统到一个 API<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#11-%E4%BB%8E%E4%B8%89%E4%B8%AA%E5%AD%90%E7%B3%BB%E7%BB%9F%E5%88%B0%E4%B8%80%E4%B8%AA-api" class="hash-link" aria-label="Direct link to 1.1 从三个子系统到一个 API" title="Direct link to 1.1 从三个子系统到一个 API" translate="no">​</a></h3>
<p><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 历史</a> 中 Serge 描述的 Gmail 视频聊天架构，是理解今天 API 设计的关键背景：</p>
<!-- -->
<p>今天你只需要 <code>navigator.mediaDevices</code>，但理解这段历史有助于解释为何某些浏览器行为仍有差异——底层仍是对接各 OS 的原生媒体框架（Windows Media Foundation、AVFoundation、V4L2 等）。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="12-feature-detection-与-api-可用性">1.2 Feature Detection 与 API 可用性<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#12-feature-detection-%E4%B8%8E-api-%E5%8F%AF%E7%94%A8%E6%80%A7" class="hash-link" aria-label="Direct link to 1.2 Feature Detection 与 API 可用性" title="Direct link to 1.2 Feature Detection 与 API 可用性" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 检测 Media Capture API 是否可用</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 建议在应用启动时调用一次，提前给用户友好提示</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">checkMediaSupport</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> result </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">secureContext</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">window</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">isSecureContext</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">mediaDevices</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token operator" style="color:#393A34">!</span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">getUserMedia</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token operator" style="color:#393A34">!</span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">getUserMedia</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">getDisplayMedia</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token operator" style="color:#393A34">!</span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">getDisplayMedia</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">enumerateDevices</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!</span><span class="token operator" style="color:#393A34">!</span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">enumerateDevices</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">supportedConstraints</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">getSupportedConstraints</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> keys </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getSupportedConstraints</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> key </span><span class="token keyword" style="color:#00009f">of</span><span class="token plain"> keys</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">supportedConstraints</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">key</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">secureContext</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"非安全上下文：请使用 HTTPS 或 localhost"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> result</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 示例输出（Chrome）：</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// supportedConstraints: { width, height, frameRate, facingMode,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">//   echoCancellation, noiseSuppression, deviceId, ... }</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>HTTPS 要求</div><div class="admonitionContent_BuS1"><p>除 <code>localhost</code> 外，<code>getUserMedia</code> 必须在 <strong>安全上下文（Secure Context）</strong> 下调用。<code>http://192.168.x.x</code> 等局域网 HTTP 地址同样会被拒绝——开发时可用 <code>npx serve</code> + <code>localhost</code>，生产必须 HTTPS。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二getusermedia-完整用法">二、getUserMedia 完整用法<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#%E4%BA%8Cgetusermedia-%E5%AE%8C%E6%95%B4%E7%94%A8%E6%B3%95" class="hash-link" aria-label="Direct link to 二、getUserMedia 完整用法" title="Direct link to 二、getUserMedia 完整用法" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-最小示例与错误处理">2.1 最小示例与错误处理<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#21-%E6%9C%80%E5%B0%8F%E7%A4%BA%E4%BE%8B%E4%B8%8E%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86" class="hash-link" aria-label="Direct link to 2.1 最小示例与错误处理" title="Direct link to 2.1 最小示例与错误处理" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 安全的 getUserMedia 封装</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * - 检查 API 是否存在</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * - 区分权限拒绝 vs 设备不存在 vs 约束无法满足</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">acquireUserMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">constraints</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">getUserMedia</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"当前浏览器不支持 getUserMedia，请使用 Chrome/Firefox/Safari 最新版"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">try</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stream </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getUserMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">constraints</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> stream</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">err</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// DOMException 名称是排障的第一线索</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">switch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">err</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">name</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"NotAllowedError"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 用户点击了「拒绝」，或页面未获用户手势</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"用户拒绝了摄像头/麦克风权限，或页面未在用户交互后调用"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"NotFoundError"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 没有摄像头/麦克风硬件，或 deviceId 无效</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"未找到匹配的音视频设备"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"NotReadableError"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 设备被其他应用独占（如 Zoom 占用了摄像头）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"设备被占用或驱动异常，请关闭其他占用摄像头的应用"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"OverconstrainedError"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// exact 约束无法满足</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">约束无法满足: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">err</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">constraint</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"SecurityError"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"非安全上下文，请使用 HTTPS 或 localhost"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">case</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"AbortError"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token comment" style="color:#999988;font-style:italic">// 硬件错误或系统中断</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">new</span><span class="token plain"> </span><span class="token class-name">Error</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"采集被系统中止，请重试"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword module" style="color:#00009f">default</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> err</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 典型调用：宽松 ideal 约束，生产环境首选</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stream </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">acquireUserMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">width</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1280</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">height</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">720</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">frameRate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">30</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">max</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">60</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">facingMode</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"user"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 移动端前置摄像头</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">audio</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">echoCancellation</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">noiseSuppression</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">autoGainControl</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 绑定到 &lt;video&gt; 元素预览</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> preview </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">document</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getElementById</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"preview"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">preview</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">srcObject</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> stream</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">preview</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">muted</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 本地预览必须静音，否则回声</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">preview</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">playsInline</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// iOS Safari 内联播放</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> preview</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">play</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="22-constraints-语义深度解析">2.2 Constraints 语义深度解析<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#22-constraints-%E8%AF%AD%E4%B9%89%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90" class="hash-link" aria-label="Direct link to 2.2 Constraints 语义深度解析" title="Direct link to 2.2 Constraints 语义深度解析" translate="no">​</a></h3>
<p>Constraints 是 <code>getUserMedia</code> 最核心的参数机制。理解四种关键字是避免 <code>OverconstrainedError</code> 的关键：</p>
<table><thead><tr><th style="text-align:left">关键字</th><th style="text-align:left">语义</th><th style="text-align:left">不满足时</th></tr></thead><tbody><tr><td style="text-align:left"><code>exact</code></td><td style="text-align:left">必须精确匹配</td><td style="text-align:left">抛出 <code>OverconstrainedError</code>，调用失败</td></tr><tr><td style="text-align:left"><code>ideal</code></td><td style="text-align:left">尽量满足，可降级</td><td style="text-align:left">浏览器选最接近的值，<strong>不失败</strong></td></tr><tr><td style="text-align:left"><code>min</code></td><td style="text-align:left">下限</td><td style="text-align:left">实际值 ≥ min，否则失败</td></tr><tr><td style="text-align:left"><code>max</code></td><td style="text-align:left">上限</td><td style="text-align:left">实际值 ≤ max，否则失败</td></tr></tbody></table>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// ❌ 危险：exact 在生产环境极易失败</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> risky </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getUserMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">width</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">exact</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1920</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">height</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">exact</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1080</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// ✅ 推荐：ideal + 事后确认</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> safe </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">getUserMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">width</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1920</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">max</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1920</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">height</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1080</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">max</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1080</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">frameRate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">30</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">max</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">60</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> videoTrack </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> safe</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getVideoTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> settings </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> videoTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getSettings</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">实际分辨率: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">settings</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">width</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">x</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">settings</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">height</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c"> @ </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">settings</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">frameRate</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">fps</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">设备 ID: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">settings</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">deviceId</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<p><strong><code>getCapabilities()</code> vs <code>getSettings()</code></strong>：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> track </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> stream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getVideoTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> caps </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getCapabilities</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">// 硬件支持的范围</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> settings </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getSettings</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">// 当前生效的值</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// caps.width = { min: 320, max: 3840 }  — 摄像头物理能力</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// settings.width = 1280                    — 本次采集实际值</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 用 caps 构建合理的 ideal 约束</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">buildVideoConstraints</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">track</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> caps </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getCapabilities</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> targetWidth </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">Math</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">min</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">1280</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> caps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">width</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">max </span><span class="token operator" style="color:#393A34">??</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1280</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> targetHeight </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token known-class-name class-name">Math</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">min</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#36acaa">720</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> caps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">height</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">max </span><span class="token operator" style="color:#393A34">??</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">720</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">width</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> targetWidth </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">height</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> targetHeight </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">frameRate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">30</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">max</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> caps</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">frameRate</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">max </span><span class="token operator" style="color:#393A34">??</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">30</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<p>生产建议流程：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="23-音频-constraints-详解">2.3 音频 Constraints 详解<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#23-%E9%9F%B3%E9%A2%91-constraints-%E8%AF%A6%E8%A7%A3" class="hash-link" aria-label="Direct link to 2.3 音频 Constraints 详解" title="Direct link to 2.3 音频 Constraints 详解" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stream </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getUserMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">audio</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 回声消除 — 会议场景必须开启</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">echoCancellation</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 噪声抑制</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">noiseSuppression</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 自动增益 — 音量过小时自动放大</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">autoGainControl</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 指定麦克风（需先 enumerateDevices 拿到 deviceId）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">deviceId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> selectedMicId </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 采样率（部分浏览器支持）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">sampleRate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">48000</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 声道数</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">channelCount</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 延迟模式 — "interactive" 低延迟，"speech-recognition" 优化识别</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">latency</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0.01</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 仅采集音频</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<table><thead><tr><th style="text-align:left">音频约束</th><th style="text-align:left">推荐值</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left"><code>echoCancellation</code></td><td style="text-align:left"><code>true</code></td><td style="text-align:left">消除扬声器回声，1v1 和会议必备</td></tr><tr><td style="text-align:left"><code>noiseSuppression</code></td><td style="text-align:left"><code>true</code></td><td style="text-align:left">抑制环境噪声</td></tr><tr><td style="text-align:left"><code>autoGainControl</code></td><td style="text-align:left"><code>true</code></td><td style="text-align:left">自动调节输入音量</td></tr><tr><td style="text-align:left"><code>sampleRate</code></td><td style="text-align:left"><code>48000</code></td><td style="text-align:left">WebRTC 默认 48kHz，不建议改</td></tr><tr><td style="text-align:left"><code>channelCount</code></td><td style="text-align:left"><code>1</code></td><td style="text-align:left">单声道足够，节省带宽</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="24-视频高级约束">2.4 视频高级约束<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#24-%E8%A7%86%E9%A2%91%E9%AB%98%E7%BA%A7%E7%BA%A6%E6%9D%9F" class="hash-link" aria-label="Direct link to 2.4 视频高级约束" title="Direct link to 2.4 视频高级约束" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stream </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getUserMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">width</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1280</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">height</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">720</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">frameRate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">30</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 对焦模式 — 部分摄像头支持</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">focusMode</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"continuous"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 曝光模式</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">exposureMode</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"continuous"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 白平衡</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">whiteBalanceMode</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"continuous"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 移动端前后摄像头</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">facingMode</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"user"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// "user" | "environment"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 分辨率宽高比</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">aspectRatio</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">16</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">9</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>约束支持因设备而异</div><div class="admonitionContent_BuS1"><p><code>focusMode</code>、<code>exposureMode</code> 等高级约束并非所有摄像头都支持。先用 <code>getSupportedConstraints()</code> 检查浏览器支持，再用 <code>getCapabilities()</code> 检查设备支持。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三enumeratedevices-与设备管理">三、enumerateDevices 与设备管理<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#%E4%B8%89enumeratedevices-%E4%B8%8E%E8%AE%BE%E5%A4%87%E7%AE%A1%E7%90%86" class="hash-link" aria-label="Direct link to 三、enumerateDevices 与设备管理" title="Direct link to 三、enumerateDevices 与设备管理" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-枚举流程与-label-隐私">3.1 枚举流程与 label 隐私<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#31-%E6%9E%9A%E4%B8%BE%E6%B5%81%E7%A8%8B%E4%B8%8E-label-%E9%9A%90%E7%A7%81" class="hash-link" aria-label="Direct link to 3.1 枚举流程与 label 隐私" title="Direct link to 3.1 枚举流程与 label 隐私" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 枚举所有媒体设备</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 注意：在用户授权 getUserMedia 之前，label 可能是空字符串（隐私保护）</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">listMediaDevices</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> devices </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">enumerateDevices</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> grouped </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">videoinput</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">// 摄像头</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">audioinput</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic">// 麦克风</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">audiooutput</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 扬声器（仅部分浏览器支持 setSinkId）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> device </span><span class="token keyword" style="color:#00009f">of</span><span class="token plain"> devices</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    grouped</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">device</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token punctuation" style="color:#393A34">]</span><span class="token operator" style="color:#393A34">?.</span><span class="token method function property-access" style="color:#d73a49">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">deviceId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> device</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">deviceId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">label</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> device</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">label</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">||</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">device</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">kind</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c"> (</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">device</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">deviceId</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation method function property-access" style="color:#d73a49">slice</span><span class="token template-string interpolation punctuation" style="color:#393A34">(</span><span class="token template-string interpolation number" style="color:#36acaa">0</span><span class="token template-string interpolation punctuation" style="color:#393A34">,</span><span class="token template-string interpolation"> </span><span class="token template-string interpolation number" style="color:#36acaa">8</span><span class="token template-string interpolation punctuation" style="color:#393A34">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">…)</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">groupId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> device</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">groupId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 同一物理设备的不同 kind 共享 groupId</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> grouped</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<!-- -->
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>groupId 的用途</div><div class="admonitionContent_BuS1"><p>同一物理设备（如带麦克风的 USB 摄像头）的 <code>videoinput</code> 和 <code>audioinput</code> 条目会共享相同的 <code>groupId</code>。切换摄像头时可同时切换对应麦克风，避免音视频来自不同设备。</p></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-扬声器选择setsinkid">3.2 扬声器选择：setSinkId<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#32-%E6%89%AC%E5%A3%B0%E5%99%A8%E9%80%89%E6%8B%A9setsinkid" class="hash-link" aria-label="Direct link to 3.2 扬声器选择：setSinkId" title="Direct link to 3.2 扬声器选择：setSinkId" translate="no">​</a></h3>
<p>部分浏览器支持将音频输出路由到指定扬声器：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 切换音频输出设备（Chrome / Edge 支持，Safari 不支持）</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@param</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">HTMLMediaElement</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment parameter" style="color:#999988;font-style:italic">element</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> - &lt;video&gt; 或 &lt;audio&gt;</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@param</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">string</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment parameter" style="color:#999988;font-style:italic">deviceId</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> - audiooutput 的 deviceId</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">setAudioOutput</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">element</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> deviceId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">typeof</span><span class="token plain"> element</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">setSinkId</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">!==</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"function"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">warn</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"当前浏览器不支持 setSinkId"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> element</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">setSinkId</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">deviceId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 用法</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> devices </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">enumerateDevices</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> speakers </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> devices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">d</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> d</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"audiooutput"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">setAudioOutput</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">remoteVideo</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> speakers</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">deviceId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="33-设备热切换与-replacetrack">3.3 设备热切换与 replaceTrack<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#33-%E8%AE%BE%E5%A4%87%E7%83%AD%E5%88%87%E6%8D%A2%E4%B8%8E-replacetrack" class="hash-link" aria-label="Direct link to 3.3 设备热切换与 replaceTrack" title="Direct link to 3.3 设备热切换与 replaceTrack" translate="no">​</a></h3>
<p>切换摄像头是会议产品的常见需求。正确做法是 <strong>替换 Track</strong>，而非重建整个 <code>RTCPeerConnection</code>：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 切换到指定摄像头</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@param</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">RTCPeerConnection</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment parameter" style="color:#999988;font-style:italic">pc</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> - 已建立的 PeerConnection（Ch2 会用到）</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@param</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">string</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment parameter" style="color:#999988;font-style:italic">deviceId</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> - 目标摄像头 deviceId</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * </span><span class="token doc-comment comment keyword" style="color:#00009f;font-style:italic">@param</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">{</span><span class="token doc-comment comment class-name" style="color:#999988;font-style:italic">MediaStream</span><span class="token doc-comment comment class-name punctuation" style="color:#393A34;font-style:italic">}</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> </span><span class="token doc-comment comment parameter" style="color:#999988;font-style:italic">currentStream</span><span class="token doc-comment comment" style="color:#999988;font-style:italic"> - 当前本地流</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">switchCamera</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> deviceId</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> currentStream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 1. 用新 deviceId 采集</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> newStream </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getUserMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">deviceId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">exact</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> deviceId </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">width</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1280</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">audio</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 只换视频轨，音频保持不变</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> newVideoTrack </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> newStream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getVideoTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> oldVideoTrack </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> currentStream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getVideoTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 2. 如果已有 PeerConnection，替换 sender 上的 track</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sender </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getSenders</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">s</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">track</span><span class="token operator" style="color:#393A34">?.</span><span class="token plain">kind </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"video"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sender</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> sender</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">replaceTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">newVideoTrack</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// replaceTrack 不会触发 renegotiation（同一编码器类型时）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 3. 更新本地预览</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  currentStream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">removeTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">oldVideoTrack</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  oldVideoTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stop</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 释放硬件资源！</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  currentStream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">newVideoTrack</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> newVideoTrack</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<!-- -->
<p><code>replaceTrack()</code> 是生产会议中「切换摄像头」的标准做法，Ch12 SFU 场景同样适用。注意：<strong>必须 <code>stop()</code> 旧 track</strong>，否则摄像头指示灯不会熄灭，硬件资源被泄漏。</p>
<table><thead><tr><th style="text-align:left">切换场景</th><th style="text-align:left">正确做法</th><th style="text-align:left">错误做法</th></tr></thead><tbody><tr><td style="text-align:left">换摄像头</td><td style="text-align:left"><code>getUserMedia</code> + <code>replaceTrack</code> + <code>stop()</code> 旧轨</td><td style="text-align:left">重建 PeerConnection</td></tr><tr><td style="text-align:left">关摄像头</td><td style="text-align:left"><code>track.enabled = false</code> 或 <code>stop()</code></td><td style="text-align:left">只隐藏 <code>&lt;video&gt;</code> 元素</td></tr><tr><td style="text-align:left">静音</td><td style="text-align:left"><code>track.enabled = false</code></td><td style="text-align:left"><code>stop()</code> 音频轨（需重新采集）</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四mediastream-与-mediastreamtrack-生命周期">四、MediaStream 与 MediaStreamTrack 生命周期<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#%E5%9B%9Bmediastream-%E4%B8%8E-mediastreamtrack-%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F" class="hash-link" aria-label="Direct link to 四、MediaStream 与 MediaStreamTrack 生命周期" title="Direct link to 四、MediaStream 与 MediaStreamTrack 生命周期" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-对象关系">4.1 对象关系<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#41-%E5%AF%B9%E8%B1%A1%E5%85%B3%E7%B3%BB" class="hash-link" aria-label="Direct link to 4.1 对象关系" title="Direct link to 4.1 对象关系" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-track-状态机">4.2 Track 状态机<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#42-track-%E7%8A%B6%E6%80%81%E6%9C%BA" class="hash-link" aria-label="Direct link to 4.2 Track 状态机" title="Direct link to 4.2 Track 状态机" translate="no">​</a></h3>
<!-- -->
<table><thead><tr><th style="text-align:left">属性/状态</th><th style="text-align:left">含义</th><th style="text-align:left">常见误区</th></tr></thead><tbody><tr><td style="text-align:left"><code>readyState: "live"</code></td><td style="text-align:left">正常采集</td><td style="text-align:left">—</td></tr><tr><td style="text-align:left"><code>readyState: "ended"</code></td><td style="text-align:left">已停止，<strong>不可恢复</strong></td><td style="text-align:left">误以为可以 restart</td></tr><tr><td style="text-align:left"><code>enabled: false</code></td><td style="text-align:left">轨道暂停发送黑帧/静音，但硬件仍占用</td><td style="text-align:left">与 <code>stop()</code> 不同</td></tr><tr><td style="text-align:left"><code>muted: true</code></td><td style="text-align:left">轨道仍在，但不产出数据</td><td style="text-align:left">用户关摄像头盖时触发</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="43-enabled-vs-stop-vs-muted">4.3 enabled vs stop vs muted<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#43-enabled-vs-stop-vs-muted" class="hash-link" aria-label="Direct link to 4.3 enabled vs stop vs muted" title="Direct link to 4.3 enabled vs stop vs muted" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> videoTrack </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> stream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getVideoTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 方式 1：enabled = false — 暂停发送，硬件仍占用，可快速恢复</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">videoTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">enabled</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 发送黑帧</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">videoTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">enabled</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 立即恢复</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 方式 2：stop() — 彻底释放硬件，不可恢复</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">videoTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stop</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// readyState → "ended"，需重新 getUserMedia</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 方式 3：muted — 系统/硬件触发，应用无法直接控制</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">videoTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onmute</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"硬件静音（如合上笔记本盖）"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="44-事件监听与资源清理">4.4 事件监听与资源清理<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#44-%E4%BA%8B%E4%BB%B6%E7%9B%91%E5%90%AC%E4%B8%8E%E8%B5%84%E6%BA%90%E6%B8%85%E7%90%86" class="hash-link" aria-label="Direct link to 4.4 事件监听与资源清理" title="Direct link to 4.4 事件监听与资源清理" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">attachTrackListeners</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">track</span><span class="token parameter punctuation" style="color:#393A34">,</span><span class="token parameter"> label</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onended</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">[</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">label</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">] track ended — 用户或系统停止了采集</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 更新 UI：显示「摄像头已关闭」</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onmute</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">[</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">label</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">] track muted — 暂时无数据</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onunmute</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">[</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">label</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c">] track unmuted — 恢复数据</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 完整清理 — 页面卸载 / 用户挂断时必须调用</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 遗漏 stop() 会导致摄像头指示灯常亮</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">releaseMediaStream</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">stream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">stream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  stream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">forEach</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">track</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    track</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">stop</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    stream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">removeTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">track</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 页面关闭时自动清理</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token dom variable" style="color:#36acaa">window</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addEventListener</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"beforeunload"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">releaseMediaStream</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">currentStream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// SPA 路由切换时也要清理</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// React: useEffect(() =&gt; () =&gt; releaseMediaStream(stream), []);</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="45-applyconstraints-运行时调整">4.5 applyConstraints 运行时调整<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#45-applyconstraints-%E8%BF%90%E8%A1%8C%E6%97%B6%E8%B0%83%E6%95%B4" class="hash-link" aria-label="Direct link to 4.5 applyConstraints 运行时调整" title="Direct link to 4.5 applyConstraints 运行时调整" translate="no">​</a></h3>
<p>无需重新 <code>getUserMedia</code>，可在 <code>live</code> 状态下动态调整部分参数：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> videoTrack </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> stream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getVideoTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 运行时降低分辨率（节省带宽，Ch10 会深入）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> videoTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">applyConstraints</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">width</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">640</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">height</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">360</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token literal-property property" style="color:#36acaa">frameRate</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">15</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 确认生效</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">videoTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getSettings</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="46-clone-与-contenthint">4.6 clone() 与 contentHint<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#46-clone-%E4%B8%8E-contenthint" class="hash-link" aria-label="Direct link to 4.6 clone() 与 contentHint" title="Direct link to 4.6 clone() 与 contentHint" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// clone() — 创建独立副本，两个 track 共享同一硬件源</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> clonedTrack </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> videoTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">clone</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 用途：一个 track 给本地预览，一个给 PeerConnection</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// contentHint — 告知编码器内容类型，影响码率分配策略</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">videoTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">contentHint</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"motion"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 运动场景（体育、游戏）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// videoTrack.contentHint = "detail";  // 细节场景（文档、白板）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// videoTrack.contentHint = "text";    // 文字场景（代码、字幕）</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>applyConstraints 的局限</div><div class="admonitionContent_BuS1"><p>并非所有约束都支持运行时修改。<code>deviceId</code> 变更必须重新 <code>getUserMedia</code> + <code>replaceTrack</code>。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五屏幕共享-getdisplaymedia">五、屏幕共享 getDisplayMedia<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#%E4%BA%94%E5%B1%8F%E5%B9%95%E5%85%B1%E4%BA%AB-getdisplaymedia" class="hash-link" aria-label="Direct link to 五、屏幕共享 getDisplayMedia" title="Direct link to 五、屏幕共享 getDisplayMedia" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-基本用法">5.1 基本用法<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#51-%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95" class="hash-link" aria-label="Direct link to 5.1 基本用法" title="Direct link to 5.1 基本用法" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">startScreenShare</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">try</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> screenStream </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getDisplayMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">cursor</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"always"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">       </span><span class="token comment" style="color:#999988;font-style:italic">// 显示鼠标光标</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token literal-property property" style="color:#36acaa">displaySurface</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"monitor"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 理想：整个屏幕（浏览器可能忽略）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token literal-property property" style="color:#36acaa">audio</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// Chrome 支持采集 Tab 音频 / 系统音频</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// 注意：getDisplayMedia 不接受 deviceId 约束</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// 注意：必须由用户手势触发</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> screenTrack </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> screenStream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getVideoTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> settings </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> screenTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getSettings</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string string" style="color:#e3116c">共享类型: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">settings</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">displaySurface</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// "monitor" | "window" | "browser"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 用户从系统 UI 停止共享时触发</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    screenTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onended</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"用户点击了「停止共享」"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// 恢复摄像头画面或更新 UI</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> screenStream</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">err</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">err</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">name</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"NotAllowedError"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"用户取消了屏幕选择"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">throw</span><span class="token plain"> err</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-getusermedia-vs-getdisplaymedia">5.2 getUserMedia vs getDisplayMedia<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#52-getusermedia-vs-getdisplaymedia" class="hash-link" aria-label="Direct link to 5.2 getUserMedia vs getDisplayMedia" title="Direct link to 5.2 getUserMedia vs getDisplayMedia" translate="no">​</a></h3>
<!-- -->
<table><thead><tr><th style="text-align:left">对比项</th><th style="text-align:left">getUserMedia</th><th style="text-align:left">getDisplayMedia</th></tr></thead><tbody><tr><td style="text-align:left">用户交互</td><td style="text-align:left">权限弹窗（首次）</td><td style="text-align:left">每次调用都弹出选择器</td></tr><tr><td style="text-align:left">停止方式</td><td style="text-align:left">应用调用 <code>track.stop()</code></td><td style="text-align:left">用户可从 OS 栏停止 → <code>onended</code></td></tr><tr><td style="text-align:left">音频</td><td style="text-align:left">麦克风</td><td style="text-align:left">Tab 音频 / 系统音频（浏览器依赖）</td></tr><tr><td style="text-align:left">Constraints</td><td style="text-align:left">完整支持</td><td style="text-align:left">仅 <code>video</code> / <code>audio</code> 布尔或少量字段</td></tr><tr><td style="text-align:left">移动端</td><td style="text-align:left">广泛支持</td><td style="text-align:left">iOS Safari 16+ 有限支持</td></tr><tr><td style="text-align:left">系统音频</td><td style="text-align:left">不适用</td><td style="text-align:left">Chrome Tab 共享时可采集 Tab 音频</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="53-屏幕共享--摄像头画中画">5.3 屏幕共享 + 摄像头画中画<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#53-%E5%B1%8F%E5%B9%95%E5%85%B1%E4%BA%AB--%E6%91%84%E5%83%8F%E5%A4%B4%E7%94%BB%E4%B8%AD%E7%94%BB" class="hash-link" aria-label="Direct link to 5.3 屏幕共享 + 摄像头画中画" title="Direct link to 5.3 屏幕共享 + 摄像头画中画" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 同时发送摄像头和屏幕 — 需要两个 VideoTrack</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * Ch2 会学到用 addTransceiver 或两次 addTrack 发送多路视频</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">startCameraAndScreen</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">pc</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> cameraStream </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getUserMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">audio</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> screenStream </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getDisplayMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">audio</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">false</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 摄像头作为主轨道</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> track </span><span class="token keyword" style="color:#00009f">of</span><span class="token plain"> cameraStream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">track</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> cameraStream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 屏幕作为第二条视频轨（需要 renegotiation）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> screenTrack </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> screenStream</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getVideoTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">screenTrack</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> screenStream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  screenTrack</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onended</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> sender </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getSenders</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">s</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> s</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">track</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> screenTrack</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sender</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> pc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">removeTrack</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">sender</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 触发 negotiationneeded → 需要重新 Offer/Answer（Ch2）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六权限模型与用户手势">六、权限模型与用户手势<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#%E5%85%AD%E6%9D%83%E9%99%90%E6%A8%A1%E5%9E%8B%E4%B8%8E%E7%94%A8%E6%88%B7%E6%89%8B%E5%8A%BF" class="hash-link" aria-label="Direct link to 六、权限模型与用户手势" title="Direct link to 六、权限模型与用户手势" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="61-permissions-api-查询">6.1 Permissions API 查询<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#61-permissions-api-%E6%9F%A5%E8%AF%A2" class="hash-link" aria-label="Direct link to 6.1 Permissions API 查询" title="Direct link to 6.1 Permissions API 查询" translate="no">​</a></h3>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic">/**</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 查询当前权限状态（Chrome / Firefox 支持）</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> * 返回值: "granted" | "denied" | "prompt"</span><br></div><div class="token-line" style="color:#393A34"><span class="token doc-comment comment" style="color:#999988;font-style:italic"> */</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">checkPermissions</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> results </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">for</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> name </span><span class="token keyword" style="color:#00009f">of</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"camera"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"microphone"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">try</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> status </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">permissions</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">query</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> name </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      results</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">name</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> status</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">state</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token comment" style="color:#999988;font-style:italic">// 监听权限变化（用户可能在地址栏修改）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      status</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method-variable function-variable method function property-access" style="color:#d73a49">onchange</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">name</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string string" style="color:#e3116c"> 权限变为: </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">${</span><span class="token template-string interpolation">status</span><span class="token template-string interpolation punctuation" style="color:#393A34">.</span><span class="token template-string interpolation property-access">state</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#393A34">}</span><span class="token template-string template-punctuation string" style="color:#e3116c">`</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">status</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">state</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"denied"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          </span><span class="token function" style="color:#d73a49">releaseMediaStream</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">currentStream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      results</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">name</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"unsupported"</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">return</span><span class="token plain"> results</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="62-permissions-policy-http-头">6.2 Permissions-Policy HTTP 头<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#62-permissions-policy-http-%E5%A4%B4" class="hash-link" aria-label="Direct link to 6.2 Permissions-Policy HTTP 头" title="Direct link to 6.2 Permissions-Policy HTTP 头" translate="no">​</a></h3>
<p>即使页面有 HTTPS，服务器也可以通过 HTTP 头禁止媒体采集：</p>
<div class="custom-code-block" data-language="http"><div class="language-http codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-http codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Permissions-Policy: camera=(self), microphone=(self), display-capture=(self)</span><br></div></code></pre></div></div></div>
<p>iframe 嵌入时需要显式授权：</p>
<div class="custom-code-block" data-language="html"><div class="language-html codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-html codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">iframe</span><span class="token tag" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">  </span><span class="token tag attr-name" style="color:#00a4db">src</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">https://your-app.com/call</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f">  </span><span class="token tag attr-name" style="color:#00a4db">allow</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">camera; microphone; display-capture</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag" style="color:#00009f"></span><br></div><div class="token-line" style="color:#393A34"><span class="token tag" style="color:#00009f"></span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">iframe</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="63-用户手势要求">6.3 用户手势要求<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#63-%E7%94%A8%E6%88%B7%E6%89%8B%E5%8A%BF%E8%A6%81%E6%B1%82" class="hash-link" aria-label="Direct link to 6.3 用户手势要求" title="Direct link to 6.3 用户手势要求" translate="no">​</a></h3>
<!-- -->
<p><strong>最佳实践</strong>：</p>
<ol>
<li class="">永远在用户点击「开始通话」「开启摄像头」等按钮后调用 <code>getUserMedia</code></li>
<li class="">权限被拒绝后，引导用户到浏览器设置页手动开启，<strong>不要反复弹窗</strong></li>
<li class="">使用 <code>Permissions API</code> 在 UI 上提前展示状态（摄像头图标灰色/绿色）</li>
<li class="">持久化权限：浏览器会记住 <code>granted</code> 状态，同一 origin 下次不再弹窗</li>
</ol>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七devicechange-事件">七、devicechange 事件<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#%E4%B8%83devicechange-%E4%BA%8B%E4%BB%B6" class="hash-link" aria-label="Direct link to 七、devicechange 事件" title="Direct link to 七、devicechange 事件" translate="no">​</a></h2>
<p>USB 摄像头插拔、蓝牙耳机连接断开时，设备列表会变化：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 注册监听器 — 整个页面生命周期只需一次</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addEventListener</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"devicechange"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token console class-name">console</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">log</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"媒体设备列表发生变化"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> devices </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">enumerateDevices</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> cameras </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> devices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">filter</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">d</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> d</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">kind</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"videoinput"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 检查当前使用的 deviceId 是否还存在</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> currentTrack </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> currentStream</span><span class="token operator" style="color:#393A34">?.</span><span class="token method function property-access" style="color:#d73a49">getVideoTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> currentDeviceId </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> currentTrack</span><span class="token operator" style="color:#393A34">?.</span><span class="token method function property-access" style="color:#d73a49">getSettings</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">deviceId</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#00009f">const</span><span class="token plain"> stillExists </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> cameras</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">some</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">d</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> d</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">deviceId</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">===</span><span class="token plain"> currentDeviceId</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token operator" style="color:#393A34">!</span><span class="token plain">stillExists </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> cameras</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">length</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic">// 当前设备被拔出，自动切换到第一个可用摄像头</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">switchCamera</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">pc</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> cameras</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">deviceId</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> currentStream</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic">// 刷新 UI 下拉列表</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">refreshDeviceSelectors</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">devices</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<!-- -->
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>devicechange 不会自动切换 track</div><div class="admonitionContent_BuS1"><p><code>devicechange</code> 只通知设备列表变化，<strong>不会</strong>自动停止或切换正在使用的 track。被拔出的设备对应的 track 会触发 <code>onended</code>，你需要同时监听两个事件。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八跨浏览器兼容adapterjs">八、跨浏览器兼容：adapter.js<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#%E5%85%AB%E8%B7%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E5%85%BC%E5%AE%B9adapterjs" class="hash-link" aria-label="Direct link to 八、跨浏览器兼容：adapter.js" title="Direct link to 八、跨浏览器兼容：adapter.js" translate="no">​</a></h2>
<p>不同浏览器对 WebRTC 的实现存在细微差异。在 WebRTC 标准化之前（参见 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 历史</a> 中 Serge 描述的「每个子系统不同 API」困境），开发者需要处理 <code>webkitGetUserMedia</code>、<code>mozGetUserMedia</code> 等前缀。</p>
<p><a href="https://github.com/webrtc/adapter" target="_blank" rel="noopener noreferrer" class="">adapter.js</a> 是官方维护的 shim：</p>
<div class="custom-code-block" data-language="html"><div class="language-html codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-html codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">&lt;!-- CDN 引入 --&gt;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag" style="color:#00009f">script</span><span class="token tag" style="color:#00009f"> </span><span class="token tag attr-name" style="color:#00a4db">src</span><span class="token tag attr-value punctuation attr-equals" style="color:#393A34">=</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag attr-value" style="color:#e3116c">https://webrtc.github.io/adapter/adapter-latest.js</span><span class="token tag attr-value punctuation" style="color:#393A34">"</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token script"></span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag" style="color:#00009f">script</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><br></div></code></pre></div></div></div>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// npm 引入（现代项目）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword module" style="color:#00009f">import</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"webrtc-adapter"</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<p>adapter.js 统一了：</p>
<table><thead><tr><th style="text-align:left">差异点</th><th style="text-align:left">无 adapter</th><th style="text-align:left">有 adapter</th></tr></thead><tbody><tr><td style="text-align:left"><code>getUserMedia</code> 前缀</td><td style="text-align:left"><code>webkit</code> / <code>moz</code></td><td style="text-align:left">统一 <code>navigator.mediaDevices.getUserMedia</code></td></tr><tr><td style="text-align:left"><code>RTCPeerConnection</code> 前缀</td><td style="text-align:left"><code>webkitRTCPeerConnection</code></td><td style="text-align:left">统一构造函数</td></tr><tr><td style="text-align:left"><code>attachMediaStream</code></td><td style="text-align:left">旧式 API</td><td style="text-align:left">自动 shim</td></tr><tr><td style="text-align:left">Safari 特定行为</td><td style="text-align:left">需手动处理</td><td style="text-align:left">内置 workaround</td></tr><tr><td style="text-align:left"><code>Promise</code> 化</td><td style="text-align:left">旧 API 用 callback</td><td style="text-align:left">统一返回 Promise</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="81-浏览器差异速查">8.1 浏览器差异速查<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#81-%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%AE%E5%BC%82%E9%80%9F%E6%9F%A5" class="hash-link" aria-label="Direct link to 8.1 浏览器差异速查" title="Direct link to 8.1 浏览器差异速查" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">特性</th><th style="text-align:center">Chrome</th><th style="text-align:center">Firefox</th><th style="text-align:center">Safari</th><th style="text-align:left">备注</th></tr></thead><tbody><tr><td style="text-align:left">getUserMedia</td><td style="text-align:center">✅</td><td style="text-align:center">✅</td><td style="text-align:center">✅</td><td style="text-align:left">均需 HTTPS</td></tr><tr><td style="text-align:left">getDisplayMedia</td><td style="text-align:center">✅</td><td style="text-align:center">✅</td><td style="text-align:center">✅ 16+</td><td style="text-align:left">Safari 有限制</td></tr><tr><td style="text-align:left">enumerateDevices label</td><td style="text-align:center">✅</td><td style="text-align:center">✅</td><td style="text-align:center">✅</td><td style="text-align:left">需先授权</td></tr><tr><td style="text-align:left">setSinkId</td><td style="text-align:center">✅</td><td style="text-align:center">❌</td><td style="text-align:center">❌</td><td style="text-align:left">仅 Chromium</td></tr><tr><td style="text-align:left">devicechange</td><td style="text-align:center">✅</td><td style="text-align:center">✅</td><td style="text-align:center">✅</td><td style="text-align:left">—</td></tr><tr><td style="text-align:left">facingMode</td><td style="text-align:center">✅</td><td style="text-align:center">✅</td><td style="text-align:center">✅</td><td style="text-align:left">移动端</td></tr><tr><td style="text-align:left">applyConstraints</td><td style="text-align:center">✅</td><td style="text-align:center">✅</td><td style="text-align:center">部分</td><td style="text-align:left">因设备而异</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>本系列 Demo 策略</div><div class="admonitionContent_BuS1"><p><code>examples/webrtc-lab</code> 面向现代 Chrome/Firefox/Safari，暂不强制依赖 adapter.js。生产项目建议引入，尤其是需支持旧版浏览器或 Electron 内嵌 WebView 时。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九常见问题与踩坑指南">九、常见问题与踩坑指南<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#%E4%B9%9D%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E4%B8%8E%E8%B8%A9%E5%9D%91%E6%8C%87%E5%8D%97" class="hash-link" aria-label="Direct link to 九、常见问题与踩坑指南" title="Direct link to 九、常见问题与踩坑指南" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q1-为什么-enumeratedevices-返回的-label-是空的">Q1: 为什么 enumerateDevices 返回的 label 是空的？<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#q1-%E4%B8%BA%E4%BB%80%E4%B9%88-enumeratedevices-%E8%BF%94%E5%9B%9E%E7%9A%84-label-%E6%98%AF%E7%A9%BA%E7%9A%84" class="hash-link" aria-label="Direct link to Q1: 为什么 enumerateDevices 返回的 label 是空的？" title="Direct link to Q1: 为什么 enumerateDevices 返回的 label 是空的？" translate="no">​</a></h3>
<p><strong>A</strong>: 浏览器隐私策略——在用户未授权 <code>getUserMedia</code> 之前，不暴露设备名称。先调用一次 <code>getUserMedia</code>（或用户授权后），再 <code>enumerateDevices</code> 即可获取 label。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q2-切换摄像头后远端画面没有变化">Q2: 切换摄像头后远端画面没有变化？<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#q2-%E5%88%87%E6%8D%A2%E6%91%84%E5%83%8F%E5%A4%B4%E5%90%8E%E8%BF%9C%E7%AB%AF%E7%94%BB%E9%9D%A2%E6%B2%A1%E6%9C%89%E5%8F%98%E5%8C%96" class="hash-link" aria-label="Direct link to Q2: 切换摄像头后远端画面没有变化？" title="Direct link to Q2: 切换摄像头后远端画面没有变化？" translate="no">​</a></h3>
<p><strong>A</strong>: 检查是否调用了 <code>sender.replaceTrack(newTrack)</code> 而非仅更新本地预览。本地 <code>&lt;video&gt;</code> 的 <code>srcObject</code> 和 PeerConnection 的 sender 是独立路径。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q3-页面关闭后摄像头指示灯还亮着">Q3: 页面关闭后摄像头指示灯还亮着？<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#q3-%E9%A1%B5%E9%9D%A2%E5%85%B3%E9%97%AD%E5%90%8E%E6%91%84%E5%83%8F%E5%A4%B4%E6%8C%87%E7%A4%BA%E7%81%AF%E8%BF%98%E4%BA%AE%E7%9D%80" class="hash-link" aria-label="Direct link to Q3: 页面关闭后摄像头指示灯还亮着？" title="Direct link to Q3: 页面关闭后摄像头指示灯还亮着？" translate="no">​</a></h3>
<p><strong>A</strong>: 遗漏了 <code>track.stop()</code>。确保在 <code>beforeunload</code>、<code>pagehide</code> 和组件 unmount 时都调用 <code>releaseMediaStream()</code>。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q4-getusermedia-在-iframe-中不工作">Q4: getUserMedia 在 iframe 中不工作？<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#q4-getusermedia-%E5%9C%A8-iframe-%E4%B8%AD%E4%B8%8D%E5%B7%A5%E4%BD%9C" class="hash-link" aria-label="Direct link to Q4: getUserMedia 在 iframe 中不工作？" title="Direct link to Q4: getUserMedia 在 iframe 中不工作？" translate="no">​</a></h3>
<p><strong>A</strong>: iframe 需要 <code>allow="camera; microphone"</code> 属性，且 iframe 自身也必须在安全上下文中。父页面还需设置 <code>Permissions-Policy</code> 允许嵌入。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q5-移动端前后摄像头怎么切换">Q5: 移动端前后摄像头怎么切换？<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#q5-%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%89%8D%E5%90%8E%E6%91%84%E5%83%8F%E5%A4%B4%E6%80%8E%E4%B9%88%E5%88%87%E6%8D%A2" class="hash-link" aria-label="Direct link to Q5: 移动端前后摄像头怎么切换？" title="Direct link to Q5: 移动端前后摄像头怎么切换？" translate="no">​</a></h3>
<p><strong>A</strong>: 使用 <code>facingMode</code> 约束而非 deviceId（移动端 deviceId 可能不稳定）：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// 前置</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">facingMode</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"user"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">// 后置</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">facingMode</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"environment"</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q6-为什么-constraints-设置了-1080p-实际只有-360p">Q6: 为什么 Constraints 设置了 1080p 实际只有 360p？<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#q6-%E4%B8%BA%E4%BB%80%E4%B9%88-constraints-%E8%AE%BE%E7%BD%AE%E4%BA%86-1080p-%E5%AE%9E%E9%99%85%E5%8F%AA%E6%9C%89-360p" class="hash-link" aria-label="Direct link to Q6: 为什么 Constraints 设置了 1080p 实际只有 360p？" title="Direct link to Q6: 为什么 Constraints 设置了 1080p 实际只有 360p？" translate="no">​</a></h3>
<p><strong>A</strong>: 浏览器在 CPU/带宽压力下会自动降级。用 <code>track.getSettings()</code> 确认实际值；如需强制，用 <code>min</code> 约束但要做好失败兜底。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q7-getdisplaymedia-和-getusermedia-能否同时调用">Q7: getDisplayMedia 和 getUserMedia 能否同时调用？<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#q7-getdisplaymedia-%E5%92%8C-getusermedia-%E8%83%BD%E5%90%A6%E5%90%8C%E6%97%B6%E8%B0%83%E7%94%A8" class="hash-link" aria-label="Direct link to Q7: getDisplayMedia 和 getUserMedia 能否同时调用？" title="Direct link to Q7: getDisplayMedia 和 getUserMedia 能否同时调用？" translate="no">​</a></h3>
<p><strong>A</strong>: 可以。两个独立的 <code>MediaStream</code>，分别有不同 Track。注意最终传给 PeerConnection 时需要管理多条 Track（Ch2 / Ch12 详述）。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q8-本地预览有回声怎么办">Q8: 本地预览有回声怎么办？<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#q8-%E6%9C%AC%E5%9C%B0%E9%A2%84%E8%A7%88%E6%9C%89%E5%9B%9E%E5%A3%B0%E6%80%8E%E4%B9%88%E5%8A%9E" class="hash-link" aria-label="Direct link to Q8: 本地预览有回声怎么办？" title="Direct link to Q8: 本地预览有回声怎么办？" translate="no">​</a></h3>
<p><strong>A</strong>: 本地 <code>&lt;video&gt;</code> 必须设置 <code>muted = true</code>。否则麦克风采集到扬声器播放的声音，形成回声。远端 <code>&lt;video&gt;</code> 不要静音。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="q9-没有摄像头怎么测试">Q9: 没有摄像头怎么测试？<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#q9-%E6%B2%A1%E6%9C%89%E6%91%84%E5%83%8F%E5%A4%B4%E6%80%8E%E4%B9%88%E6%B5%8B%E8%AF%95" class="hash-link" aria-label="Direct link to Q9: 没有摄像头怎么测试？" title="Direct link to Q9: 没有摄像头怎么测试？" translate="no">​</a></h3>
<p><strong>A</strong>: Chrome 启动参数 <code>--use-fake-device-for-media-stream</code> 会注入假设备；或在 <code>chrome://flags</code> 启用虚拟摄像头。CI 环境常用 <a href="https://github.com/node-webrtc/node-webrtc" target="_blank" rel="noopener noreferrer" class="">node-webrtc</a> 或 Puppeteer 的 fake media 模式。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十实战-lab">十、实战 Lab<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#%E5%8D%81%E5%AE%9E%E6%88%98-lab" class="hash-link" aria-label="Direct link to 十、实战 Lab" title="Direct link to 十、实战 Lab" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="101-运行-ch01-demo">10.1 运行 Ch01 Demo<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#101-%E8%BF%90%E8%A1%8C-ch01-demo" class="hash-link" aria-label="Direct link to 10.1 运行 Ch01 Demo" title="Direct link to 10.1 运行 Ch01 Demo" translate="no">​</a></h3>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token builtin class-name">cd</span><span class="token plain"> examples/webrtc-lab/client/ch01-media-devices</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">npx serve </span><span class="token builtin class-name">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 打开 http://localhost:3000</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="102-练习清单">10.2 练习清单<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#102-%E7%BB%83%E4%B9%A0%E6%B8%85%E5%8D%95" class="hash-link" aria-label="Direct link to 10.2 练习清单" title="Direct link to 10.2 练习清单" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:center">#</th><th style="text-align:left">练习</th><th style="text-align:left">预期观察</th></tr></thead><tbody><tr><td style="text-align:center">1</td><td style="text-align:left">点击「开始预览」，允许权限</td><td style="text-align:left">本地 <code>&lt;video&gt;</code> 出现画面；<code>enumerateDevices</code> 返回完整 label</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left">在下拉框切换摄像头</td><td style="text-align:left">画面切换；<code>chrome://webrtc-internals</code> 中可见新 deviceId</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left">插入/拔出 USB 摄像头</td><td style="text-align:left">控制台输出 <code>devicechange</code>；下拉列表自动刷新</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left">点击「屏幕共享」</td><td style="text-align:left">系统选择器弹出；选中后预览变为屏幕；点击 OS 停止栏触发 <code>onended</code></td></tr><tr><td style="text-align:center">5</td><td style="text-align:left">点击「停止」</td><td style="text-align:left">摄像头指示灯熄灭；<code>track.readyState</code> 变为 <code>ended</code></td></tr><tr><td style="text-align:center">6</td><td style="text-align:left">打开 <code>chrome://webrtc-internals</code></td><td style="text-align:left">查看 <code>getUserMedia</code> 请求参数、实际分辨率、帧率</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="103-扩展挑战">10.3 扩展挑战<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#103-%E6%89%A9%E5%B1%95%E6%8C%91%E6%88%98" class="hash-link" aria-label="Direct link to 10.3 扩展挑战" title="Direct link to 10.3 扩展挑战" translate="no">​</a></h3>
<ol>
<li class=""><strong>权限状态 UI</strong>：用 <code>navigator.permissions.query</code> 在按钮旁显示摄像头/麦克风权限图标</li>
<li class=""><strong>能力自适应</strong>：读取 <code>getCapabilities()</code>，根据 <code>max</code> 值动态生成分辨率选项</li>
<li class=""><strong>轨道信息面板</strong>：实时展示每个 track 的 <code>kind</code>、<code>readyState</code>、<code>muted</code>、<code>getSettings()</code></li>
<li class=""><strong>错误恢复</strong>：<code>NotReadableError</code> 时自动重试 3 次，每次间隔 1 秒</li>
<li class=""><strong>contentHint 实验</strong>：分别设置 <code>"motion"</code> 和 <code>"detail"</code>，在 <code>chrome://webrtc-internals</code> 观察码率差异</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="104-demo-核心代码导读">10.4 Demo 核心代码导读<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#104-demo-%E6%A0%B8%E5%BF%83%E4%BB%A3%E7%A0%81%E5%AF%BC%E8%AF%BB" class="hash-link" aria-label="Direct link to 10.4 Demo 核心代码导读" title="Direct link to 10.4 Demo 核心代码导读" translate="no">​</a></h3>
<p>Lab 的 <code>main.js</code> 实现了本章大部分概念：</p>
<div class="custom-code-block" data-language="javascript"><div class="language-javascript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-javascript codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic">// examples/webrtc-lab/client/ch01-media-devices/main.js（节选）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">async</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">function</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">startPreview</span><span class="token punctuation" style="color:#393A34">(</span><span class="token parameter">constraints </span><span class="token parameter operator" style="color:#393A34">=</span><span class="token parameter"> </span><span class="token parameter punctuation" style="color:#393A34">{</span><span class="token parameter punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">stopTracks</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 先释放旧轨道，避免泄漏</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  currentStream </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">getUserMedia</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">video</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> videoDeviceId</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">?</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">deviceId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">exact</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> videoDeviceId </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">width</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1280</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">height</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">ideal</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">720</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      </span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token literal-property property" style="color:#36acaa">audio</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> audioDeviceId </span><span class="token operator" style="color:#393A34">?</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">deviceId</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token literal-property property" style="color:#36acaa">exact</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> audioDeviceId </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token spread operator" style="color:#393A34">...</span><span class="token plain">constraints</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  preview</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">srcObject</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> currentStream</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword control-flow" style="color:#00009f">await</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">enumerateDevices</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 授权后刷新 label</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token dom variable" style="color:#36acaa">navigator</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">mediaDevices</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#d73a49">addEventListener</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"devicechange"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#393A34">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#d73a49">enumerateDevices</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token comment" style="color:#999988;font-style:italic">// 设备插拔时刷新列表</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十一本章小结">十一、本章小结<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#%E5%8D%81%E4%B8%80%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93" class="hash-link" aria-label="Direct link to 十一、本章小结" title="Direct link to 十一、本章小结" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">要点</th><th style="text-align:left">内容</th></tr></thead><tbody><tr><td style="text-align:left">入口 API</td><td style="text-align:left"><code>getUserMedia</code> / <code>getDisplayMedia</code> / <code>enumerateDevices</code></td></tr><tr><td style="text-align:left">核心对象</td><td style="text-align:left"><code>MediaStream</code> → <code>MediaStreamTrack</code>，理解生命周期与事件</td></tr><tr><td style="text-align:left">Constraints</td><td style="text-align:left">生产用 <code>ideal</code>，事后 <code>getSettings()</code> 确认；避免 <code>exact</code></td></tr><tr><td style="text-align:left">设备切换</td><td style="text-align:left"><code>enumerateDevices</code> + <code>getUserMedia(新 deviceId)</code> + <code>replaceTrack</code></td></tr><tr><td style="text-align:left">权限</td><td style="text-align:left">HTTPS + 用户手势 + <code>Permissions API</code> + <code>Permissions-Policy</code></td></tr><tr><td style="text-align:left">设备热插拔</td><td style="text-align:left">监听 <code>devicechange</code> + track <code>onended</code>，检查当前 track 有效性</td></tr><tr><td style="text-align:left">跨浏览器</td><td style="text-align:left"><code>adapter.js</code> 抹平前缀差异</td></tr><tr><td style="text-align:left">资源清理</td><td style="text-align:left"><code>track.stop()</code> 释放硬件，页面卸载必须清理</td></tr></tbody></table>
<p><strong>下一篇（Ch2）</strong> 我们把采集到的 <code>MediaStream</code> 通过 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call"><code>RTCPeerConnection</code></a> 发送给另一个浏览器，用 Offer/Answer 模型完成第一个 P2P 视频通话。</p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-browser-api#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://www.w3.org/TR/mediacapture-streams/" target="_blank" rel="noopener noreferrer" class="">W3C Media Capture and Streams</a></li>
<li class=""><a href="https://www.w3.org/TR/screen-capture/" target="_blank" rel="noopener noreferrer" class="">W3C Screen Capture</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia" target="_blank" rel="noopener noreferrer" class="">MDN — MediaDevices.getUserMedia()</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia" target="_blank" rel="noopener noreferrer" class="">MDN — MediaDevices.getDisplayMedia()</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack" target="_blank" rel="noopener noreferrer" class="">MDN — MediaStreamTrack</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API" target="_blank" rel="noopener noreferrer" class="">MDN — Permissions API</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy" target="_blank" rel="noopener noreferrer" class="">MDN — Permissions-Policy</a></li>
<li class=""><a href="https://webrtc.github.io/samples/src/content/getusermedia/gum/" target="_blank" rel="noopener noreferrer" class="">WebRTC Samples — getUserMedia</a></li>
<li class=""><a href="https://webrtc.github.io/samples/src/content/getusermedia/getdisplaymedia/" target="_blank" rel="noopener noreferrer" class="">WebRTC Samples — getDisplayMedia</a></li>
<li class=""><a href="https://github.com/webrtc/adapter" target="_blank" rel="noopener noreferrer" class="">adapter.js</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史</a></li>
<li class=""><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">Ch0 架构全景</a></li>
<li class=""><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">Ch2 第一个 P2P 视频通话</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>实时通信</category>
            <category>教程</category>
            <category>Frontend Development</category>
            <category>Deep Dive</category>
        </item>
        <item>
            <title><![CDATA[WebRTC 全景实战 (0)：架构全景与协议栈地图]]></title>
            <link>https://rainlib.vercel.app/en/blog/webrtc-architecture</link>
            <guid>https://rainlib.vercel.app/en/blog/webrtc-architecture</guid>
            <pubDate>Thu, 11 Jun 2026 08:00:00 GMT</pubDate>
            <description><![CDATA[从零理解 WebRTC 的设计目标、协议栈分层、W3C API 与 IETF 标准对照，建立贯穿全系列的认知地图]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>"WebRTC 不是某一个 API，而是一整套让浏览器能够安全、低延迟地交换实时媒体的开放标准。"</p>
</blockquote>
<p>如果你曾经尝试在网页里做视频通话，大概率遇到过这样的困惑：<code>RTCPeerConnection</code> 的文档能看懂，但 ICE 一直 <code>failed</code>；SDP 里一堆 <code>a=rtpmap</code> 不知道在协商什么；明明本地预览正常，对方却听不到声音。</p>
<p>这些问题的根源，通常不是某个 API 调用错了，而是<strong>缺少一张 WebRTC 协议栈的全景地图</strong>。</p>
<p>本系列 <strong>WebRTC 全景实战</strong> 共 16 篇，将从架构认知出发，逐层深入到信令、SDP、ICE、DTLS/SRTP、RTP/RTCP、编解码、拥塞控制、SFU 架构与生产部署，每篇均配套 <code>examples/webrtc-lab</code> 实战代码。本文作为第零章，回答最根本的问题：<strong>WebRTC 是什么？它从哪来？它由哪些层组成？</strong></p>
<!-- -->
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="本篇术语表系列总纲">本篇术语表（系列总纲）<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#%E6%9C%AC%E7%AF%87%E6%9C%AF%E8%AF%AD%E8%A1%A8%E7%B3%BB%E5%88%97%E6%80%BB%E7%BA%B2" class="hash-link" aria-label="Direct link to 本篇术语表（系列总纲）" title="Direct link to 本篇术语表（系列总纲）" translate="no">​</a></h2>
<p>后续各章会重复出现这些概念，建议先建立整体认知：</p>
<table><thead><tr><th style="text-align:left">术语</th><th style="text-align:left">英文</th><th style="text-align:left">解释</th></tr></thead><tbody><tr><td style="text-align:left"><strong>WebRTC</strong></td><td style="text-align:left">Web Real-Time Communication</td><td style="text-align:left">W3C + IETF 联合定义的浏览器实时通信标准栈，<strong>不是</strong>某个 npm 包或单一 SDK</td></tr><tr><td style="text-align:left"><strong>W3C</strong></td><td style="text-align:left">World Wide Web Consortium</td><td style="text-align:left">定义浏览器 JavaScript API（<code>getUserMedia</code>、<code>RTCPeerConnection</code> 等）</td></tr><tr><td style="text-align:left"><strong>IETF</strong></td><td style="text-align:left">Internet Engineering Task Force</td><td style="text-align:left">定义底层协议（ICE、DTLS、SRTP、RTP、SCTP 等 RFC）</td></tr><tr><td style="text-align:left"><strong>P2P</strong></td><td style="text-align:left">Peer-to-Peer</td><td style="text-align:left">两个或多个端点直接交换媒体，不经过中央媒体服务器</td></tr><tr><td style="text-align:left"><strong>SFU</strong></td><td style="text-align:left">Selective Forwarding Unit</td><td style="text-align:left">选择性转发单元：服务端转发 RTP 包但不转码，见 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">Ch12</a></td></tr><tr><td style="text-align:left"><strong>MCU</strong></td><td style="text-align:left">Multipoint Control Unit</td><td style="text-align:left">多点控制单元：服务端混流/转码后分发，CPU 开销大</td></tr><tr><td style="text-align:left"><strong>Signaling</strong></td><td style="text-align:left">信令</td><td style="text-align:left">WebRTC <strong>标准外</strong>的控制通道，用于交换 SDP 和 ICE Candidate</td></tr><tr><td style="text-align:left"><strong>SDP</strong></td><td style="text-align:left">Session Description Protocol</td><td style="text-align:left">文本格式的会话「合同」，描述 Codec、ICE 参数、DTLS 指纹</td></tr><tr><td style="text-align:left"><strong>ICE</strong></td><td style="text-align:left">Interactive Connectivity Establishment</td><td style="text-align:left">在多条网络路径中选择最优 UDP 通道的算法</td></tr><tr><td style="text-align:left"><strong>STUN</strong></td><td style="text-align:left">Session Traversal Utilities for NAT</td><td style="text-align:left">帮助客户端发现公网 IP<!-- -->:Port<!-- --> 的协议</td></tr><tr><td style="text-align:left"><strong>TURN</strong></td><td style="text-align:left">Traversal Using Relays around NAT</td><td style="text-align:left">NAT 穿透失败时的中继服务器协议</td></tr><tr><td style="text-align:left"><strong>DTLS</strong></td><td style="text-align:left">Datagram Transport Layer Security</td><td style="text-align:left">UDP 上的 TLS，用于密钥协商</td></tr><tr><td style="text-align:left"><strong>SRTP</strong></td><td style="text-align:left">Secure RTP</td><td style="text-align:left">加密的 RTP 媒体传输</td></tr><tr><td style="text-align:left"><strong>RTP/RTCP</strong></td><td style="text-align:left">Real-time Transport / Control Protocol</td><td style="text-align:left">媒体数据面与控制反馈协议，1996 年标准化</td></tr><tr><td style="text-align:left"><strong>JSEP</strong></td><td style="text-align:left">JavaScript Session Establishment Protocol</td><td style="text-align:left">Offer/Answer 协商模型，<a href="https://datatracker.ietf.org/doc/html/rfc8829" target="_blank" rel="noopener noreferrer" class="">RFC 8829</a></td></tr><tr><td style="text-align:left"><strong>MBONE</strong></td><td style="text-align:left">Multicast Backbone</td><td style="text-align:left">1990 年代 Internet 多播实验网络，WebRTC 前身工具的运行环境</td></tr><tr><td style="text-align:left"><strong>NPAPI</strong></td><td style="text-align:left">Netscape Plugin API</td><td style="text-align:left">已废弃的浏览器插件接口，Gmail 视频聊天曾依赖它</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="系列学习路线图">系列学习路线图<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#%E7%B3%BB%E5%88%97%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF%E5%9B%BE" class="hash-link" aria-label="Direct link to 系列学习路线图" title="Direct link to 系列学习路线图" translate="no">​</a></h2>
<!-- -->
<table><thead><tr><th style="text-align:left">Phase</th><th style="text-align:left">章节</th><th style="text-align:left">核心能力</th><th style="text-align:left">配套 Lab</th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left">Ch0–2</td><td style="text-align:left">建立全局地图，跑通第一个 P2P</td><td style="text-align:left"><code>ch01-media-devices</code>, <code>ch02-p2p-basic</code></td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">Ch3–6</td><td style="text-align:left">理解连接建立的每一层</td><td style="text-align:left"><code>signaling/</code>, coturn</td></tr><tr><td style="text-align:left">3</td><td style="text-align:left">Ch7–9</td><td style="text-align:left">媒体加密、传输、编解码</td><td style="text-align:left">Wireshark 抓包</td></tr><tr><td style="text-align:left">4</td><td style="text-align:left">Ch10–12</td><td style="text-align:left">拥塞控制、Simulcast、SFU</td><td style="text-align:left">LiveKit / Pion</td></tr><tr><td style="text-align:left">5</td><td style="text-align:left">Ch13–15</td><td style="text-align:left">生产排障、部署、Capstone</td><td style="text-align:left">完整视频会议</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一历史脉络从-mbone-到浏览器">一、历史脉络：从 MBONE 到浏览器<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#%E4%B8%80%E5%8E%86%E5%8F%B2%E8%84%89%E7%BB%9C%E4%BB%8E-mbone-%E5%88%B0%E6%B5%8F%E8%A7%88%E5%99%A8" class="hash-link" aria-label="Direct link to 一、历史脉络：从 MBONE 到浏览器" title="Direct link to 一、历史脉络：从 MBONE 到浏览器" translate="no">​</a></h2>
<p>理解 WebRTC 的设计，需要理解它<strong>站在哪些协议之上、又为什么放弃了哪些旧路</strong>。本节参考 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史</a> 中协议作者与 Google 早期团队的访谈，梳理关键转折。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="11-时间线四十年实时通信演进">1.1 时间线：四十年实时通信演进<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#11-%E6%97%B6%E9%97%B4%E7%BA%BF%E5%9B%9B%E5%8D%81%E5%B9%B4%E5%AE%9E%E6%97%B6%E9%80%9A%E4%BF%A1%E6%BC%94%E8%BF%9B" class="hash-link" aria-label="Direct link to 1.1 时间线：四十年实时通信演进" title="Direct link to 1.1 时间线：四十年实时通信演进" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="12-rtp-的诞生从工具到协议">1.2 RTP 的诞生：从「工具」到「协议」<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#12-rtp-%E7%9A%84%E8%AF%9E%E7%94%9F%E4%BB%8E%E5%B7%A5%E5%85%B7%E5%88%B0%E5%8D%8F%E8%AE%AE" class="hash-link" aria-label="Direct link to 1.2 RTP 的诞生：从「工具」到「协议」" title="Direct link to 1.2 RTP 的诞生：从「工具」到「协议」" translate="no">​</a></h3>
<p>RTP 共同作者 <strong>Ron Frederick</strong> 在 1992 年基于 Sun VideoPix 帧采集卡编写 <strong>nv</strong> 网络视频会议工具，目标是在 <strong>128 kbps ISDN</strong> 带宽下传输可接受画质——为此他实现了专用软件视频压缩（后获专利 US5485212A），并在 MBONE（Internet 多播主干网）上向全球广播 IETF 会议。</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>设计意图</div><div class="admonitionContent_BuS1"><p>nv、vat 等早期工具各自有轻量会话协议。IETF 工作组（Van Jacobson、Steve Casner、Henning Schulzrinne 等）的目标不是再做一个工具，而是<strong>提炼所有工具可共用的媒体传输基础</strong>——这就是 RTP/RTCP（后修订为 <a href="https://datatracker.ietf.org/doc/html/rfc3550" target="_blank" rel="noopener noreferrer" class="">RFC 3550</a>）。</p></div></div>
<p>Ron Frederick 在访谈中反思：RTP 在<strong>单播</strong>场景下 RTCP 显得过于复杂；许多人转向 TCP/HTTP 流式传输因为「足够好」。但 TCP 多方通话需向每个对等方重复发送相同数据，带宽效率骤降——这正是 WebRTC 坚持 UDP + RTP 的原因。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="13-为什么-webrtc-是单播不是-ip-多播">1.3 为什么 WebRTC 是单播，不是 IP 多播？<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#13-%E4%B8%BA%E4%BB%80%E4%B9%88-webrtc-%E6%98%AF%E5%8D%95%E6%92%AD%E4%B8%8D%E6%98%AF-ip-%E5%A4%9A%E6%92%AD" class="hash-link" aria-label="Direct link to 1.3 为什么 WebRTC 是单播，不是 IP 多播？" title="Direct link to 1.3 为什么 WebRTC 是单播，不是 IP 多播？" translate="no">​</a></h3>
<!-- -->
<table><thead><tr><th style="text-align:left">维度</th><th style="text-align:left">IP 多播（MBONE 时代）</th><th style="text-align:left">WebRTC 单播 + SFU</th></tr></thead><tbody><tr><td style="text-align:left">网络要求</td><td style="text-align:left">全网/router 支持多播</td><td style="text-align:left">标准互联网即可</td></tr><tr><td style="text-align:left">服务端</td><td style="text-align:left">极简（多播代转发）</td><td style="text-align:left">需要 SFU/TURN</td></tr><tr><td style="text-align:left">运营商</td><td style="text-align:left">几乎不部署多播</td><td style="text-align:left">完全兼容</td></tr><tr><td style="text-align:left">安全</td><td style="text-align:left">难以 per-user 加密</td><td style="text-align:left">DTLS/SRTP 原生支持</td></tr></tbody></table>
<p>Marratech（后被 Google 收购）早期也押注多播——「网络帮你把包传给所有人，服务器可以非常简单」。但行业最终转向 <strong>packet shufflers（SFU）</strong>，因为公网多播从未规模化部署。WebRTC 的设计选择是<strong>现实主义的</strong>：在现有互联网上工作，而非等待网络升级。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="14-webrtc-诞生google-的三重动机">1.4 WebRTC 诞生：Google 的三重动机<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#14-webrtc-%E8%AF%9E%E7%94%9Fgoogle-%E7%9A%84%E4%B8%89%E9%87%8D%E5%8A%A8%E6%9C%BA" class="hash-link" aria-label="Direct link to 1.4 WebRTC 诞生：Google 的三重动机" title="Direct link to 1.4 WebRTC 诞生：Google 的三重动机" translate="no">​</a></h3>
<p>Google 产品经理 <strong>Serge Lachapelle</strong>（Marratech 联合创始人）在 <a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">Curious 访谈</a> 中总结了 WebRTC 诞生的背景：</p>
<!-- -->
<p><strong>Gmail 语音视频聊天</strong>（WebRTC 前身）的教训：Justin Uberti 将 GIPS（音频）、Vidyo（视频）、libjingle（网络）三个子系统拼在一起——每个都为不同问题设计，API 完全不同。WebRTC 的目标就是<strong>一次性解决这些集成痛点</strong>，让开发者专注业务。</p>
<p><strong>刻意不标准化信令</strong>：SIP 等已有方案存在，重新标准化信令「不会创造有价值贡献」，反而可能演变为政治问题。因此 WebRTC 只标准化<strong>媒体通道</strong>，信令留给应用层——这也是 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">Ch3</a> 必须自己写 WebSocket 服务器的根本原因。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="15-从-npapi-插件到沙盒化浏览器">1.5 从 NPAPI 插件到沙盒化浏览器<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#15-%E4%BB%8E-npapi-%E6%8F%92%E4%BB%B6%E5%88%B0%E6%B2%99%E7%9B%92%E5%8C%96%E6%B5%8F%E8%A7%88%E5%99%A8" class="hash-link" aria-label="Direct link to 1.5 从 NPAPI 插件到沙盒化浏览器" title="Direct link to 1.5 从 NPAPI 插件到沙盒化浏览器" translate="no">​</a></h3>
<!-- -->
<table><thead><tr><th style="text-align:left">阶段</th><th style="text-align:left">技术栈</th><th style="text-align:left">问题</th></tr></thead><tbody><tr><td style="text-align:left">Gmail 视频聊天</td><td style="text-align:left">GIPS + Vidyo + libjingle + NPAPI</td><td style="text-align:left">三套 API、插件安全风险</td></tr><tr><td style="text-align:left">Chrome 原生 WebRTC</td><td style="text-align:left">统一栈 + 沙盒进程</td><td style="text-align:left">默认加密、无插件依赖</td></tr><tr><td style="text-align:left">W3C Recommendation</td><td style="text-align:left">跨浏览器标准 API</td><td style="text-align:left">Firefox/Safari 独立实现</td></tr></tbody></table>
<p>Justin Uberti（Google 工程师，后领导 WebRTC 团队）在整合 Gmail 视频聊天时的核心贡献，是把三个异构子系统拼成<strong>一个可用的产品</strong>——这段「集成噩梦」直接催生了「一次性解决所有集成问题」的 WebRTC 目标。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="16-maastricht-2010跨公司午餐会">1.6 Maastricht 2010：跨公司午餐会<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#16-maastricht-2010%E8%B7%A8%E5%85%AC%E5%8F%B8%E5%8D%88%E9%A4%90%E4%BC%9A" class="hash-link" aria-label="Direct link to 1.6 Maastricht 2010：跨公司午餐会" title="Direct link to 1.6 Maastricht 2010：跨公司午餐会" translate="no">​</a></h3>
<p>2010 年夏天，在荷兰 Maastricht 的一次非正式午餐会上，Google、Cisco、Ericsson、Skype、Mozilla、Linden Labs 等公司的工程师聚在一起讨论「浏览器里的实时通信应该是什么样」。档案可在 <a href="https://rtc-web.alvestrand.com/" target="_blank" rel="noopener noreferrer" class="">rtc-web.alvestrand.com</a> 查阅。</p>
<p>这次会议的意义：</p>
<ul>
<li class=""><strong>跨厂商共识</strong>：不是 Google 一家说了算，而是行业共同方向</li>
<li class=""><strong>Skype 贡献 Opus</strong>：Skype 已在 IETF 完成 Opus 音频 Codec 标准化，为 WebRTC 音频奠定基础</li>
<li class=""><strong>Harald Alvestrand 启动 IETF 流程</strong>：Google 的 Harald 此前已有丰富 IETF 经验，推动 RTCWeb 工作组</li>
</ul>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二webrtc-解决什么问题">二、WebRTC 解决什么问题<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#%E4%BA%8Cwebrtc-%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88%E9%97%AE%E9%A2%98" class="hash-link" aria-label="Direct link to 二、WebRTC 解决什么问题" title="Direct link to 二、WebRTC 解决什么问题" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-实时通信-vs-流媒体">2.1 实时通信 vs 流媒体<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#21-%E5%AE%9E%E6%97%B6%E9%80%9A%E4%BF%A1-vs-%E6%B5%81%E5%AA%92%E4%BD%93" class="hash-link" aria-label="Direct link to 2.1 实时通信 vs 流媒体" title="Direct link to 2.1 实时通信 vs 流媒体" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">维度</th><th style="text-align:left">WebRTC</th><th style="text-align:left">WebSocket + HLS/DASH</th></tr></thead><tbody><tr><td style="text-align:left"><strong>传输</strong></td><td style="text-align:left">UDP 为主（RTP over DTLS）</td><td style="text-align:left">TCP（HTTP）</td></tr><tr><td style="text-align:left"><strong>延迟</strong></td><td style="text-align:left">100–300ms 级</td><td style="text-align:left">3–30s（分片缓冲）</td></tr><tr><td style="text-align:left"><strong>交互</strong></td><td style="text-align:left">双向、多路、可 P2P</td><td style="text-align:left">主要是单向拉流</td></tr><tr><td style="text-align:left"><strong>典型场景</strong></td><td style="text-align:left">视频会议、连麦、远程桌面、AI 语音</td><td style="text-align:left">直播观看、点播</td></tr></tbody></table>
<p>WebRTC 的核心价值是：<strong>在不可控的公网环境下，让两个或多个端点之间建立低延迟、加密的实时媒体通道</strong>。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="22-三代-rtc-架构演进">2.2 三代 RTC 架构演进<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#22-%E4%B8%89%E4%BB%A3-rtc-%E6%9E%B6%E6%9E%84%E6%BC%94%E8%BF%9B" class="hash-link" aria-label="Direct link to 2.2 三代 RTC 架构演进" title="Direct link to 2.2 三代 RTC 架构演进" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">架构</th><th style="text-align:left">工作方式</th><th style="text-align:left">延迟</th><th style="text-align:left">服务端 CPU</th><th style="text-align:left">适用规模</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Full Mesh P2P</strong></td><td style="text-align:left">每对参与者直连</td><td style="text-align:left">最低</td><td style="text-align:left">无媒体处理</td><td style="text-align:left">≤4 人</td></tr><tr><td style="text-align:left"><strong>MCU</strong></td><td style="text-align:left">服务端混流后分发</td><td style="text-align:left">较高</td><td style="text-align:left">极高（转码）</td><td style="text-align:left">传统电话会议</td></tr><tr><td style="text-align:left"><strong>SFU</strong></td><td style="text-align:left">服务端选择性转发，不转码</td><td style="text-align:left">低</td><td style="text-align:left">低且可预测</td><td style="text-align:left">10–10,000+ 人</td></tr></tbody></table>
<p>本系列前半段聚焦 <strong>P2P 原理</strong>（Ch1–Ch6），后半段进入 <strong>SFU 生产架构</strong>（Ch10–Ch15）。理解 P2P 是理解 SFU 的基础——SFU 本质上是在帮每个参与者维护多条「逻辑上的 P2P 订阅关系」。</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="23-webrtc-不适用场景">2.3 WebRTC 不适用场景<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#23-webrtc-%E4%B8%8D%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF" class="hash-link" aria-label="Direct link to 2.3 WebRTC 不适用场景" title="Direct link to 2.3 WebRTC 不适用场景" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">场景</th><th style="text-align:left">为何不适合</th><th style="text-align:left">替代方案</th></tr></thead><tbody><tr><td style="text-align:left">万人单向直播</td><td style="text-align:left">P2P/SFU 无法支撑分发规模</td><td style="text-align:left">HLS/DASH + CDN</td></tr><tr><td style="text-align:left">高延迟可接受</td><td style="text-align:left">WebRTC 复杂度不值得</td><td style="text-align:left">WebSocket + 预录流</td></tr><tr><td style="text-align:left">仅文本聊天</td><td style="text-align:left">杀鸡用牛刀</td><td style="text-align:left">WebSocket / SSE</td></tr><tr><td style="text-align:left">需要服务端录制混流</td><td style="text-align:left">需额外 Egress 组件</td><td style="text-align:left">LiveKit Egress / FFmpeg</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三w3c-与-ietf两套标准的分工">三、W3C 与 IETF：两套标准的分工<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#%E4%B8%89w3c-%E4%B8%8E-ietf%E4%B8%A4%E5%A5%97%E6%A0%87%E5%87%86%E7%9A%84%E5%88%86%E5%B7%A5" class="hash-link" aria-label="Direct link to 三、W3C 与 IETF：两套标准的分工" title="Direct link to 三、W3C 与 IETF：两套标准的分工" translate="no">​</a></h2>
<!-- -->
<p><strong>关键理解</strong>：你在 JavaScript 里调用的是 W3C API；浏览器内部（C++）实现 IETF 协议。你<strong>不需要</strong>手动构造 RTP 包或 DTLS 握手——但需要理解这些层在做什么，才能 Debug。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四webrtc-协议栈全景">四、WebRTC 协议栈全景<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#%E5%9B%9Bwebrtc-%E5%8D%8F%E8%AE%AE%E6%A0%88%E5%85%A8%E6%99%AF" class="hash-link" aria-label="Direct link to 四、WebRTC 协议栈全景" title="Direct link to 四、WebRTC 协议栈全景" translate="no">​</a></h2>
<p>WebRTC 由 <strong>W3C</strong>（浏览器 API）和 <strong>IETF</strong>（底层协议）两套标准共同定义。<a href="https://datatracker.ietf.org/doc/html/rfc8825" target="_blank" rel="noopener noreferrer" class="">RFC 8825</a> 是总览文档，推荐阅读。</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-各层职责速查">4.1 各层职责速查<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#41-%E5%90%84%E5%B1%82%E8%81%8C%E8%B4%A3%E9%80%9F%E6%9F%A5" class="hash-link" aria-label="Direct link to 4.1 各层职责速查" title="Direct link to 4.1 各层职责速查" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">层级</th><th style="text-align:left">标准</th><th style="text-align:left">职责</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Signaling</strong></td><td style="text-align:left">无标准（应用自定义）</td><td style="text-align:left">交换 SDP、ICE Candidate、Room 状态</td></tr><tr><td style="text-align:left"><strong>JSEP</strong></td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8829" target="_blank" rel="noopener noreferrer" class="">RFC 8829</a></td><td style="text-align:left">Offer/Answer 协商模型</td></tr><tr><td style="text-align:left"><strong>SDP</strong></td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8866" target="_blank" rel="noopener noreferrer" class="">RFC 8866</a></td><td style="text-align:left">会话描述：编解码器、端口、ICE 参数</td></tr><tr><td style="text-align:left"><strong>ICE</strong></td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8445" target="_blank" rel="noopener noreferrer" class="">RFC 8445</a></td><td style="text-align:left">在多条网络路径中选最优连通路径</td></tr><tr><td style="text-align:left"><strong>STUN</strong></td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc5389" target="_blank" rel="noopener noreferrer" class="">RFC 5389</a></td><td style="text-align:left">发现公网 IP/端口</td></tr><tr><td style="text-align:left"><strong>TURN</strong></td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc5766" target="_blank" rel="noopener noreferrer" class="">RFC 5766</a></td><td style="text-align:left">NAT 穿透失败时的中继</td></tr><tr><td style="text-align:left"><strong>DTLS</strong></td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc6347" target="_blank" rel="noopener noreferrer" class="">RFC 6347</a></td><td style="text-align:left">UDP 上的 TLS，密钥协商</td></tr><tr><td style="text-align:left"><strong>SRTP</strong></td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc3711" target="_blank" rel="noopener noreferrer" class="">RFC 3711</a></td><td style="text-align:left">媒体流加密</td></tr><tr><td style="text-align:left"><strong>SCTP</strong></td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8831" target="_blank" rel="noopener noreferrer" class="">RFC 8831</a></td><td style="text-align:left">Data Channel 多路复用</td></tr><tr><td style="text-align:left"><strong>RTP/RTCP</strong></td><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc3550" target="_blank" rel="noopener noreferrer" class="">RFC 3550</a></td><td style="text-align:left">媒体包传输与 QoS 反馈</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>关键认知</div><div class="admonitionContent_BuS1"><p><strong>Signaling 不在 WebRTC 标准内。</strong> 浏览器 API 只负责建立连接，你必须自己实现信令通道（WebSocket 最常见）来交换 SDP 和 ICE Candidate。这是新手第一个坑。</p></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-w3c-api-与-ietf-协议对照">4.2 W3C API 与 IETF 协议对照<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#42-w3c-api-%E4%B8%8E-ietf-%E5%8D%8F%E8%AE%AE%E5%AF%B9%E7%85%A7" class="hash-link" aria-label="Direct link to 4.2 W3C API 与 IETF 协议对照" title="Direct link to 4.2 W3C API 与 IETF 协议对照" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">你写的代码</th><th style="text-align:left">触发的协议行为</th></tr></thead><tbody><tr><td style="text-align:left"><code>getUserMedia()</code></td><td style="text-align:left">捕获 MediaStreamTrack，不涉及网络</td></tr><tr><td style="text-align:left"><code>pc.addTrack()</code></td><td style="text-align:left">创建 RTCRtpSender，SDP 中出现新 m-line</td></tr><tr><td style="text-align:left"><code>pc.createOffer()</code></td><td style="text-align:left">生成 SDP Offer（JSEP）</td></tr><tr><td style="text-align:left"><code>pc.setLocalDescription()</code></td><td style="text-align:left">开始 ICE Gathering</td></tr><tr><td style="text-align:left"><code>pc.onicecandidate</code></td><td style="text-align:left">产出 ICE Candidate</td></tr><tr><td style="text-align:left"><code>pc.setRemoteDescription()</code></td><td style="text-align:left">开始 ICE Connectivity Check</td></tr><tr><td style="text-align:left"><code>pc.createDataChannel()</code></td><td style="text-align:left">协商 SCTP over DTLS</td></tr></tbody></table>
<hr>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="43-协议层依赖关系">4.3 协议层依赖关系<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#43-%E5%8D%8F%E8%AE%AE%E5%B1%82%E4%BE%9D%E8%B5%96%E5%85%B3%E7%B3%BB" class="hash-link" aria-label="Direct link to 4.3 协议层依赖关系" title="Direct link to 4.3 协议层依赖关系" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="44-完整-rfc-索引本系列引用">4.4 完整 RFC 索引（本系列引用）<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#44-%E5%AE%8C%E6%95%B4-rfc-%E7%B4%A2%E5%BC%95%E6%9C%AC%E7%B3%BB%E5%88%97%E5%BC%95%E7%94%A8" class="hash-link" aria-label="Direct link to 4.4 完整 RFC 索引（本系列引用）" title="Direct link to 4.4 完整 RFC 索引（本系列引用）" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">RFC</th><th style="text-align:left">标题</th><th style="text-align:left">对应章节</th></tr></thead><tbody><tr><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8825" target="_blank" rel="noopener noreferrer" class="">8825</a></td><td style="text-align:left">WebRTC Overview</td><td style="text-align:left">Ch0</td></tr><tr><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8829" target="_blank" rel="noopener noreferrer" class="">8829</a></td><td style="text-align:left">JSEP</td><td style="text-align:left">Ch2, Ch4</td></tr><tr><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8866" target="_blank" rel="noopener noreferrer" class="">8866</a></td><td style="text-align:left">SDP</td><td style="text-align:left">Ch4</td></tr><tr><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8445" target="_blank" rel="noopener noreferrer" class="">8445</a></td><td style="text-align:left">ICE</td><td style="text-align:left">Ch5</td></tr><tr><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc5389" target="_blank" rel="noopener noreferrer" class="">5389</a></td><td style="text-align:left">STUN</td><td style="text-align:left">Ch5</td></tr><tr><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc5766" target="_blank" rel="noopener noreferrer" class="">5766</a></td><td style="text-align:left">TURN</td><td style="text-align:left">Ch5, Ch14</td></tr><tr><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc6347" target="_blank" rel="noopener noreferrer" class="">6347</a></td><td style="text-align:left">DTLS</td><td style="text-align:left">Ch7</td></tr><tr><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc5764" target="_blank" rel="noopener noreferrer" class="">5764</a></td><td style="text-align:left">DTLS-SRTP</td><td style="text-align:left">Ch7</td></tr><tr><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc3711" target="_blank" rel="noopener noreferrer" class="">3711</a></td><td style="text-align:left">SRTP</td><td style="text-align:left">Ch7</td></tr><tr><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8827" target="_blank" rel="noopener noreferrer" class="">8827</a></td><td style="text-align:left">WebRTC Security</td><td style="text-align:left">Ch7</td></tr><tr><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc3550" target="_blank" rel="noopener noreferrer" class="">3550</a></td><td style="text-align:left">RTP/RTCP</td><td style="text-align:left">Ch8</td></tr><tr><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc7587" target="_blank" rel="noopener noreferrer" class="">7587</a></td><td style="text-align:left">Opus</td><td style="text-align:left">Ch9</td></tr><tr><td style="text-align:left"><a href="https://datatracker.ietf.org/doc/html/rfc8831" target="_blank" rel="noopener noreferrer" class="">8831</a></td><td style="text-align:left">Data Channels</td><td style="text-align:left">Ch6</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五一次-p2p-通话的完整时序">五、一次 P2P 通话的完整时序<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#%E4%BA%94%E4%B8%80%E6%AC%A1-p2p-%E9%80%9A%E8%AF%9D%E7%9A%84%E5%AE%8C%E6%95%B4%E6%97%B6%E5%BA%8F" class="hash-link" aria-label="Direct link to 五、一次 P2P 通话的完整时序" title="Direct link to 五、一次 P2P 通话的完整时序" translate="no">​</a></h2>
<p>在写任何代码之前，先理解端到端流程（信令用 WebSocket 举例）：</p>
<!-- -->
<p>Phase 1（<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">Ch1</a>–<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">Ch2</a>）将实现这个流程；Phase 2（Ch3–Ch6）将把每一步拆解开。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-连接建立四阶段">5.1 连接建立四阶段<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#51-%E8%BF%9E%E6%8E%A5%E5%BB%BA%E7%AB%8B%E5%9B%9B%E9%98%B6%E6%AE%B5" class="hash-link" aria-label="Direct link to 5.1 连接建立四阶段" title="Direct link to 5.1 连接建立四阶段" translate="no">​</a></h3>
<!-- -->
<table><thead><tr><th style="text-align:left">阶段</th><th style="text-align:left">成功标志</th><th style="text-align:left">失败常见原因</th></tr></thead><tbody><tr><td style="text-align:left">信令</td><td style="text-align:left">Offer/Answer 交换完成</td><td style="text-align:left">WebSocket 断开、SDP 格式错误</td></tr><tr><td style="text-align:left">ICE</td><td style="text-align:left"><code>iceConnectionState=connected</code></td><td style="text-align:left">NAT 对称、无 TURN</td></tr><tr><td style="text-align:left">DTLS</td><td style="text-align:left"><code>connectionState=connected</code></td><td style="text-align:left">fingerprint 不匹配</td></tr><tr><td style="text-align:left">SRTP</td><td style="text-align:left"><code>ontrack</code> 触发、有画面</td><td style="text-align:left">Codec 无交集、防火墙</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六适用场景与选型建议">六、适用场景与选型建议<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#%E5%85%AD%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF%E4%B8%8E%E9%80%89%E5%9E%8B%E5%BB%BA%E8%AE%AE" class="hash-link" aria-label="Direct link to 六、适用场景与选型建议" title="Direct link to 六、适用场景与选型建议" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">场景</th><th style="text-align:left">推荐架构</th><th style="text-align:left">本系列对应章节</th></tr></thead><tbody><tr><td style="text-align:left">1v1 视频通话</td><td style="text-align:left">Full Mesh P2P</td><td style="text-align:left">Ch2</td></tr><tr><td style="text-align:left">3–8 人会议</td><td style="text-align:left">SFU</td><td style="text-align:left">Ch12</td></tr><tr><td style="text-align:left">大型直播（万人观看）</td><td style="text-align:left">CDN + 可选连麦 SFU</td><td style="text-align:left">Ch11, Ch12</td></tr><tr><td style="text-align:left">实时白板 / 游戏状态</td><td style="text-align:left">Data Channel</td><td style="text-align:left">Ch6</td></tr><tr><td style="text-align:left">AI 语音助手</td><td style="text-align:left">SFU + Agent 框架</td><td style="text-align:left">Ch15 + <a class="" href="https://rainlib.vercel.app/en/recommend/livekit-introduction">LiveKit 介绍</a></td></tr><tr><td style="text-align:left">纯单向直播</td><td style="text-align:left">HLS/WebRTC 推流 Ingress</td><td style="text-align:left">超出本系列，参考 LiveKit Ingress</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七常见误解-faq">七、常见误解 FAQ<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#%E4%B8%83%E5%B8%B8%E8%A7%81%E8%AF%AF%E8%A7%A3-faq" class="hash-link" aria-label="Direct link to 七、常见误解 FAQ" title="Direct link to 七、常见误解 FAQ" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">误解</th><th style="text-align:left">真相</th></tr></thead><tbody><tr><td style="text-align:left">「WebRTC 是 P2P，不需要服务器」</td><td style="text-align:left">需要<strong>信令服务器</strong>；多数场景还需要 <strong>TURN</strong> 和 <strong>SFU</strong></td></tr><tr><td style="text-align:left">「WebRTC 可以替代 WebSocket」</td><td style="text-align:left">WebSocket 适合信令和控制；WebRTC 适合低延迟媒体</td></tr><tr><td style="text-align:left">「只要 getUserMedia 就能视频通话」</td><td style="text-align:left">还需要 PeerConnection + 信令 + ICE</td></tr><tr><td style="text-align:left">「WebRTC 完全免费」</td><td style="text-align:left">开源免费，但 TURN 带宽、SFU 服务器有运维成本</td></tr><tr><td style="text-align:left">「所有浏览器 WebRTC 行为一致」</td><td style="text-align:left">有差异，建议用 <a href="https://github.com/webrtc/adapter" target="_blank" rel="noopener noreferrer" class="">adapter.js</a></td></tr><tr><td style="text-align:left">「Simulcast 是 WebRTC 标准」</td><td style="text-align:left">Simulcast 是广泛部署的扩展，SFU 必须支持</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八实战-lab打开调试工具">八、实战 Lab：打开调试工具<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#%E5%85%AB%E5%AE%9E%E6%88%98-lab%E6%89%93%E5%BC%80%E8%B0%83%E8%AF%95%E5%B7%A5%E5%85%B7" class="hash-link" aria-label="Direct link to 八、实战 Lab：打开调试工具" title="Direct link to 八、实战 Lab：打开调试工具" translate="no">​</a></h2>
<p>本篇无需写代码，但请完成以下准备：</p>
<ol>
<li class="">使用 Chrome 打开 <code>chrome://webrtc-internals</code></li>
<li class="">访问 <a href="https://webrtc.github.io/samples/src/content/peerconnection/pc1/" target="_blank" rel="noopener noreferrer" class="">WebRTC Samples</a> 建立一个测试连接</li>
<li class="">在 webrtc-internals 中观察：<!-- -->
<ul>
<li class=""><code>RTCPeerConnection</code> 列表</li>
<li class="">ICE candidate pair 状态</li>
<li class="">Stats 面板中的 <code>inbound-rtp</code> / <code>outbound-rtp</code></li>
</ul>
</li>
</ol>
<p>这些面板将在 <a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">Ch13</a> 成为生产排障的核心工具。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="81-webrtc-internals-观察清单">8.1 webrtc-internals 观察清单<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#81-webrtc-internals-%E8%A7%82%E5%AF%9F%E6%B8%85%E5%8D%95" class="hash-link" aria-label="Direct link to 8.1 webrtc-internals 观察清单" title="Direct link to 8.1 webrtc-internals 观察清单" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">观察项</th><th style="text-align:left">位置</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">PeerConnection 列表</td><td style="text-align:left">首页</td><td style="text-align:left">每个 Tab 一个 PC</td></tr><tr><td style="text-align:left">ICE candidate pair</td><td style="text-align:left">ICE 标签</td><td style="text-align:left"><code>state=succeeded</code> 的行</td></tr><tr><td style="text-align:left">Local/Remote SDP</td><td style="text-align:left">SDP 标签</td><td style="text-align:left">对比 Offer/Answer</td></tr><tr><td style="text-align:left">Stats graphs</td><td style="text-align:left">Graphs 标签</td><td style="text-align:left">码率、丢包实时曲线</td></tr><tr><td style="text-align:left">getUserMedia</td><td style="text-align:left">媒体标签</td><td style="text-align:left">采集分辨率与帧率</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九本章小结与下一篇预告">九、本章小结与下一篇预告<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#%E4%B9%9D%E6%9C%AC%E7%AB%A0%E5%B0%8F%E7%BB%93%E4%B8%8E%E4%B8%8B%E4%B8%80%E7%AF%87%E9%A2%84%E5%91%8A" class="hash-link" aria-label="Direct link to 九、本章小结与下一篇预告" title="Direct link to 九、本章小结与下一篇预告" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">维度</th><th style="text-align:left">核心要点</th></tr></thead><tbody><tr><td style="text-align:left"><strong>定位</strong></td><td style="text-align:left">浏览器端低延迟、加密、P2P 实时媒体框架</td></tr><tr><td style="text-align:left"><strong>标准</strong></td><td style="text-align:left">W3C 定义 API，IETF 定义协议栈</td></tr><tr><td style="text-align:left"><strong>关键层</strong></td><td style="text-align:left">Signaling → JSEP/SDP → ICE → DTLS → SRTP → Codec</td></tr><tr><td style="text-align:left"><strong>架构</strong></td><td style="text-align:left">P2P 适合小规模；SFU 是生产会议的主流</td></tr><tr><td style="text-align:left"><strong>学习路径</strong></td><td style="text-align:left">先跑通 P2P，再深入每层协议，最后上 SFU</td></tr></tbody></table>
<p><strong>下一篇（Ch1）</strong> 我们从最顶层 API 开始：<a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api"><code>getUserMedia</code></a> 如何获取摄像头和麦克风，如何切换设备和处理权限。</p>
<hr>
<blockquote>
<p><strong>系列导航</strong></p>
<table><thead><tr><th style="text-align:center">章节</th><th style="text-align:left">主题</th><th style="text-align:center">状态</th></tr></thead><tbody><tr><td style="text-align:center">0</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-architecture">架构全景与协议栈地图</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">1</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-browser-api">浏览器媒体 API 与设备管理</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">2</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-first-p2p-call">第一个 P2P 视频通话</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">3</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-signaling">信令服务器设计与会话状态机</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">4</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sdp-deep-dive">SDP 解剖与媒体协商</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">5</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-ice-stun-turn">ICE、STUN、TURN 与 NAT 穿透</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">6</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-data-channel">Data Channel 与 SCTP over DTLS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">7</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-dtls-srtp">DTLS 握手与 SRTP 加密体系</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">8</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-rtp-rtcp">RTP/RTCP 媒体传输与 QoS</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">9</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-codecs-simulcast-intro">音视频编解码与 Simulcast 入门</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">10</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-bandwidth-estimation">带宽估计与拥塞控制 GCC</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">11</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-simulcast-svc">Simulcast、SVC 与选择性订阅</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">12</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-sfu-architecture">SFU/MCU/Mesh 架构与 Pion 实战</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">13</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-debugging-observability">调试工具链与可观测性</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">14</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-scale-turn-production">TURN 集群部署与多区域扩展</a></td><td style="text-align:center">✅ 已发布</td></tr><tr><td style="text-align:center">15</td><td style="text-align:left"><a class="" href="https://rainlib.vercel.app/en/blog/webrtc-capstone-video-conference">Capstone 生产级视频会议系统</a></td><td style="text-align:center">✅ 已发布</td></tr></tbody></table>
</blockquote>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="references">References<a href="https://rainlib.vercel.app/en/blog/webrtc-architecture#references" class="hash-link" aria-label="Direct link to References" title="Direct link to References" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://datatracker.ietf.org/doc/html/rfc8825" target="_blank" rel="noopener noreferrer" class="">RFC 8825 — Overview: Real-Time Protocols for Browser-Based Applications</a></li>
<li class=""><a href="https://www.w3.org/TR/webrtc/" target="_blank" rel="noopener noreferrer" class="">W3C WebRTC Recommendation</a></li>
<li class=""><a href="https://webrtcforthecurious.com/zh/docs/10-history-of-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — 历史（含 RTP/WebRTC 作者访谈）</a></li>
<li class=""><a href="https://webrtcforthecurious.com/docs/01-what-is-webrtc/" target="_blank" rel="noopener noreferrer" class="">WebRTC for the Curious — Introduction</a></li>
<li class=""><a href="https://rtc-web.alvestrand.com/" target="_blank" rel="noopener noreferrer" class="">Maastricht 2010 RTC-Web 午餐会档案</a></li>
<li class=""><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API" target="_blank" rel="noopener noreferrer" class="">MDN — WebRTC API</a></li>
<li class=""><a href="https://webrtc.org/getting-started/overview" target="_blank" rel="noopener noreferrer" class="">webrtc.org — Getting Started</a></li>
<li class=""><a href="https://hpbn.co/webrtc/" target="_blank" rel="noopener noreferrer" class="">High Performance Browser Networking — WebRTC (Ilya Grigorik)</a></li>
</ul>]]></content:encoded>
            <category>WebRTC</category>
            <category>实时通信</category>
            <category>Real-Time Communication</category>
            <category>Deep Dive</category>
            <category>Architecture</category>
        </item>
        <item>
            <title><![CDATA[从 AI 糊墙到大师级 UI：高星前端 Agent Skills 终极精选与一键安装指南]]></title>
            <link>https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend</link>
            <guid>https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend</guid>
            <pubDate>Mon, 08 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[按 UI 设计规范、设计系统、框架技能、性能体验四大类型，盘点 frontend-design、impeccable、vercel-labs/skills、pixijs-skills 等高星前端 Agent Skills，附场景化选型与 npx 一键安装指南。]]></description>
            <content:encoded><![CDATA[<p>AI 写前端很快，但默认产出往往「能跑、能看、却一眼 AI 味」——紫色渐变、Inter 字体、嵌套灰卡片、inline 样式、过时的框架 API。问题不在模型能力，而在<strong>缺少分层约束</strong>：该管审美的没管审美，该管框架写法的没管写法，该管性能的没管性能。</p>
<p><strong>Agent Skills</strong> 就是给 AI 的分层外挂：UI 规范管视觉，设计系统管组件与合规，框架技能管技术栈正确性，性能技能管 Web Vitals。本文按这四类体系整理 7 个高星前端 Skill，并给出场景化组合与 <code>npx skills add</code> 安装方式。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="agent-skills-是什么">Agent Skills 是什么？<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#agent-skills-%E6%98%AF%E4%BB%80%E4%B9%88" class="hash-link" aria-label="Direct link to Agent Skills 是什么？" title="Direct link to Agent Skills 是什么？" translate="no">​</a></h2>
<p>Skill 是一个含 <code>SKILL.md</code> 的文件夹，向 AI 注入<strong>可渐进加载</strong>的领域知识：</p>
<table><thead><tr><th style="text-align:left">组成部分</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Metadata &amp; Instructions</strong></td><td style="text-align:left">定义激活场景与设计/编码准则</td></tr><tr><td style="text-align:left"><strong>Steering 命令</strong></td><td style="text-align:left">如 <code>/audit</code>、<code>/typeset</code>，发送高层设计意图</td></tr><tr><td style="text-align:left"><strong>Scripts &amp; Examples</strong></td><td style="text-align:left">调用本地工具或固定输出模板</td></tr></tbody></table>
<p>与整段 System Prompt 不同，Skills 遵循<strong>渐进式披露</strong>——任务匹配时才加载，省 Token、保上下文干净。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="前端-skills-的四种类型">前端 Skills 的四种类型<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#%E5%89%8D%E7%AB%AF-skills-%E7%9A%84%E5%9B%9B%E7%A7%8D%E7%B1%BB%E5%9E%8B" class="hash-link" aria-label="Direct link to 前端 Skills 的四种类型" title="Direct link to 前端 Skills 的四种类型" translate="no">​</a></h2>
<p>先把 Skill 按<strong>职责</strong>分清楚，再谈安装。混装无效，分层叠加才有效。</p>
<!-- -->
<table><thead><tr><th style="text-align:left">类型</th><th style="text-align:left">解决什么问题</th><th style="text-align:left">典型 Skill</th><th style="text-align:left">何时必装</th></tr></thead><tbody><tr><td style="text-align:left"><strong>UI/UX 设计规范</strong></td><td style="text-align:left">AI 糊墙 UI、审美同质化、缺乏设计意图</td><td style="text-align:left"><code>frontend-design</code>、<code>impeccable</code>、<code>taste-skill</code></td><td style="text-align:left">任何面向用户的界面</td></tr><tr><td style="text-align:left"><strong>设计系统与无障碍</strong></td><td style="text-align:left">组件范式缺失、a11y 不合规、B 端复杂度</td><td style="text-align:left"><code>ui-ux-pro-max-skill</code></td><td style="text-align:left">后台、仪表盘、数据大屏</td></tr><tr><td style="text-align:left"><strong>框架 / 工具链技能</strong></td><td style="text-align:left">框架 API 写错、工程结构混乱</td><td style="text-align:left"><code>vercel-labs/skills</code>、<code>pixijs-skills</code></td><td style="text-align:left">按技术栈选装</td></tr><tr><td style="text-align:left"><strong>性能与体验</strong></td><td style="text-align:left">LCP/INP/CLS 不达标</td><td style="text-align:left"><code>addyosmani/agent-skills</code></td><td style="text-align:left">C 端高流量页面</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="全景索引">全景索引<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#%E5%85%A8%E6%99%AF%E7%B4%A2%E5%BC%95" class="hash-link" aria-label="Direct link to 全景索引" title="Direct link to 全景索引" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">类型</th><th style="text-align:left">Skill</th><th style="text-align:center">维护方</th><th style="text-align:left">一句话</th></tr></thead><tbody><tr><td style="text-align:left">UI 规范</td><td style="text-align:left"><a href="https://github.com/anthropics/skills/tree/main/skills/frontend-design" target="_blank" rel="noopener noreferrer" class=""><code>frontend-design</code></a></td><td style="text-align:center">官方</td><td style="text-align:left">确立独特视觉方向，打破 AI 模版审美</td></tr><tr><td style="text-align:left">UI 规范</td><td style="text-align:left"><a href="https://github.com/pbakaus/impeccable" target="_blank" rel="noopener noreferrer" class=""><code>impeccable</code></a></td><td style="text-align:center">社区</td><td style="text-align:left">23+ Steering 命令，虚拟创意总监</td></tr><tr><td style="text-align:left">UI 规范</td><td style="text-align:left"><a href="https://github.com/Leonxlnx/taste-skill" target="_blank" rel="noopener noreferrer" class=""><code>taste-skill</code></a></td><td style="text-align:center">社区</td><td style="text-align:left">设计意图推理 + 灵感配图</td></tr><tr><td style="text-align:left">设计系统</td><td style="text-align:left"><a href="https://github.com/nextlevelbuilder/ui-ux-pro-max-skill" target="_blank" rel="noopener noreferrer" class=""><code>ui-ux-pro-max-skill</code></a></td><td style="text-align:center">社区</td><td style="text-align:left">50+ 组件范式、a11y 审计、配色字体库</td></tr><tr><td style="text-align:left">框架技能</td><td style="text-align:left"><a href="https://github.com/vercel-labs/skills" target="_blank" rel="noopener noreferrer" class=""><code>vercel-labs/skills</code></a></td><td style="text-align:center">官方</td><td style="text-align:left">React / Next.js 工程最佳实践</td></tr><tr><td style="text-align:left">框架技能</td><td style="text-align:left"><a href="https://github.com/pixijs/pixijs-skills" target="_blank" rel="noopener noreferrer" class=""><code>pixijs-skills</code></a></td><td style="text-align:center">官方</td><td style="text-align:left">PixiJS v8 共 25 项细分技能</td></tr><tr><td style="text-align:left">性能体验</td><td style="text-align:left"><a href="https://github.com/addyosmani/agent-skills" target="_blank" rel="noopener noreferrer" class=""><code>addyosmani/agent-skills</code></a></td><td style="text-align:center">社区</td><td style="text-align:left">Core Web Vitals 深度优化</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一uiux-设计规范">一、UI/UX 设计规范<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#%E4%B8%80uiux-%E8%AE%BE%E8%AE%A1%E8%A7%84%E8%8C%83" class="hash-link" aria-label="Direct link to 一、UI/UX 设计规范" title="Direct link to 一、UI/UX 设计规范" translate="no">​</a></h2>
<blockquote>
<p><strong>管什么</strong>：视觉方向、排版、配色、动效、反 AI 糊墙。<br>
<strong>不管什么</strong>：React 组件结构、PixiJS API、Bundle 体积。<br>
<strong>推荐叠法</strong>：<code>frontend-design</code> 作底座 → <code>impeccable</code> 精修 → 新项目加 <code>taste-skill</code> 推风格。</p>
</blockquote>
<hr>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="anthropicsfrontend-design"><code>anthropics/frontend-design</code><a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#anthropicsfrontend-design" class="hash-link" aria-label="Direct link to anthropicsfrontend-design" title="Direct link to anthropicsfrontend-design" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left"></th><th style="text-align:left"></th></tr></thead><tbody><tr><td style="text-align:left"><strong>类型</strong></td><td style="text-align:left">UI/UX 设计规范</td></tr><tr><td style="text-align:left"><strong>维护方</strong></td><td style="text-align:left">Anthropic 官方</td></tr><tr><td style="text-align:left"><strong>适用</strong></td><td style="text-align:left">所有需要「不像 AI 做的」界面</td></tr></tbody></table>
<p><strong>解决</strong>：Space Grotesk + 白底紫渐变等模版化套路。</p>
<p><strong>核心能力</strong></p>
<ul>
<li class="">编写代码前强制确立视觉流派（极简、粗野、复古未来等）</li>
<li class="">Display Font + 正文字体搭配，打破 Inter/Arial 泛滥</li>
<li class="">不对称布局、grid-breaking、负空间留白</li>
<li class="">渐变网格、噪点纹理、层叠透明等背景质感</li>
<li class="">首屏 Staggered Reveal，而非杂乱 hover 动效</li>
</ul>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">npx skills </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> anthropics/skills </span><span class="token parameter variable" style="color:#36acaa">--skill</span><span class="token plain"> frontend-design</span><br></div></code></pre></div></div></div>
<hr>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="pbakausimpeccable"><code>pbakaus/impeccable</code><a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#pbakausimpeccable" class="hash-link" aria-label="Direct link to pbakausimpeccable" title="Direct link to pbakausimpeccable" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left"></th><th style="text-align:left"></th></tr></thead><tbody><tr><td style="text-align:left"><strong>类型</strong></td><td style="text-align:left">UI/UX 设计规范</td></tr><tr><td style="text-align:left"><strong>维护方</strong></td><td style="text-align:left">Paul Bakaus（前 Google DevRel、AMP 创建者）</td></tr><tr><td style="text-align:left"><strong>适用</strong></td><td style="text-align:left">页面精修、设计评审、品牌调性落地</td></tr></tbody></table>
<p><strong>解决</strong>：嵌套灰卡片、间距窒息、非黑即白配色。</p>
<p><strong>Steering 命令精选</strong></p>
<table><thead><tr><th style="text-align:left">命令</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:left"><code>/colorize</code></td><td style="text-align:left">品牌配色，杜绝 generic AI 蓝紫</td></tr><tr><td style="text-align:left"><code>/typeset</code></td><td style="text-align:left">字重对比、行高、现代排版</td></tr><tr><td style="text-align:left"><code>/animate</code></td><td style="text-align:left">Framer Motion / CSS 关键帧微交互</td></tr><tr><td style="text-align:left"><code>/audit</code></td><td style="text-align:left">AI 设计评审，诊断低级视觉错误</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>CLI 检测</div><div class="admonitionContent_BuS1"><p>不依赖 AI 也可扫描视觉反模式：<code>npx impeccable detect &lt;path&gt;</code></p></div></div>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">npx skills </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> pbakaus/impeccable</span><br></div></code></pre></div></div></div>
<hr>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="leonxlnxtaste-skill"><code>Leonxlnx/taste-skill</code><a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#leonxlnxtaste-skill" class="hash-link" aria-label="Direct link to leonxlnxtaste-skill" title="Direct link to leonxlnxtaste-skill" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left"></th><th style="text-align:left"></th></tr></thead><tbody><tr><td style="text-align:left"><strong>类型</strong></td><td style="text-align:left">UI/UX 设计规范</td></tr><tr><td style="text-align:left"><strong>维护方</strong></td><td style="text-align:left">社区</td></tr><tr><td style="text-align:left"><strong>适用</strong></td><td style="text-align:left">从零做品牌原型、概念设计</td></tr></tbody></table>
<p><strong>解决</strong>：缺乏设计简报时 AI 乱猜风格。</p>
<p><strong>核心能力</strong></p>
<ol>
<li class=""><strong>设计意图推理</strong> — 读项目简报，推断 Minimalist / Editorial / Enterprise SaaS 等风格</li>
<li class=""><strong>灵感配图</strong> — 生成契合风格的 DALL-E / Midjourney 提示词，再回传 AI 还原</li>
</ol>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">npx skills </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> Leonxlnx/taste-skill</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二设计系统与无障碍">二、设计系统与无障碍<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#%E4%BA%8C%E8%AE%BE%E8%AE%A1%E7%B3%BB%E7%BB%9F%E4%B8%8E%E6%97%A0%E9%9A%9C%E7%A2%8D" class="hash-link" aria-label="Direct link to 二、设计系统与无障碍" title="Direct link to 二、设计系统与无障碍" translate="no">​</a></h2>
<blockquote>
<p><strong>管什么</strong>：组件范式、配色盘、字体搭配、图表配置、a11y 合规。<br>
<strong>与 UI 规范的区别</strong>：UI 规范管「美不美」，设计系统管「全不全、合不合规」。<br>
<strong>典型场景</strong>：企业后台、SaaS 仪表盘、数据可视化大屏。</p>
</blockquote>
<hr>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="nextlevelbuilderui-ux-pro-max-skill"><code>nextlevelbuilder/ui-ux-pro-max-skill</code><a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#nextlevelbuilderui-ux-pro-max-skill" class="hash-link" aria-label="Direct link to nextlevelbuilderui-ux-pro-max-skill" title="Direct link to nextlevelbuilderui-ux-pro-max-skill" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left"></th><th style="text-align:left"></th></tr></thead><tbody><tr><td style="text-align:left"><strong>类型</strong></td><td style="text-align:left">设计系统与无障碍</td></tr><tr><td style="text-align:left"><strong>维护方</strong></td><td style="text-align:left">社区</td></tr><tr><td style="text-align:left"><strong>适用</strong></td><td style="text-align:left">复杂 B 端、多组件页面</td></tr></tbody></table>
<p><strong>内置资产</strong></p>
<table><thead><tr><th style="text-align:left">资产</th><th style="text-align:left">规模</th></tr></thead><tbody><tr><td style="text-align:left">现代组件范式</td><td style="text-align:left">50+</td></tr><tr><td style="text-align:left">精选配色盘</td><td style="text-align:left">21 组</td></tr><tr><td style="text-align:left">字体搭配</td><td style="text-align:left">50 种</td></tr><tr><td style="text-align:left">图表配置范式</td><td style="text-align:left">20+（Recharts、D3.js 等）</td></tr></tbody></table>
<p><strong>无障碍审计</strong>：对比度、ARIA Labels、Focus States、移动端 Touch Targets。</p>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">npx skills </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> nextlevelbuilder/ui-ux-pro-max-skill</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三框架--工具链技能">三、框架 / 工具链技能<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#%E4%B8%89%E6%A1%86%E6%9E%B6--%E5%B7%A5%E5%85%B7%E9%93%BE%E6%8A%80%E8%83%BD" class="hash-link" aria-label="Direct link to 三、框架 / 工具链技能" title="Direct link to 三、框架 / 工具链技能" translate="no">​</a></h2>
<blockquote>
<p><strong>管什么</strong>：特定技术栈的 API 正确性、项目结构、生命周期、迁移路径。<br>
<strong>与 UI 规范的区别</strong>：框架 Skill 不管紫色渐变，只管 <code>useEffect</code> 别写错、<code>app.init()</code> 记得 await。<br>
<strong>选型原则</strong>：按栈选装，不要全装——React 项目装 Vercel，Canvas 项目装 PixiJS。</p>
</blockquote>
<hr>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="vercel-labsskills--react--nextjs"><code>vercel-labs/skills</code> — React / Next.js<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#vercel-labsskills--react--nextjs" class="hash-link" aria-label="Direct link to vercel-labsskills--react--nextjs" title="Direct link to vercel-labsskills--react--nextjs" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left"></th><th style="text-align:left"></th></tr></thead><tbody><tr><td style="text-align:left"><strong>类型</strong></td><td style="text-align:left">框架 / 工具链技能</td></tr><tr><td style="text-align:left"><strong>维护方</strong></td><td style="text-align:left">Vercel Labs</td></tr><tr><td style="text-align:left"><strong>适用</strong></td><td style="text-align:left">React、Next.js、TypeScript 项目</td></tr></tbody></table>
<p><strong>解决</strong>：inline CSS 泛滥、Hooks 误用、类型不规范、SSR 不兼容、目录结构混乱。</p>
<p><strong>核心能力</strong></p>
<ul>
<li class="">组件单一职责与可测性</li>
<li class="">TypeScript 严格类型与 Props 设计</li>
<li class="">SSR / RSC 兼容写法</li>
<li class="">现代 React / Next.js 目录与分包规范</li>
</ul>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">npx skills </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> vercel-labs/skills</span><br></div></code></pre></div></div></div>
<hr>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="pixijspixijs-skills--pixijs-v8--canvas-2d"><code>pixijs/pixijs-skills</code> — PixiJS v8 / Canvas 2D<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#pixijspixijs-skills--pixijs-v8--canvas-2d" class="hash-link" aria-label="Direct link to pixijspixijs-skills--pixijs-v8--canvas-2d" title="Direct link to pixijspixijs-skills--pixijs-v8--canvas-2d" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left"></th><th style="text-align:left"></th></tr></thead><tbody><tr><td style="text-align:left"><strong>类型</strong></td><td style="text-align:left">框架 / 工具链技能</td></tr><tr><td style="text-align:left"><strong>维护方</strong></td><td style="text-align:left">PixiJS 官方 · <a href="https://pixijs.com/llms" target="_blank" rel="noopener noreferrer" class="">pixijs.com/llms</a></td></tr><tr><td style="text-align:left"><strong>适用</strong></td><td style="text-align:left">游戏、粒子特效、Canvas 可视化、WebGL 滤镜</td></tr></tbody></table>
<p><strong>解决</strong>：v7 旧 API、同步 init、Assets 加载错误、Graphics 滥用掉帧、Shader 不符合 v8。</p>
<p><strong>25 项细分 Skill（按模块）</strong></p>
<table><thead><tr><th style="text-align:left">模块</th><th style="text-align:left">代表 Skill</th><th style="text-align:left">覆盖内容</th></tr></thead><tbody><tr><td style="text-align:left">基础</td><td style="text-align:left"><code>pixijs-application</code></td><td style="text-align:left">Application、Ticker、生命周期</td></tr><tr><td style="text-align:left">渲染</td><td style="text-align:left"><code>pixijs-core-concepts</code></td><td style="text-align:left">渲染管线、Scene Graph</td></tr><tr><td style="text-align:left">资源</td><td style="text-align:left"><code>pixijs-assets</code></td><td style="text-align:left">Assets.load、bundles、spritesheet</td></tr><tr><td style="text-align:left">场景</td><td style="text-align:left"><code>pixijs-scene-*</code></td><td style="text-align:left">Sprite、Graphics、Text、Mesh</td></tr><tr><td style="text-align:left">交互</td><td style="text-align:left"><code>pixijs-events</code></td><td style="text-align:left">指针、拖拽、hitArea</td></tr><tr><td style="text-align:left">特效</td><td style="text-align:left"><code>pixijs-filters</code></td><td style="text-align:left">内置 / 自定义 Filter、GLSL/WGSL</td></tr><tr><td style="text-align:left">工程</td><td style="text-align:left"><code>pixijs-performance</code></td><td style="text-align:left">对象池、批处理、剔除</td></tr><tr><td style="text-align:left">迁移</td><td style="text-align:left"><code>pixijs-migration-v8</code></td><td style="text-align:left">v7 → v8 升级路径</td></tr></tbody></table>
<p><strong>安装与集成</strong></p>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># 通用（40+ Agent）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">npx skills </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> https://github.com/pixijs/pixijs-skills</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Cursor：Settings → Rules → Remote Rule → pixijs/pixijs-skills</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Claude Code：/plugin marketplace add pixijs/pixijs-skills</span><br></div></code></pre></div></div></div>
<p><strong>示例 Prompt</strong></p>
<div class="custom-code-block" data-language="text"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Build a PixiJS v8 scene with Assets, Container, Sprite, and ticker-based animation.</span><br></div></code></pre></div></div></div>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>llms.txt 备用</div><div class="admonitionContent_BuS1"><p>工具不支持 Skill 时，可用 <a href="https://pixijs.com/llms.txt" target="_blank" rel="noopener noreferrer" class="">llms.txt</a>（索引）、<code>llms-medium.txt</code>（教程）、<code>llms-full.txt</code>（完整 API）。Copilot 用户复制 <a href="https://raw.githubusercontent.com/pixijs/pixijs-skills/main/.github/copilot-instructions.md" target="_blank" rel="noopener noreferrer" class=""><code>copilot-instructions.md</code></a> 到 <code>.github/</code>。</p></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四性能与体验优化">四、性能与体验优化<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#%E5%9B%9B%E6%80%A7%E8%83%BD%E4%B8%8E%E4%BD%93%E9%AA%8C%E4%BC%98%E5%8C%96" class="hash-link" aria-label="Direct link to 四、性能与体验优化" title="Direct link to 四、性能与体验优化" translate="no">​</a></h2>
<blockquote>
<p><strong>管什么</strong>：Core Web Vitals、渲染管线、加载策略。<br>
<strong>与框架技能的区别</strong>：Vercel Skill 管代码结构，性能 Skill 管 LCP 是否 &lt; 2.5s。<br>
<strong>叠法</strong>：C 端页面 = UI 规范 + 框架技能 + 本类 Skill。</p>
</blockquote>
<hr>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="addyosmaniagent-skills"><code>addyosmani/agent-skills</code><a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#addyosmaniagent-skills" class="hash-link" aria-label="Direct link to addyosmaniagent-skills" title="Direct link to addyosmaniagent-skills" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left"></th><th style="text-align:left"></th></tr></thead><tbody><tr><td style="text-align:left"><strong>类型</strong></td><td style="text-align:left">性能与体验</td></tr><tr><td style="text-align:left"><strong>维护方</strong></td><td style="text-align:left">Addy Osmani（Google Chrome 团队）</td></tr><tr><td style="text-align:left"><strong>适用</strong></td><td style="text-align:left">高流量 C 端、内容密集页面</td></tr></tbody></table>
<p><strong>解决</strong>：LCP 慢、INP 卡顿、CLS 抖动、首屏阻塞。</p>
<p><strong>优化规则</strong></p>
<ul>
<li class=""><code>content-visibility: auto</code> 提升滚动性能</li>
<li class="">Fetch Priority 图片预加载</li>
<li class="">Skeleton Screen 与懒加载</li>
<li class="">渲染逻辑与 bundle 分割</li>
</ul>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">npx skills </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> addyosmani/agent-skills </span><span class="token parameter variable" style="color:#36acaa">--skill</span><span class="token plain"> frontend-ui-engineering</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="场景化选型指南">场景化选型指南<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#%E5%9C%BA%E6%99%AF%E5%8C%96%E9%80%89%E5%9E%8B%E6%8C%87%E5%8D%97" class="hash-link" aria-label="Direct link to 场景化选型指南" title="Direct link to 场景化选型指南" translate="no">​</a></h2>
<p>按项目类型选 Skill，而非按 Star 数堆砌。</p>
<table><thead><tr><th style="text-align:left">你的场景</th><th style="text-align:left">推荐组合</th><th style="text-align:left">安装顺序</th></tr></thead><tbody><tr><td style="text-align:left"><strong>通用 Web 应用</strong></td><td style="text-align:left">UI 规范 + React 框架 + 性能</td><td style="text-align:left"><code>frontend-design</code> → <code>vercel-labs/skills</code> → <code>addyosmani</code></td></tr><tr><td style="text-align:left"><strong>摆脱 AI 糊墙</strong></td><td style="text-align:left">双 UI 规范</td><td style="text-align:left"><code>frontend-design</code> + <code>impeccable</code></td></tr><tr><td style="text-align:left"><strong>品牌原型 / 0→1</strong></td><td style="text-align:left">风格推导 + 美学底座</td><td style="text-align:left"><code>taste-skill</code> + <code>frontend-design</code></td></tr><tr><td style="text-align:left"><strong>企业后台 / 仪表盘</strong></td><td style="text-align:left">设计系统 + UI 规范</td><td style="text-align:left"><code>ui-ux-pro-max-skill</code> + <code>frontend-design</code></td></tr><tr><td style="text-align:left"><strong>Canvas / 游戏 / 粒子</strong></td><td style="text-align:left">PixiJS 框架（按需叠 UI）</td><td style="text-align:left"><code>pixijs-skills</code>（+ 可选 <code>frontend-design</code>）</td></tr><tr><td style="text-align:left"><strong>高并发 C 端</strong></td><td style="text-align:left">全栈四层</td><td style="text-align:left">UI + Vercel + Addy Osmani</td></tr></tbody></table>
<p><strong>默认黄金组合</strong>（多数 React 项目）：</p>
<div class="custom-code-block" data-language="text"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">frontend-design  +  vercel-labs/skills  +  addyosmani/agent-skills</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">     ↑ 好看              ↑ 写对                 ↑ 跑快</span><br></div></code></pre></div></div></div>
<p>B 端加 <code>ui-ux-pro-max-skill</code>；Canvas 项目把 <code>vercel-labs/skills</code> 换成 <code>pixijs-skills</code>。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="安装与管理">安装与管理<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#%E5%AE%89%E8%A3%85%E4%B8%8E%E7%AE%A1%E7%90%86" class="hash-link" aria-label="Direct link to 安装与管理" title="Direct link to 安装与管理" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="项目级-vs-全局">项目级 vs 全局<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#%E9%A1%B9%E7%9B%AE%E7%BA%A7-vs-%E5%85%A8%E5%B1%80" class="hash-link" aria-label="Direct link to 项目级 vs 全局" title="Direct link to 项目级 vs 全局" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">方式</th><th style="text-align:left">路径</th><th style="text-align:left">适用</th></tr></thead><tbody><tr><td style="text-align:left"><strong>项目级（推荐）</strong></td><td style="text-align:left"><code>./.claude/skills/</code> 或 <code>.agent/skills/</code></td><td style="text-align:left">团队共享，随 Git 提交</td></tr><tr><td style="text-align:left"><strong>全局级</strong></td><td style="text-align:left"><code>~/.claude/skills/</code></td><td style="text-align:left">个人跨项目通用</td></tr></tbody></table>
<div class="custom-code-block" data-language="bash"><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># 项目级</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">npx skills </span><span class="token function" style="color:#d73a49">add</span><span class="token plain"> pbakaus/impeccable</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 全局级（手动 clone 示例）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">git</span><span class="token plain"> clone https://github.com/pbakaus/impeccable ~/.claude/skills/impeccable</span><br></div></code></pre></div></div></div>
<p>注册中心：<a href="https://skills.sh/" target="_blank" rel="noopener noreferrer" class="">skills.sh</a></p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="cursor--cline-等非-claude-工具">Cursor / Cline 等非 Claude 工具<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#cursor--cline-%E7%AD%89%E9%9D%9E-claude-%E5%B7%A5%E5%85%B7" class="hash-link" aria-label="Direct link to Cursor / Cline 等非 Claude 工具" title="Direct link to Cursor / Cline 等非 Claude 工具" translate="no">​</a></h3>
<p>Skills 本质是结构化 Markdown + Prompt，迁移成本低：</p>
<table><thead><tr><th style="text-align:left">工具</th><th style="text-align:left">用法</th></tr></thead><tbody><tr><td style="text-align:left"><strong>Cursor</strong></td><td style="text-align:left">复制 <code>SKILL.md</code> 到 <code>.cursorrules</code>，或 Settings → Rules → Remote Rule</td></tr><tr><td style="text-align:left"><strong>Cline / Roo-Code</strong></td><td style="text-align:left">将 <code>SKILL.md</code> 路径填入自定义 Rules</td></tr><tr><td style="text-align:left"><strong>Copilot</strong></td><td style="text-align:left">复制仓库内 <code>copilot-instructions.md</code>（如 PixiJS 官方提供）</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="结语">结语<a href="https://rainlib.vercel.app/en/blog/curated-agent-skills-for-frontend#%E7%BB%93%E8%AF%AD" class="hash-link" aria-label="Direct link to 结语" title="Direct link to 结语" translate="no">​</a></h2>
<p>前端 Agent Skills 不是「装越多越好」，而是<strong>按类型分层</strong>：</p>
<ul>
<li class=""><strong>UI 规范</strong> → 不像 AI 做的</li>
<li class=""><strong>设计系统</strong> → 组件全、合规过</li>
<li class=""><strong>框架技能</strong> → API 写对、结构清晰</li>
<li class=""><strong>性能体验</strong> → 加载快、交互顺</li>
</ul>
<p>从 <code>npx skills add</code> 开始，先装与你项目类型匹配的那一层，再按需叠加。加载了正确 Skill 的 AI，才是有设计灵魂与工程品味的协作者。</p>]]></content:encoded>
            <category>Skills</category>
            <category>AI</category>
            <category>Frontend Development</category>
            <category>UI/UX Design</category>
            <category>Prompt Engineering</category>
            <category>PixiJS</category>
            <category>React</category>
        </item>
        <item>
            <title><![CDATA[Awesome GPT-Image-2 提示词画廊：378 个案例全量分类拆解]]></title>
            <link>https://rainlib.vercel.app/en/blog/awesome-gpt-image-2-prompt-gallery</link>
            <guid>https://rainlib.vercel.app/en/blog/awesome-gpt-image-2-prompt-gallery</guid>
            <pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[把 awesome-gpt-image-2 的 378 张案例图整理成可学习的 Tab 交互画廊：按 UI、信息图、海报、商品、品牌、摄影、插画、角色、空间、文档等类型展示图片与提示词。]]></description>
            <content:encoded><![CDATA[<p>这篇文章把 <code>awesome-gpt-image-2-main</code> 中的 <strong>378 张 case 图片</strong>整理进博客资产目录，并把画廊 Markdown 中能解析到的 <strong>375 条完整案例提示词</strong>做成按类型切换的学习型画廊。原始资料中缺少标题和提示词文本的 Case 12、169、170 也保留在「补充图片」Tab，确保图片素材不遗漏。</p>
<!-- -->
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(130px, 1fr));gap:1rem;margin:2rem 0"><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">64</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">UI与界面</span></div><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">52</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">信息图与图表</span></div><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">63</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">海报与排版</span></div><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">23</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">商品与电商</span></div><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">13</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">品牌与标志</span></div><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">45</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">摄影与写实</span></div><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">30</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">插画与艺术</span></div><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">20</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">人物与角色</span></div><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">15</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">建筑与空间</span></div><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">18</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">场景与叙事</span></div><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">3</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">历史与古风</span></div><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">6</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">文档与出版物</span></div><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">23</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">其他应用</span></div><div style="border:1px solid var(--ifm-color-emphasis-200);border-radius:8px;background:var(--ifm-background-surface-color);padding:1rem;display:flex;flex-direction:column;align-items:center;text-align:center;box-shadow:0 2px 6px rgba(0,0,0,0.04)"><strong style="font-size:1.5rem;color:var(--ifm-color-primary)">3</strong><span style="color:var(--ifm-color-emphasis-700);font-size:0.9rem;margin-top:4px;font-weight:bold">补充图片</span></div></div>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="怎么读这些提示词">怎么读这些提示词<a href="https://rainlib.vercel.app/en/blog/awesome-gpt-image-2-prompt-gallery#%E6%80%8E%E4%B9%88%E8%AF%BB%E8%BF%99%E4%BA%9B%E6%8F%90%E7%A4%BA%E8%AF%8D" class="hash-link" aria-label="Direct link to 怎么读这些提示词" title="Direct link to 怎么读这些提示词" translate="no">​</a></h2>
<p>稳定的图片提示词不是灵感句，而是小型设计 Brief。读每个案例时，可以重点观察五件事：任务类型、画面结构、视觉语言、内容约束、输出条件。</p>
<div class="custom-code-block" data-language="text"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">任务类型：UI 截图 / 信息图 / 海报 / 商品广告 / 品牌系统 / 摄影 / 角色设定 / 文档图鉴</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">画面结构：标题区 + 主体视觉 + 模块区 + 标注 / 时间线 / 网格 / 底部说明</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">视觉语言：镜头、光线、材质、配色、字体、构图、风格参考</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">内容约束：必须出现的文字、模块数量、语言、比例、不要乱码、不要拥挤</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">输出条件：9:16 / 4:5 / 1:1 / 16:9 / 21:9，用途是社媒、电商、PPT、文章配图还是系列化生产</span><br></div></code></pre></div></div></div>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="全量分类案例">全量分类案例<a href="https://rainlib.vercel.app/en/blog/awesome-gpt-image-2-prompt-gallery#%E5%85%A8%E9%87%8F%E5%88%86%E7%B1%BB%E6%A1%88%E4%BE%8B" class="hash-link" aria-label="Direct link to 全量分类案例" title="Direct link to 全量分类案例" translate="no">​</a></h2>
<div><div style="display:flex;flex-wrap:wrap;gap:0.6rem;margin-bottom:1rem"><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-primary);background:var(--ifm-color-primary);color:#fff;font-size:0.9rem;font-weight:bold;cursor:pointer;transition:all 0.2s ease;box-shadow:0 4px 10px rgba(0,0,0,0.15)">UI与界面<!-- --> (<!-- -->64<!-- -->)</button><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-emphasis-300);background:var(--ifm-background-surface-color);color:var(--ifm-font-color-base);font-size:0.9rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:none">信息图与图表<!-- --> (<!-- -->52<!-- -->)</button><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-emphasis-300);background:var(--ifm-background-surface-color);color:var(--ifm-font-color-base);font-size:0.9rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:none">海报与排版<!-- --> (<!-- -->63<!-- -->)</button><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-emphasis-300);background:var(--ifm-background-surface-color);color:var(--ifm-font-color-base);font-size:0.9rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:none">商品与电商<!-- --> (<!-- -->23<!-- -->)</button><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-emphasis-300);background:var(--ifm-background-surface-color);color:var(--ifm-font-color-base);font-size:0.9rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:none">品牌与标志<!-- --> (<!-- -->13<!-- -->)</button><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-emphasis-300);background:var(--ifm-background-surface-color);color:var(--ifm-font-color-base);font-size:0.9rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:none">摄影与写实<!-- --> (<!-- -->45<!-- -->)</button><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-emphasis-300);background:var(--ifm-background-surface-color);color:var(--ifm-font-color-base);font-size:0.9rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:none">插画与艺术<!-- --> (<!-- -->30<!-- -->)</button><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-emphasis-300);background:var(--ifm-background-surface-color);color:var(--ifm-font-color-base);font-size:0.9rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:none">人物与角色<!-- --> (<!-- -->20<!-- -->)</button><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-emphasis-300);background:var(--ifm-background-surface-color);color:var(--ifm-font-color-base);font-size:0.9rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:none">建筑与空间<!-- --> (<!-- -->15<!-- -->)</button><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-emphasis-300);background:var(--ifm-background-surface-color);color:var(--ifm-font-color-base);font-size:0.9rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:none">场景与叙事<!-- --> (<!-- -->18<!-- -->)</button><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-emphasis-300);background:var(--ifm-background-surface-color);color:var(--ifm-font-color-base);font-size:0.9rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:none">历史与古风<!-- --> (<!-- -->3<!-- -->)</button><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-emphasis-300);background:var(--ifm-background-surface-color);color:var(--ifm-font-color-base);font-size:0.9rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:none">文档与出版物<!-- --> (<!-- -->6<!-- -->)</button><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-emphasis-300);background:var(--ifm-background-surface-color);color:var(--ifm-font-color-base);font-size:0.9rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:none">其他应用<!-- --> (<!-- -->23<!-- -->)</button><button style="padding:0.5rem 1.2rem;border-radius:999px;border:1px solid var(--ifm-color-emphasis-300);background:var(--ifm-background-surface-color);color:var(--ifm-font-color-base);font-size:0.9rem;font-weight:500;cursor:pointer;transition:all 0.2s ease;box-shadow:none">补充图片<!-- --> (<!-- -->3<!-- -->)</button></div><div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:1.5rem;margin-top:1.5rem"><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case2-0704c73c125aa2f249fa317c2bfc28dd.jpg" alt="社媒界面截图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->2</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">社媒界面截图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->小红书号4264014889</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case7-605dfed8be4203250015e6cd73ad9df8.jpg" alt="应用界面样机图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->7</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">应用界面样机图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->小红书号944846927</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case17-994f08229eb92616596ee66decc05f18.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->17</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@wory37303852](https://x.com/wory37303852)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case21-747e743768c46b86c9d585650105a22a.jpg" alt="直播界面设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->21</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">直播界面设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@sjbbxhz](https://x.com/sjbbxhz)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case48-456ed56a1de4e9518d1beb214f1f88be.jpg" alt="直播界面设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->48</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">直播界面设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@kylegeeks](https://x.com/kylegeeks)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case49-ce080bb498a284644556c224f088be71.jpg" alt="直播界面设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->49</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">直播界面设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@kylegeeks](https://x.com/kylegeeks)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case57-f2130d7603bf32f4e8bba57ae2f94718.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->57</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@liyue\_ai](https://x.com/liyue_ai)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case91-b0063e4be6d30df3314abd09208334ff.jpg" alt="游戏界面截图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->91</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">游戏界面截图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@wolfaidev](https://x.com/wolfaidev)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case92-e758e48f1130d4b88cdf3c8a4f3a9e26.jpg" alt="视频封面界面图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->92</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">视频封面界面图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@Yuupapa\_free](https://x.com/Yuupapa_free)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case99-50aba2c56fadd85f93e01c4b56470936.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->99</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@naga\_zyashin](https://x.com/naga_zyashin)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case101-274c66918e361ba1652f6fba353df1e9.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->101</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@naga\_zyashin](https://x.com/naga_zyashin)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case103-7b180778cba095c549f5df3b22f97adf.jpg" alt="视频封面界面图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->103</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">视频封面界面图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@bowowwoaaa2](https://x.com/bowowwoaaa2)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case104-2bcbed67a73cd4fcb2358e12455d4222.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->104</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@marouane53](https://x.com/marouane53)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case106-af6ee3b9e81351c455f8958393decab8.jpg" alt="应用界面样机图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->106</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">应用界面样机图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@abdiisan](https://x.com/abdiisan)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case107-247f619140be619e6adc12fbede5dccc.jpg" alt="应用界面样机图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->107</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">应用界面样机图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@tehno\_maniak](https://x.com/tehno_maniak)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case110-4cbeeba93ee539892acd8c3b51b30383.jpg" alt="视频封面界面图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->110</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">视频封面界面图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@TlanoAI](https://x.com/TlanoAI)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case111-52e0bacdb7ddba183ff80f798ca8639a.jpg" alt="视频封面界面图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->111</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">视频封面界面图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@mirochill](https://x.com/mirochill)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case130-8989cdcc1b44ee31ec89b2f61cb59478.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->130</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@chi\_vc\_](https://x.com/chi_vc_)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case131-8cecf17bcd363d3f5932e68df100f09e.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->131</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@IndieDevHailey](https://x.com/IndieDevHailey)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case132-20b607f047551f06cccf5e922426fb63.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->132</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@Colin\_Leeee](https://x.com/Colin_Leeee)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case133-0c0c5dbf8be75b18dcad593eff0aad65.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->133</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@yyyole](https://x.com/yyyole)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case134-058c20d3f263899b4d57b3831c1b5477.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->134</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@ryuya\_\_31](https://x.com/ryuya__31)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case135-bba4a6965d851e790ab8c75613cd6ac1.jpg" alt="应用界面样机图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->135</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">应用界面样机图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@ryuya\_\_31](https://x.com/ryuya__31)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case137-a22ee61be119eaf6da010f940b95f192.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->137</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@ryuya\_\_31](https://x.com/ryuya__31)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case149-58bad8c5d47eb09ac3b16c593b2cb483.jpg" alt="直播界面设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->149</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">直播界面设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@JCutcut47692](https://x.com/JCutcut47692)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case151-27c829c7cfffd4822d24dbfd3b484d3f.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->151</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@kitune\_fire45](https://x.com/kitune_fire45)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case152-4230892b50adfc5e1c21da78080f9088.jpg" alt="直播界面设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->152</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">直播界面设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@coder\_left](https://x.com/coder_left)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case156-46c6185b82f24a6ede6289647d08ca19.jpg" alt="应用界面样机图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->156</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">应用界面样机图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@linxiaobei888](https://x.com/linxiaobei888)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case158-8eddc8231f2d6d7efce2cdda1a3c38b0.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->158</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@coconut\_256](https://x.com/coconut_256)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case159-3b369798c3cf67ced25b564f93e5feb4.jpg" alt="界面交互设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->159</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">界面交互设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@onlyhuman028](https://x.com/onlyhuman028)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case161-f59e6835f70ea094a07d054035d294bc.jpg" alt="应用界面样机图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->161</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">应用界面样机图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@DanDaniDaniel01](https://x.com/DanDaniDaniel01)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case163-f98a86989fddd6199614cc585fb5ed0c.jpg" alt="诗仙李白月下直播起舞" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->163</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">诗仙李白月下直播起舞</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@MrLarus](https://x.com/MrLarus/status/2046585220393324553)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case164-76aa9f8036be784f00446d617c4c3ef2.jpg" alt="特朗普太空直播间破千万" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->164</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">特朗普太空直播间破千万</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@songguoxiansen](https://x.com/songguoxiansen/status/2046478609238626569)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case167-e3e8c62bb9f9a13c762cd9c0bc3d1f18.jpg" alt="大唐玄武门之变的朋友圈" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->167</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">大唐玄武门之变的朋友圈</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@Tz\_2022](https://x.com/Tz_2022/status/2046523491940225366)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case176-4709427b82230f427b74c41be456cae1.jpg" alt="苏轼被贬首日朋友圈曝光" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->176</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">苏轼被贬首日朋友圈曝光</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@MrLarus](https://x.com/MrLarus/status/2046585220393324553)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case177-6b6c1a0c91a0d1cbe8cc0c9dbea29aca.jpg" alt="吉利银河暗黑中控界面" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->177</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">吉利银河暗黑中控界面</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@xin\_pai88825](https://x.com/xin_pai88825/status/2046576100592201946)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case184-64ad6a5b6af366a27529b838116c68d5.jpg" alt="杜甫朋友圈吐槽茅屋被掀翻" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->184</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">杜甫朋友圈吐槽茅屋被掀翻</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@MrLarus](https://x.com/MrLarus/status/2046585220393324553)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case197-140ba5fb773ecb73b0c525123688197e.jpg" alt="英雄联盟特朗普中路对决哈梅内伊" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->197</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">英雄联盟特朗普中路对决哈梅内伊</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@underwoodxie96](https://x.com/underwoodxie96/status/2046529342415790275)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case200-dc3384dc907bd9160ac27c440de141b5.jpg" alt="热度爆表的美女内衣直播间" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->200</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">热度爆表的美女内衣直播间</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@xiaohu](https://x.com/xiaohu/status/2046536551681954207)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case213-3e5c42d8452513ff47c9280df33bf0dd.jpg" alt="金瓶梅古风开放世界游戏截图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->213</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">金瓶梅古风开放世界游戏截图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@op7418](https://x.com/op7418/status/2046520509651886451)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case227-f956636c2292922c3fa31f4ba92d960b.jpg" alt="哔哩哔哩户晨风直播截图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->227</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">哔哩哔哩户晨风直播截图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@austinit](https://x.com/austinit/status/2044994519649997183)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case239-3cabfefb548ff874934a6737586ddf40.jpg" alt="刘亦菲抖音直播畅聊中" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->239</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">刘亦菲抖音直播畅聊中</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@alanblogsooo](https://x.com/alanblogsooo/status/2044784762594918516)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case243-086046e12b76996cca22d97cbb8ccd0b.jpg" alt="定制专属风格界面设计系统" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->243</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">定制专属风格界面设计系统</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@stark\_nico99](https://x.com/stark_nico99/status/2045836554451706125)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case249-ea5346384367c4716de6f7f4e7f33fb8.jpg" alt="美女举牌感谢大哥打赏大火箭" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->249</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">美女举牌感谢大哥打赏大火箭</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@joshesye](https://x.com/joshesye/status/2044796366950703316)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case255-3550721053f7b3a9b08234b3a5cbf48a.jpg" alt="瑜伽裤女主播展示身材曲线" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->255</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">瑜伽裤女主播展示身材曲线</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@joshesye](https://x.com/joshesye/status/2044796366950703316)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case256-0f4638affa112eb88556721c0346cce4.jpg" alt="抖音直播间的绝美女主播" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->256</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">抖音直播间的绝美女主播</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@joshesye](https://x.com/joshesye/status/2044796366950703316)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case257-f5fb2a16afa5c961017a37ff351c5025.jpg" alt="抖音汉服美女直播带货截图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->257</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">抖音汉服美女直播带货截图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@joshesye](https://x.com/joshesye/status/2044796366950703316)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case258-0bd5faf99338f56fa7d9fe0d9c02f46a.jpg" alt="快手直播离婚预告手机截图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->258</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">快手直播离婚预告手机截图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@MrLarus](https://x.com/MrLarus/status/2045373105041007013)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case259-4fb93edfce53d49ac2355dc775e399f0.jpg" alt="精致女孩背后的网贷真相" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->259</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">精致女孩背后的网贷真相</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@MrLarus](https://x.com/MrLarus/status/2045373105041007013)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case260-9c21bf781df18ca97713d66bfc11d241.jpg" alt="社媒界面截图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->260</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">社媒界面截图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@MrLarus](https://x.com/MrLarus/status/2045373105041007013)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case261-f0c4a19ea4dee2d3ac8b6043a819348c.jpg" alt="智能视频生成器暗黑界面设计" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->261</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">智能视频生成器暗黑界面设计</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@austinit](https://x.com/austinit/status/2044968740782272596)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case263-00e2e52c8602f89bd31b8553f6d0b63f.jpg" alt="唯美二次元角色介绍网页" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->263</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">唯美二次元角色介绍网页</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@09lyco](https://x.com/09lyco/status/2045281845391323175)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case267-9d2e8e2b6840e4ccace5d904f6186bdf.jpg" alt="宋朝文人的赛博朋友圈" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->267</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">宋朝文人的赛博朋友圈</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@Panda20230902](https://x.com/Panda20230902/status/2045385588065313057)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case269-46cefe5c72be4781986ef9068711b6b2.jpg" alt="拒绝盲目催婚的暖心视频号截图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->269</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">拒绝盲目催婚的暖心视频号截图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@MrLarus](https://x.com/MrLarus/status/2045373105041007013)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case282-194905ca8e00c6035d3f48a81673f185.jpg" alt="温柔治愈系二次元手机截图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->282</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">温柔治愈系二次元手机截图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@Zoulinshen](https://x.com/Zoulinshen/status/2045082518089810073)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case287-e2c790604aa6afeea5428a927c7c7d55.jpg" alt="不知火舞的小红书主页" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->287</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">不知火舞的小红书主页</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@rionaifantasy](https://x.com/rionaifantasy/status/2045356799751303194)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case288-7f0657f2c42119c2230f0f4726345ae2.jpg" alt="抖音美女直播间界面设计" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->288</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">抖音美女直播间界面设计</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@msjiaozhu](https://x.com/msjiaozhu/status/2045470160576999812)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case289-2467bdcb3e175bd3e2927552e36a2882.jpg" alt="直播界面设计图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->289</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">直播界面设计图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@rionaifantasy](https://x.com/rionaifantasy/status/2045356799751303194)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case308-412aa28484d3a507ffca3036e086e14b.jpg" alt="抖音直播截图画面" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->308</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">抖音直播截图画面</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@\_FORAB](https://x.com/_FORAB/status/2044744023261519920)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case323-e3ac95930448e2a481cd31f7f9c1172c.jpg" alt="应用界面样机图" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->323</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">应用界面样机图</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@Mystveil7](https://x.com/Mystveil7/status/2015776042989039997)</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case330-691f5a0dfc4550fc03194c4613198a22.png" alt="月下美女直播画面" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->330</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">月下美女直播画面</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->苍何原创实测（公众号文章《我逆向了 329 条 GPT-Image2 提示词模板，全部开源！》）</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case335-01dff0790bae026150b12f2a192e6e33.png" alt="朋友圈截图生成" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->335</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">朋友圈截图生成</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->苍何原创实测（公众号文章《我逆向了 329 条 GPT-Image2 提示词模板，全部开源！》）</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case336-74166e27eff4e63c26c8c12bcbbfc9ab.png" alt="个人网页视觉设计" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->336</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">个人网页视觉设计</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->苍何原创实测（公众号文章《我逆向了 329 条 GPT-Image2 提示词模板，全部开源！》）</p></div></div></article><style>
    .gpt-gallery-modal {
      display: flex;
      flex-direction: row;
      width: min(1200px, 95vw);
      height: min(85vh, 800px);
      border-radius: 16px;
      background: var(--ifm-background-surface-color);
      box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
      overflow: hidden;
    }
    .gpt-gallery-modal-img-col {
      flex: 0 0 50%;
      background: #111;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .gpt-gallery-modal-img {
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
    .gpt-gallery-modal-content-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      background: var(--ifm-background-surface-color);
      overflow: hidden;
    }
    @media (max-width: 850px) {
      .gpt-gallery-modal {
        flex-direction: column;
      }
      .gpt-gallery-modal-img-col {
        flex: 0 0 40vh;
      }
      .gpt-gallery-modal-content-col {
        flex: 1;
      }
    }
  </style><article style="position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--ifm-color-emphasis-200);background:#000;box-shadow:0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);transition:transform 0.2s ease, box-shadow 0.2s ease;cursor:pointer;aspect-ratio:4 / 5"><img src="https://rainlib.vercel.app/en/assets/images/case339-06a5a99294aee050d21a8509d281cc96.jpg" alt="Apple 风格自然科普海报" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.3s ease"><div style="position:absolute;inset:0;background:linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.4) 40%, transparent 60%);pointer-events:none"></div><div style="position:absolute;inset:0;display:flex;flex-direction:column;padding:1rem;justify-content:space-between"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div style="display:flex;flex-wrap:wrap;gap:6px"><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(255,255,255,0.95);color:#000;box-shadow:0 2px 8px rgba(0,0,0,0.2)">#<!-- -->339</span><span style="display:inline-flex;align-items:center;border-radius:999px;padding:0.2rem 0.6rem;font-size:0.75rem;line-height:1.35;font-weight:600;letter-spacing:0.02em;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);background:rgba(0,0,0,0.5);color:#fff;border:1px solid rgba(255,255,255,0.3)">UI与界面</span></div><div style="border-radius:999px;padding:6px 12px;font-size:0.75rem;font-weight:700;background:rgba(0,0,0,0.5);color:#fff;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.25);box-shadow:0 2px 8px rgba(0,0,0,0.2)">查看提示词</div></div><div><h3 style="margin:0;font-size:1.15rem;line-height:1.35;font-weight:800;color:#fff;text-shadow:0 2px 6px rgba(0,0,0,0.6)">Apple 风格自然科普海报</h3><p style="margin:0.4rem 0 0;color:rgba(255,255,255,0.8);font-size:0.85rem;text-shadow:0 1px 4px rgba(0,0,0,0.6)">来源：<!-- -->[@berryxia](https://x.com/berryxia/status/2048251413147644100)</p></div></div></article></div></div>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="通用改写模板">通用改写模板<a href="https://rainlib.vercel.app/en/blog/awesome-gpt-image-2-prompt-gallery#%E9%80%9A%E7%94%A8%E6%94%B9%E5%86%99%E6%A8%A1%E6%9D%BF" class="hash-link" aria-label="Direct link to 通用改写模板" title="Direct link to 通用改写模板" translate="no">​</a></h2>
<p>把任意 case 改成自己的主题时，不要只替换名词。建议保留结构，替换变量：</p>
<div class="custom-code-block" data-language="text"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">生成一张[任务类型]图片。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">主体：[产品 / 人物 / 品牌 / 技术 / 地点 / 菜品 / 历史事件]。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">画面结构：[标题区] + [主体视觉] + [模块/标注/图表/网格] + [底部说明]。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">视觉风格：[摄影镜头]、[光线]、[材质]、[色彩]、[字体]、[版式参考]。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">必须出现：[准确文字、数字、标签、Logo 或标题]。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">禁止出现：[乱码、错误文字、多余 Logo、拥挤背景、错误比例]。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">输出条件：比例 [9:16 / 4:5 / 1:1 / 16:9 / 21:9]，用途 [社媒 / 博客 / 电商 / PPT]。</span><br></div></code></pre></div></div></div>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="分类学习重点">分类学习重点<a href="https://rainlib.vercel.app/en/blog/awesome-gpt-image-2-prompt-gallery#%E5%88%86%E7%B1%BB%E5%AD%A6%E4%B9%A0%E9%87%8D%E7%82%B9" class="hash-link" aria-label="Direct link to 分类学习重点" title="Direct link to 分类学习重点" translate="no">​</a></h2>
<ul>
<li class=""><strong>UI 与界面</strong>：重点看平台、画幅、状态栏、按钮文案和交互数据如何被锁定。</li>
<li class=""><strong>信息图与图表</strong>：重点看模块数量、箭头、图例、短文案和阅读顺序。</li>
<li class=""><strong>海报与排版</strong>：重点看主视觉锚点、标题拼写、留白和字体层级。</li>
<li class=""><strong>商品与电商</strong>：重点看产品规格、卖点、包装、光线、材质和 CTA 氛围。</li>
<li class=""><strong>品牌与标志</strong>：重点看 Logo、色板、字体、包装、社媒和触点系统如何一起出现。</li>
<li class=""><strong>摄影与写实</strong>：重点看镜头、焦段、自然细节、身份保留和环境可信度。</li>
<li class=""><strong>插画与艺术</strong>：重点看风格词、材质词、构图说明和情绪控制。</li>
<li class=""><strong>人物与角色</strong>：重点看设定表、网格、动作、表情、配件和角色一致性。</li>
<li class=""><strong>建筑与空间</strong>：重点看视角、结构层级、拆解标注和空间关系。</li>
<li class=""><strong>文档与出版物</strong>：重点看阅读场景、版面系统、文字密度和可读性约束。</li>
</ul>]]></content:encoded>
            <category>GPT-Image-2</category>
            <category>Image Generation</category>
            <category>Prompt Engineering</category>
            <category>AI Design</category>
            <category>OpenAI</category>
        </item>
        <item>
            <title><![CDATA[GPT-Image-2 深度解析：OpenAI 最强图像生成模型与提示词实战指南]]></title>
            <link>https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive</link>
            <guid>https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive</guid>
            <pubDate>Wed, 22 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[全面解析 OpenAI GPT-Image-2 图像生成模型：核心特性、API 调用、场景对比、提示词工程与社区精选案例，助你从零掌握 AI 生图的前沿实践。]]></description>
            <content:encoded><![CDATA[<p>2026 年 4 月，OpenAI 正式发布了其最新一代图像生成模型 —— <strong>GPT-Image-2</strong>。作为 GPT-Image 家族的旗舰迭代，它不仅在生成质量上实现了质的飞跃，更在<strong>文字渲染精度</strong>、<strong>多图编辑</strong>、<strong>风格多样性</strong>以及<strong>API 生态整合</strong>方面树立了新的行业标杆。</p>
<p>本文将带你从<strong>模型特性</strong>、<strong>技术架构</strong>、<strong>API 使用</strong>、<strong>应用场景</strong>到<strong>提示词工程最佳实践</strong>，全方位吃透 GPT-Image-2。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一gpt-image-2-核心特性">一、GPT-Image-2 核心特性<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%E4%B8%80gpt-image-2-%E6%A0%B8%E5%BF%83%E7%89%B9%E6%80%A7" class="hash-link" aria-label="Direct link to 一、GPT-Image-2 核心特性" title="Direct link to 一、GPT-Image-2 核心特性" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="11-模型定位">1.1 模型定位<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#11-%E6%A8%A1%E5%9E%8B%E5%AE%9A%E4%BD%8D" class="hash-link" aria-label="Direct link to 1.1 模型定位" title="Direct link to 1.1 模型定位" translate="no">​</a></h3>
<p>GPT-Image-2 是 OpenAI 的<strong>最先进图像生成模型</strong>（State-of-the-art），专为<strong>快速、高质量的图像生成与编辑</strong>而设计。它支持灵活的图像尺寸和高保真度的图像输入，是当前 OpenAI 平台上功能最全面的视觉创作引擎。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="12-核心能力一览">1.2 核心能力一览<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#12-%E6%A0%B8%E5%BF%83%E8%83%BD%E5%8A%9B%E4%B8%80%E8%A7%88" class="hash-link" aria-label="Direct link to 1.2 核心能力一览" title="Direct link to 1.2 核心能力一览" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:left">能力维度</th><th style="text-align:left">GPT-Image-2</th><th style="text-align:left">DALL·E 3</th><th style="text-align:left">Midjourney v6</th></tr></thead><tbody><tr><td style="text-align:left"><strong>文字渲染</strong></td><td style="text-align:left">⭐⭐⭐⭐⭐ 近乎完美</td><td style="text-align:left">⭐⭐⭐ 偶有错误</td><td style="text-align:left">⭐⭐ 表现一般</td></tr><tr><td style="text-align:left"><strong>图像编辑</strong></td><td style="text-align:left">✅ 原生支持 Mask + 多图编辑</td><td style="text-align:left">❌ 不支持</td><td style="text-align:left">❌ 不支持</td></tr><tr><td style="text-align:left"><strong>多图输入</strong></td><td style="text-align:left">✅ 最多 4 张参考图</td><td style="text-align:left">❌</td><td style="text-align:left">❌</td></tr><tr><td style="text-align:left"><strong>透明背景</strong></td><td style="text-align:left">✅ 原生 PNG 透明通道</td><td style="text-align:left">❌</td><td style="text-align:left">❌</td></tr><tr><td style="text-align:left"><strong>多轮对话编辑</strong></td><td style="text-align:left">✅ 通过 Responses API</td><td style="text-align:left">❌</td><td style="text-align:left">❌</td></tr><tr><td style="text-align:left"><strong>流式生成</strong></td><td style="text-align:left">✅ 渐进式出图</td><td style="text-align:left">❌</td><td style="text-align:left">❌</td></tr><tr><td style="text-align:left"><strong>灵活尺寸</strong></td><td style="text-align:left">✅ 任意比例</td><td style="text-align:left">固定比例</td><td style="text-align:left">固定比例</td></tr><tr><td style="text-align:left"><strong>API 可用性</strong></td><td style="text-align:left">✅ Image API + Responses API</td><td style="text-align:left">✅ 仅 Image API</td><td style="text-align:left">❌ 无官方 API</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="13-突破性亮点">1.3 突破性亮点<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#13-%E7%AA%81%E7%A0%B4%E6%80%A7%E4%BA%AE%E7%82%B9" class="hash-link" aria-label="Direct link to 1.3 突破性亮点" title="Direct link to 1.3 突破性亮点" translate="no">​</a></h3>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="-文字渲染精度ai-生图的最大痛点终于被攻克">🎯 文字渲染精度：AI 生图的最大痛点终于被攻克<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#-%E6%96%87%E5%AD%97%E6%B8%B2%E6%9F%93%E7%B2%BE%E5%BA%A6ai-%E7%94%9F%E5%9B%BE%E7%9A%84%E6%9C%80%E5%A4%A7%E7%97%9B%E7%82%B9%E7%BB%88%E4%BA%8E%E8%A2%AB%E6%94%BB%E5%85%8B" class="hash-link" aria-label="Direct link to 🎯 文字渲染精度：AI 生图的最大痛点终于被攻克" title="Direct link to 🎯 文字渲染精度：AI 生图的最大痛点终于被攻克" translate="no">​</a></h4>
<p>过去 AI 生成的图像中，文字经常出现错字、乱码、笔画变形等问题。GPT-Image-2 在这一领域实现了革命性突破：</p>
<ul>
<li class=""><strong>海报标题</strong>：中英日多语言大字标题，笔画清晰、排版精确</li>
<li class=""><strong>信息图表</strong>：科普百科图、流程图中的密集文字标注</li>
<li class=""><strong>UI 截图</strong>：模拟手机界面、社交媒体页面中的完整中文 UI 元素</li>
<li class=""><strong>书法字帖</strong>：甚至能生成笔画结构正确的书法临摹字帖</li>
</ul>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="️-高保真图像编辑不只是生成更是创作工具">🖼️ 高保真图像编辑：不只是"生成"，更是"创作工具"<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%EF%B8%8F-%E9%AB%98%E4%BF%9D%E7%9C%9F%E5%9B%BE%E5%83%8F%E7%BC%96%E8%BE%91%E4%B8%8D%E5%8F%AA%E6%98%AF%E7%94%9F%E6%88%90%E6%9B%B4%E6%98%AF%E5%88%9B%E4%BD%9C%E5%B7%A5%E5%85%B7" class="hash-link" aria-label="Direct link to 🖼️ 高保真图像编辑：不只是&quot;生成&quot;，更是&quot;创作工具&quot;" title="Direct link to 🖼️ 高保真图像编辑：不只是&quot;生成&quot;，更是&quot;创作工具&quot;" translate="no">​</a></h4>
<p>GPT-Image-2 同时具备<strong>生成</strong>和<strong>编辑</strong>两大核心能力：</p>
<ul>
<li class=""><strong>Mask 遮罩编辑</strong>：通过提供遮罩图，精准控制"画面中哪些区域需要改变"</li>
<li class=""><strong>多图融合</strong>：最多支持 4 张参考图像输入，实现"将这些物品组合成一个礼品篮"这类创意合成</li>
<li class=""><strong>多轮迭代</strong>：通过 Responses API，支持对话式的渐进编辑 —— "先画一只猫拥抱水獭"→"现在让它看起来更写实"</li>
</ul>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="-风格多样性从超写实到水墨画无所不能">🌈 风格多样性：从超写实到水墨画，无所不能<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#-%E9%A3%8E%E6%A0%BC%E5%A4%9A%E6%A0%B7%E6%80%A7%E4%BB%8E%E8%B6%85%E5%86%99%E5%AE%9E%E5%88%B0%E6%B0%B4%E5%A2%A8%E7%94%BB%E6%97%A0%E6%89%80%E4%B8%8D%E8%83%BD" class="hash-link" aria-label="Direct link to 🌈 风格多样性：从超写实到水墨画，无所不能" title="Direct link to 🌈 风格多样性：从超写实到水墨画，无所不能" translate="no">​</a></h4>
<p>社区实践表明，GPT-Image-2 在以下风格上表现出色：</p>
<ul>
<li class="">超写实人像摄影（35mm 胶片感、CCD 相机质感）</li>
<li class="">新中式水墨海报（东方美学、S 型构图）</li>
<li class="">日系 Fujifilm 胶片风</li>
<li class="">赛博朋克 UI 设计系统</li>
<li class="">涂鸦速写风（Doodle Sketch）</li>
<li class="">科普百科图鉴（信息图设计）</li>
<li class="">游戏角色设定图（三视图、表情差分）</li>
<li class="">社交媒体 UI 截图模拟</li>
</ul>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二api-接入指南">二、API 接入指南<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%E4%BA%8Capi-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97" class="hash-link" aria-label="Direct link to 二、API 接入指南" title="Direct link to 二、API 接入指南" translate="no">​</a></h2>
<p>GPT-Image-2 提供两种 API 接入方式，适配不同的使用场景。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-image-api简单直接的生成编辑">2.1 Image API：简单直接的生成/编辑<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#21-image-api%E7%AE%80%E5%8D%95%E7%9B%B4%E6%8E%A5%E7%9A%84%E7%94%9F%E6%88%90%E7%BC%96%E8%BE%91" class="hash-link" aria-label="Direct link to 2.1 Image API：简单直接的生成/编辑" title="Direct link to 2.1 Image API：简单直接的生成/编辑" translate="no">​</a></h3>
<p>适合一次性生成或编辑单张图片的场景。</p>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="生成图片python">生成图片（Python）<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%E7%94%9F%E6%88%90%E5%9B%BE%E7%89%87python" class="hash-link" aria-label="Direct link to 生成图片（Python）" title="Direct link to 生成图片（Python）" translate="no">​</a></h4>
<div class="custom-code-block" data-language="python"><div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> openai </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> OpenAI</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> base64</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">client </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> OpenAI</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">result </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">images</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">generate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    model</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"gpt-image-2"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    prompt</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"一张充满新中式美学的城市宣传海报，S型流动构图，广州地标建筑群"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">image_base64 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> result</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">data</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">b64_json</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">image_bytes </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> base64</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">b64decode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">image_base64</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token builtin">open</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"poster.png"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"wb"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">as</span><span class="token plain"> f</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    f</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">write</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">image_bytes</span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div></div>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="编辑图片多图融合">编辑图片（多图融合）<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%E7%BC%96%E8%BE%91%E5%9B%BE%E7%89%87%E5%A4%9A%E5%9B%BE%E8%9E%8D%E5%90%88" class="hash-link" aria-label="Direct link to 编辑图片（多图融合）" title="Direct link to 编辑图片（多图融合）" translate="no">​</a></h4>
<div class="custom-code-block" data-language="python"><div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">result </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">images</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">edit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    model</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"gpt-image-2"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    image</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token builtin">open</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"product_a.png"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"rb"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token builtin">open</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"product_b.png"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"rb"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token builtin">open</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"product_c.png"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"rb"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    prompt</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"将这些产品组合成一张精美的电商主图，白色背景，高级感"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div></div>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="mask-遮罩编辑">Mask 遮罩编辑<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#mask-%E9%81%AE%E7%BD%A9%E7%BC%96%E8%BE%91" class="hash-link" aria-label="Direct link to Mask 遮罩编辑" title="Direct link to Mask 遮罩编辑" translate="no">​</a></h4>
<div class="custom-code-block" data-language="python"><div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">result </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">images</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">edit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    model</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"gpt-image-2"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    image</span><span class="token operator" style="color:#393A34">=</span><span class="token builtin">open</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"room.png"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"rb"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    mask</span><span class="token operator" style="color:#393A34">=</span><span class="token builtin">open</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"mask.png"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"rb"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    prompt</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"在遮罩区域放置一只可爱的柴犬"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="22-responses-api对话式多轮创作">2.2 Responses API：对话式多轮创作<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#22-responses-api%E5%AF%B9%E8%AF%9D%E5%BC%8F%E5%A4%9A%E8%BD%AE%E5%88%9B%E4%BD%9C" class="hash-link" aria-label="Direct link to 2.2 Responses API：对话式多轮创作" title="Direct link to 2.2 Responses API：对话式多轮创作" translate="no">​</a></h3>
<p>适合需要迭代优化、上下文关联的创作场景。</p>
<div class="custom-code-block" data-language="python"><div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># 第一轮：生成初版</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">response </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">responses</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    model</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"gpt-5.4"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token builtin">input</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"生成一张灰色虎斑猫拥抱水獭的温馨插画"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    tools</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"type"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"image_generation"</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 第二轮：基于上一轮结果编辑</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">response_v2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">responses</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">create</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    model</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"gpt-5.4"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    previous_response_id</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">response</span><span class="token punctuation" style="color:#393A34">.</span><span class="token builtin">id</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token builtin">input</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"现在让它看起来更写实，像照片一样"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    tools</span><span class="token operator" style="color:#393A34">=</span><span class="token punctuation" style="color:#393A34">[</span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"type"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"image_generation"</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">]</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="23-流式生成渐进式出图">2.3 流式生成：渐进式出图<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#23-%E6%B5%81%E5%BC%8F%E7%94%9F%E6%88%90%E6%B8%90%E8%BF%9B%E5%BC%8F%E5%87%BA%E5%9B%BE" class="hash-link" aria-label="Direct link to 2.3 流式生成：渐进式出图" title="Direct link to 2.3 流式生成：渐进式出图" translate="no">​</a></h3>
<div class="custom-code-block" data-language="python"><div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">stream </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> client</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">images</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">generate</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    prompt</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"一条白色猫头鹰羽毛编织的河流，蜿蜒穿过宁静的冬季雪景"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    model</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"gpt-image-2"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    stream</span><span class="token operator" style="color:#393A34">=</span><span class="token boolean" style="color:#36acaa">True</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    partial_images</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">2</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># 生成过程中返回 0-3 张中间图</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> event </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> stream</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token builtin">type</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">==</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"image_generation.partial_image"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        idx </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">partial_image_index</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        image_bytes </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> base64</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">b64decode</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">event</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">b64_json</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">with</span><span class="token plain"> </span><span class="token builtin">open</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-interpolation string" style="color:#e3116c">f"progress_</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">{</span><span class="token string-interpolation interpolation">idx</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">}</span><span class="token string-interpolation string" style="color:#e3116c">.png"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"wb"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">as</span><span class="token plain"> f</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            f</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">write</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">image_bytes</span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="24-输出格式定制">2.4 输出格式定制<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#24-%E8%BE%93%E5%87%BA%E6%A0%BC%E5%BC%8F%E5%AE%9A%E5%88%B6" class="hash-link" aria-label="Direct link to 2.4 输出格式定制" title="Direct link to 2.4 输出格式定制" translate="no">​</a></h3>
<p>GPT-Image-2 支持灵活的输出格式控制：</p>
<table><thead><tr><th style="text-align:left">参数</th><th style="text-align:left">可选值</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left"><code>quality</code></td><td style="text-align:left"><code>low</code> / <code>medium</code> / <code>high</code> / <code>auto</code></td><td style="text-align:left">图像质量等级</td></tr><tr><td style="text-align:left"><code>size</code></td><td style="text-align:left"><code>1024x1024</code> / <code>1024x1536</code> / <code>1536x1024</code> 等</td><td style="text-align:left">图像尺寸</td></tr><tr><td style="text-align:left"><code>output_format</code></td><td style="text-align:left"><code>png</code> / <code>jpeg</code> / <code>webp</code></td><td style="text-align:left">输出格式</td></tr><tr><td style="text-align:left"><code>background</code></td><td style="text-align:left"><code>transparent</code> / <code>opaque</code></td><td style="text-align:left">背景透明度（仅 PNG/WebP）</td></tr><tr><td style="text-align:left"><code>n</code></td><td style="text-align:left"><code>1-4</code></td><td style="text-align:left">单次请求生成数量</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三最佳应用场景">三、最佳应用场景<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%E4%B8%89%E6%9C%80%E4%BD%B3%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF" class="hash-link" aria-label="Direct link to 三、最佳应用场景" title="Direct link to 三、最佳应用场景" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31--海报与平面设计">3.1 📐 海报与平面设计<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#31--%E6%B5%B7%E6%8A%A5%E4%B8%8E%E5%B9%B3%E9%9D%A2%E8%AE%BE%E8%AE%A1" class="hash-link" aria-label="Direct link to 3.1 📐 海报与平面设计" title="Direct link to 3.1 📐 海报与平面设计" translate="no">​</a></h3>
<p>GPT-Image-2 在海报设计领域的表现堪称惊艳。其强大的文字渲染能力和构图理解，使其能够生成接近专业设计师水准的作品。</p>
<p><strong>典型场景：</strong></p>
<ul>
<li class="">城市宣传海报（S 型构图 + 地标融合）</li>
<li class="">产品广告创意图</li>
<li class="">电影概念海报</li>
<li class="">活动邀请函</li>
</ul>
<p><strong>示例提示词 — 城市宣传海报：</strong></p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">一张充满新春喜庆氛围但不失高雅格调的 2026 城市宣传海报。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">双重曝光，构图延续了S型的流动感；</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">在纯白的纹理背景右下角，一个身穿中国传统服饰的微缩人物正在挥舞着一条</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">长长的红色丝绸舞带，这条红绸在空中舞动，不仅展现出丝绸的柔顺质感，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">更在向左上方飘动的过程中，奇幻地变形成了一条壮丽的山脉河流。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">在这条"河流"中，叠加了一个有山有海河的广州城市手绘图，国潮，景色尽在眼底。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">广州的地标建筑(广州塔，珠江新城建筑群，珠江, 广州城里古建筑，游轮，白云山）。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">云雾环绕，仙气缥缈，色彩丰富，结构复杂，细节丰富，但因为大面积的留白，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">画面依然显得清新脱俗，左下角排版着"SPRING 2026"和竖排的宣传语，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">整体寓意"千年商都，魅力广州"。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">文字排版优美，大方，字迹清晰完整，尺寸9:16。</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32--人像与摄影">3.2 📸 人像与摄影<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#32--%E4%BA%BA%E5%83%8F%E4%B8%8E%E6%91%84%E5%BD%B1" class="hash-link" aria-label="Direct link to 3.2 📸 人像与摄影" title="Direct link to 3.2 📸 人像与摄影" translate="no">​</a></h3>
<p>GPT-Image-2 在模拟真实摄影效果方面表现出色，能够精确还原不同镜头、胶片和打光条件下的视觉质感。</p>
<p><strong>示例提示词 — 35mm 胶片人像：</strong></p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Analog 35mm film photography, soft airy Japanese-style aesthetic,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">gentle diffused natural window light, slight overexposure, pastel tones,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">low contrast, soft highlights, minimal indoor setting near a window</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">with white curtains, clean light-colored wall, natural composition,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">eye-level, slightly closer full-body framing (mid-thigh to head),</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">young East Asian woman, natural minimal makeup, soft realistic skin texture,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">long slightly messy dark hair, oversized white button-up shirt,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">light casual shorts, barefoot, simple and relaxed styling,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">standing naturally with relaxed posture, arms loosely at sides,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">facing camera, gentle soft smile, subtle stillness,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">focus on light, air, and quiet everyday mood,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">soft film grain, dreamy and understated atmosphere --ar 9:16</span><br></div></code></pre></div></div></div>
<p><strong>示例提示词 — CCD 相机风格：</strong></p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">mobile phone photo, old CCD camera aesthetic, harsh flash, grainy,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">dim messy indoor lighting, candid snapshot feeling, slight motion blur,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">young Korean female idol, soft innocent look</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="33--角色设计与动漫">3.3 🎨 角色设计与动漫<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#33--%E8%A7%92%E8%89%B2%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%8A%A8%E6%BC%AB" class="hash-link" aria-label="Direct link to 3.3 🎨 角色设计与动漫" title="Direct link to 3.3 🎨 角色设计与动漫" translate="no">​</a></h3>
<p>GPT-Image-2 能够生成完整的角色设定图，包括三视图、表情差分、装备拆解和色板。</p>
<p><strong>示例提示词 — 角色设定卡：</strong></p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">基于此角色和背景，请制作一份类似官方设定资料的角色资料卡。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">・包含三视图：正面、侧面和背面</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">・添加角色面部表情的变化</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">・分解并展示服装和装备的详细部分</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">・添加色板</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">・包含世界观设定的简要说明</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">・总体上，使用有组织的布局（白色背景，插画风格）</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">高分辨率、专业概念艺术风格</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="34--信息图与科普百科">3.4 📊 信息图与科普百科<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#34--%E4%BF%A1%E6%81%AF%E5%9B%BE%E4%B8%8E%E7%A7%91%E6%99%AE%E7%99%BE%E7%A7%91" class="hash-link" aria-label="Direct link to 3.4 📊 信息图与科普百科" title="Direct link to 3.4 📊 信息图与科普百科" translate="no">​</a></h3>
<p>这是 GPT-Image-2 最具颠覆性的应用场景之一。过去 AI 生成的图像无法承载大量精确文字，但 GPT-Image-2 改变了这一局面。</p>
<p><strong>示例提示词 — 科普百科图：</strong></p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">请根据【主题】生成一张高质量竖版「科普百科图」。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">这张图不是普通海报,也不是单纯插画,而是一张兼具</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">"图鉴感、百科感、信息结构感、收藏感"的模块化科普信息图。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">请让画面包含:</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">- 一个清晰漂亮的主题主视觉</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">- 若干局部特征放大细节</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">- 多个圆角模块化信息分区</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">- 清楚的标题层级与重点标签</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">- 简洁但丰富的百科内容</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">- 可视化评分、要点总结或Top 5模块</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">视觉要求:</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">浅色干净背景,柔和配色,轻阴影,精致小图标,圆角信息框,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">整洁排版,信息密度高但不拥挤。</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="35--ui-设计与截图模拟">3.5 📱 UI 设计与截图模拟<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#35--ui-%E8%AE%BE%E8%AE%A1%E4%B8%8E%E6%88%AA%E5%9B%BE%E6%A8%A1%E6%8B%9F" class="hash-link" aria-label="Direct link to 3.5 📱 UI 设计与截图模拟" title="Direct link to 3.5 📱 UI 设计与截图模拟" translate="no">​</a></h3>
<p>GPT-Image-2 甚至能生成逼真的手机截图、社交媒体页面和完整的 UI 设计系统。</p>
<p><strong>示例提示词 — UI 设计系统：</strong></p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">用这种风格帮我生成一套UI设计系统，包含网页、移动端、卡片、</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">控件、按钮以及其它</span><br></div></code></pre></div></div></div>
<p><strong>示例提示词 — 社交媒体截图：</strong></p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">"宋朝人的朋友圈"，古今穿越幽默融合界面设计风格，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">画面模拟手机社交媒体界面，但内容全部是宋朝场景。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">头像是宋代文人画像，用户名"苏东坡SuShi_Official"，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">发布内容"刚到黄州，被贬了但心情还行。今天自己做了东坡肉，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">味道绝了，附菜谱："，配图为工笔画风格的东坡肉特写，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">点赞列表"黄庭坚、秦观、佛印等126人"，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">评论区"王安石：呵呵""司马光：还是那个味道"</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四提示词工程从入门到精通">四、提示词工程：从入门到精通<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%E5%9B%9B%E6%8F%90%E7%A4%BA%E8%AF%8D%E5%B7%A5%E7%A8%8B%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E7%B2%BE%E9%80%9A" class="hash-link" aria-label="Direct link to 四、提示词工程：从入门到精通" title="Direct link to 四、提示词工程：从入门到精通" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-基础原则">4.1 基础原则<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#41-%E5%9F%BA%E7%A1%80%E5%8E%9F%E5%88%99" class="hash-link" aria-label="Direct link to 4.1 基础原则" title="Direct link to 4.1 基础原则" translate="no">​</a></h3>
<ol>
<li class=""><strong>越具体越好</strong>：不要只说"画一个人"，要说明年龄、服装、姿势、表情、光线、镜头、背景等所有细节</li>
<li class=""><strong>指定技术参数</strong>：相机型号 (35mm)、胶片品牌 (Fujifilm Pro 400H)、滤镜效果 (soft black mist)</li>
<li class=""><strong>明确构图语言</strong>：S 型构图、三分法、中轴对称、低角度仰拍、鸟瞰视角</li>
<li class=""><strong>控制排除项</strong>：使用"no watermark, no text, no plastic skin"等排除项避免不想要的元素</li>
<li class=""><strong>指定画面比例</strong>：通过 <code>--ar 9:16</code>（竖版）或 <code>--ar 16:9</code>（横版）明确告知比例偏好</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-进阶技巧">4.2 进阶技巧<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#42-%E8%BF%9B%E9%98%B6%E6%8A%80%E5%B7%A7" class="hash-link" aria-label="Direct link to 4.2 进阶技巧" title="Direct link to 4.2 进阶技巧" translate="no">​</a></h3>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="-电影感构建公式">🎬 电影感构建公式<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#-%E7%94%B5%E5%BD%B1%E6%84%9F%E6%9E%84%E5%BB%BA%E5%85%AC%E5%BC%8F" class="hash-link" aria-label="Direct link to 🎬 电影感构建公式" title="Direct link to 🎬 电影感构建公式" translate="no">​</a></h4>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">[摄影技术] + [光线描述] + [主体描述] + [姿态/表情] +</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[环境/背景] + [氛围/情绪] + [质量控制] + [排除项]</span><br></div></code></pre></div></div></div>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="-海报设计公式">📐 海报设计公式<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#-%E6%B5%B7%E6%8A%A5%E8%AE%BE%E8%AE%A1%E5%85%AC%E5%BC%8F" class="hash-link" aria-label="Direct link to 📐 海报设计公式" title="Direct link to 📐 海报设计公式" translate="no">​</a></h4>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">[风格定义] + [构图结构(S型/对角线/中轴)] + [主视觉元素] +</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[色彩体系] + [文字排版要求] + [材质/质感] + [比例]</span><br></div></code></pre></div></div></div>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="-逆向出图从简到繁的迭代法">🧪 逆向出图：从简到繁的迭代法<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#-%E9%80%86%E5%90%91%E5%87%BA%E5%9B%BE%E4%BB%8E%E7%AE%80%E5%88%B0%E7%B9%81%E7%9A%84%E8%BF%AD%E4%BB%A3%E6%B3%95" class="hash-link" aria-label="Direct link to 🧪 逆向出图：从简到繁的迭代法" title="Direct link to 🧪 逆向出图：从简到繁的迭代法" translate="no">​</a></h4>
<p>有时候，简短的提示词反而效果更好：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">曼荼羅の近未来SF版を描いて</span><br></div></code></pre></div></div></div>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">收藏版史诗海报，人物侧脸剪影中生长出完整世界观与经典场景。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">整体偏电影海报加梦幻水彩插画风。</span><br></div></code></pre></div></div></div>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Create a Science fiction movie poster</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="43-场景化提示词模板">4.3 场景化提示词模板<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#43-%E5%9C%BA%E6%99%AF%E5%8C%96%E6%8F%90%E7%A4%BA%E8%AF%8D%E6%A8%A1%E6%9D%BF" class="hash-link" aria-label="Direct link to 4.3 场景化提示词模板" title="Direct link to 4.3 场景化提示词模板" translate="no">​</a></h3>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="️-城市旅行海报模板">🏙️ 城市旅行海报模板<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%EF%B8%8F-%E5%9F%8E%E5%B8%82%E6%97%85%E8%A1%8C%E6%B5%B7%E6%8A%A5%E6%A8%A1%E6%9D%BF" class="hash-link" aria-label="Direct link to 🏙️ 城市旅行海报模板" title="Direct link to 🏙️ 城市旅行海报模板" translate="no">​</a></h4>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">一张充满[节日/氛围]的 [年份] [城市名] 城市宣传海报。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">双重曝光，构图延续了S型的流动感；</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">在纯白的纹理背景右下角，[微缩人物描述]，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[丝绸/流动元素]变形成壮丽的山脉河流，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">其中叠加[城市地标列表]。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">云雾环绕，仙气缥缈，[整体色调]，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">左下角排版着"[标题文字]"和竖排的宣传语"[广告语]"。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">文字排版优美，大方，字迹清晰完整，尺寸9:16。</span><br></div></code></pre></div></div></div>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="-人像摄影模板">📸 人像摄影模板<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#-%E4%BA%BA%E5%83%8F%E6%91%84%E5%BD%B1%E6%A8%A1%E6%9D%BF" class="hash-link" aria-label="Direct link to 📸 人像摄影模板" title="Direct link to 📸 人像摄影模板" translate="no">​</a></h4>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">[胶片/相机类型] photography, [光线条件], [滤镜效果],</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[年龄] [性别] [人种], [妆容], [皮肤质感],</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[发型], [服装], [姿势], [表情],</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[环境/背景], [构图/镜头角度],</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">[质量控制: ultra-realistic, 8K, no airbrushing]</span><br></div></code></pre></div></div></div>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="-信息图模板">📊 信息图模板<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#-%E4%BF%A1%E6%81%AF%E5%9B%BE%E6%A8%A1%E6%9D%BF" class="hash-link" aria-label="Direct link to 📊 信息图模板" title="Direct link to 📊 信息图模板" translate="no">​</a></h4>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">请根据【[主题]】生成一张高质量竖版「[信息图类型]」。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">整体风格参考[参考风格]。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">画面包含:</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">- [主视觉描述]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">- [信息模块列表]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">- [色彩/排版要求]</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五gpt-image-2-vs-其他模型什么场景选什么模型">五、GPT-Image-2 vs 其他模型：什么场景选什么模型？<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%E4%BA%94gpt-image-2-vs-%E5%85%B6%E4%BB%96%E6%A8%A1%E5%9E%8B%E4%BB%80%E4%B9%88%E5%9C%BA%E6%99%AF%E9%80%89%E4%BB%80%E4%B9%88%E6%A8%A1%E5%9E%8B" class="hash-link" aria-label="Direct link to 五、GPT-Image-2 vs 其他模型：什么场景选什么模型？" title="Direct link to 五、GPT-Image-2 vs 其他模型：什么场景选什么模型？" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:left">使用场景</th><th style="text-align:left">推荐模型</th><th style="text-align:left">原因</th></tr></thead><tbody><tr><td style="text-align:left">需要精确文字的海报/图表</td><td style="text-align:left"><strong>GPT-Image-2</strong></td><td style="text-align:left">文字渲染无出其右</td></tr><tr><td style="text-align:left">多轮迭代式创作</td><td style="text-align:left"><strong>GPT-Image-2</strong> (Responses API)</td><td style="text-align:left">原生支持对话式编辑</td></tr><tr><td style="text-align:left">产品图合成/编辑</td><td style="text-align:left"><strong>GPT-Image-2</strong></td><td style="text-align:left">支持 Mask + 多图输入</td></tr><tr><td style="text-align:left">纯艺术风格探索</td><td style="text-align:left">Midjourney v6</td><td style="text-align:left">美学风格更大胆</td></tr><tr><td style="text-align:left">快速低成本批量生图</td><td style="text-align:left">GPT-Image-1-Mini</td><td style="text-align:left">成本更低</td></tr><tr><td style="text-align:left">需要透明背景的素材</td><td style="text-align:left"><strong>GPT-Image-2</strong></td><td style="text-align:left">原生支持 transparent background</td></tr></tbody></table>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六社区精选提示词画廊">六、社区精选提示词画廊<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%E5%85%AD%E7%A4%BE%E5%8C%BA%E7%B2%BE%E9%80%89%E6%8F%90%E7%A4%BA%E8%AF%8D%E7%94%BB%E5%BB%8A" class="hash-link" aria-label="Direct link to 六、社区精选提示词画廊" title="Direct link to 六、社区精选提示词画廊" translate="no">​</a></h2>
<p>以下精选了社区中最具代表性和启发性的提示词案例，按类别分类呈现。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="-极简一句话出片">🎨 极简一句话出片<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#-%E6%9E%81%E7%AE%80%E4%B8%80%E5%8F%A5%E8%AF%9D%E5%87%BA%E7%89%87" class="hash-link" aria-label="Direct link to 🎨 极简一句话出片" title="Direct link to 🎨 极简一句话出片" translate="no">​</a></h3>
<p>有时候，最简短的提示词反而能获得惊艳的效果，充分利用模型的创意自主性：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">根据你对我的认知，给我生成一个"你认识的我"的图片</span><br></div></code></pre></div></div></div>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">帮我生成xxxx真迹图片</span><br></div></code></pre></div></div></div>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">生成【城市】三天旅游攻略</span><br></div></code></pre></div></div></div>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">请根据【主题】生成一张高设计感的人物关系图海报。</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="-新中式美学">🏯 新中式美学<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#-%E6%96%B0%E4%B8%AD%E5%BC%8F%E7%BE%8E%E5%AD%A6" class="hash-link" aria-label="Direct link to 🏯 新中式美学" title="Direct link to 🏯 新中式美学" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">极简新中式美学风格，画面以淡雅的灰白色为底，呈现出一种纸艺剪影般的立体感。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">一条S形蜿蜒的裂痕状边缘将画面分割，仿佛撕开了一层纸面，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">露出内部色彩斑斓的东方山水景象。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">裂口内，一条蜿蜒的河流自上而下贯穿整个构图，河水以深浅不一的蓝色渲染。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">整体构图呈S形曲线，富有韵律感，画作边缘采用撕纸效果。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">下方题字"东方美学"以黑色楷体书写，日期"2026/04/18"与红色印章相呼应。</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="-生活创意">🍜 生活创意<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#-%E7%94%9F%E6%B4%BB%E5%88%9B%E6%84%8F" class="hash-link" aria-label="Direct link to 🍜 生活创意" title="Direct link to 🍜 生活创意" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">一张手绘风格的城市美食地图，以成都为主题。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">画面以鸟瞰视角的手绘简化城市地图为底，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">地图上分布着 12 个美食地点的精致手绘小插画：</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">春熙路的串串香、宽窄巷子的三大炮、建设路的蛋烘糕、</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">玉林路的火锅等。地图边缘用手绘藤蔓和辣椒装饰形成边框。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">左上角标题"成都·吃货暴走地图"使用胖圆的手绘美术字。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">整体画风为水彩+彩铅混合的手绘质感。</span><br></div></code></pre></div></div></div>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">帮我制作辣椒炒肉这道菜的详细制作流程图,真实风格,适用于小红书图文比例</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="-游戏与二次元">🎮 游戏与二次元<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#-%E6%B8%B8%E6%88%8F%E4%B8%8E%E4%BA%8C%E6%AC%A1%E5%85%83" class="hash-link" aria-label="Direct link to 🎮 游戏与二次元" title="Direct link to 🎮 游戏与二次元" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">生成圣斗士星矢12个黄金圣斗士的12宫格卡牌图片，</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">每张卡牌上写上对应的中文名,每行4个,宽高比16:9。</span><br></div></code></pre></div></div></div>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">以中国连环画（小人书）的风格帮我绘制大闹天空</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="️-跨界混搭">🕹️ 跨界混搭<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%EF%B8%8F-%E8%B7%A8%E7%95%8C%E6%B7%B7%E6%90%AD" class="hash-link" aria-label="Direct link to 🕹️ 跨界混搭" title="Direct link to 🕹️ 跨界混搭" translate="no">​</a></h3>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">counter strike in game screenshot, mixed with Terraria</span><br></div></code></pre></div></div></div>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">在计算机博物馆里,一个程序员在展厅中央,正在演示C语言编程,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">很多参观者在围观,屏幕上的代码清晰可见。</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">旁边的牌子写着:古法编程,现场表演。2D卡通画风,16:9</span><br></div></code></pre></div></div></div>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七使用注意事项">七、使用注意事项<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%E4%B8%83%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9" class="hash-link" aria-label="Direct link to 七、使用注意事项" title="Direct link to 七、使用注意事项" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="71-安全与合规">7.1 安全与合规<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#71-%E5%AE%89%E5%85%A8%E4%B8%8E%E5%90%88%E8%A7%84" class="hash-link" aria-label="Direct link to 7.1 安全与合规" title="Direct link to 7.1 安全与合规" translate="no">​</a></h3>
<ul>
<li class="">使用 GPT Image 模型前，可能需要在 <a href="https://platform.openai.com/settings/organization/general" target="_blank" rel="noopener noreferrer" class="">OpenAI 开发者控制台</a> 完成 <strong>API 组织验证</strong></li>
<li class="">所有生成的图像都会经过 OpenAI 的内容安全审查（Safety Checks）</li>
<li class="">生成包含真实人物肖像的内容需特别注意版权和隐私问题</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="72-成本控制">7.2 成本控制<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#72-%E6%88%90%E6%9C%AC%E6%8E%A7%E5%88%B6" class="hash-link" aria-label="Direct link to 7.2 成本控制" title="Direct link to 7.2 成本控制" translate="no">​</a></h3>
<p>GPT-Image-2 的定价基于图像分辨率和质量等级。建议：</p>
<ul>
<li class="">开发调试阶段使用 <code>quality: "low"</code> 降低成本</li>
<li class="">批量生图时合理使用 <code>n</code> 参数（单次请求最多 4 张）</li>
<li class="">使用 Flex processing 获取折扣价格</li>
<li class="">参考 <a href="https://developers.openai.com/api/docs/guides/image-generation#calculating-costs" target="_blank" rel="noopener noreferrer" class="">官方定价计算器</a> 估算成本</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="73-最佳实践">7.3 最佳实践<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#73-%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5" class="hash-link" aria-label="Direct link to 7.3 最佳实践" title="Direct link to 7.3 最佳实践" translate="no">​</a></h3>
<ol>
<li class=""><strong>先用 Responses API 探索创意</strong>：利用多轮对话快速迭代找到满意的方向</li>
<li class=""><strong>确定方向后切换 Image API</strong>：直接调用更高效、延迟更低</li>
<li class=""><strong>善用 Mask 编辑</strong>：局部修改比重新生成整张图更高效</li>
<li class=""><strong>保存 revised_prompt</strong>：API 会自动优化你的提示词，查看 <code>revised_prompt</code> 可以学习更好的描述方式</li>
<li class=""><strong>流式生成提升体验</strong>：面向用户的应用建议开启 <code>stream: true</code> + <code>partial_images</code></li>
</ol>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八结语">八、结语<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%E5%85%AB%E7%BB%93%E8%AF%AD" class="hash-link" aria-label="Direct link to 八、结语" title="Direct link to 八、结语" translate="no">​</a></h2>
<p>GPT-Image-2 的发布标志着 AI 图像生成从"勉强可用"进化到了"专业级创作工具"的新阶段。其在<strong>文字渲染</strong>、<strong>图像编辑</strong>和<strong>多模态协作</strong>方面的突破，让它不仅是设计师的灵感助手，更是内容创作者、产品经理和开发者手中的生产力利器。</p>
<p>最重要的是，好的工具只是起点，<strong>好的提示词才是核心竞争力</strong>。希望本文提供的提示词模板、场景案例和工程技巧，能帮助你在 AI 生图的道路上走得更远。</p>
<hr>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="相关资源">相关资源<a href="https://rainlib.vercel.app/en/blog/gpt-image-2-deep-dive#%E7%9B%B8%E5%85%B3%E8%B5%84%E6%BA%90" class="hash-link" aria-label="Direct link to 相关资源" title="Direct link to 相关资源" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://developers.openai.com/api/docs/models/gpt-image-2" target="_blank" rel="noopener noreferrer" class="">OpenAI GPT-Image-2 官方文档</a></li>
<li class=""><a href="https://developers.openai.com/api/docs/guides/image-generation" target="_blank" rel="noopener noreferrer" class="">Image Generation API 完整指南</a></li>
<li class=""><a href="https://developers.openai.com/cookbook/examples/multimodal/image-gen-models-prompting-guide" target="_blank" rel="noopener noreferrer" class="">GPT Image Prompting Guide (Cookbook)</a></li>
<li class=""><a href="https://github.com/openai/openai-imagegen-demo" target="_blank" rel="noopener noreferrer" class="">Image Generation Demo Repo</a></li>
<li class=""><a href="https://github.com/EvoLinkAI/awesome-gpt-image-2-prompts" target="_blank" rel="noopener noreferrer" class="">Awesome GPT-Image-2 Prompts (社区提示词库)</a></li>
<li class=""><a href="https://developers.openai.com/api/docs/pricing#image-generation" target="_blank" rel="noopener noreferrer" class="">OpenAI 定价页面</a></li>
</ul>]]></content:encoded>
            <category>OpenAI</category>
            <category>GPT-Image-2</category>
            <category>Image Generation</category>
            <category>AI Models</category>
            <category>Prompt Engineering</category>
            <category>AI</category>
        </item>
        <item>
            <title><![CDATA[NFC 安全芯片深度解析：MIFARE DESFire EV1/EV2/EV3、NTAG DNA 与 TAPLINX 全链路指南]]></title>
            <link>https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx</link>
            <guid>https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx</guid>
            <pubDate>Sun, 12 Apr 2026 10:00:00 GMT</pubDate>
            <description><![CDATA[全面解析 NXP MIFARE DESFire 三代演进（EV1/EV2/EV3）、NTAG DNA 防伪认证芯片、TAPLINX 云管理平台，以及开源实战方案]]></description>
            <content:encoded><![CDATA[<div style="margin-top:2rem;margin-bottom:2rem"><div class="animate-pulse bg-zinc-800/50 rounded-3xl w-full aspect-video"></div></div>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="一mifare-desfire-系列概述">一、MIFARE DESFire 系列概述<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#%E4%B8%80mifare-desfire-%E7%B3%BB%E5%88%97%E6%A6%82%E8%BF%B0" class="hash-link" aria-label="Direct link to 一、MIFARE DESFire 系列概述" title="Direct link to 一、MIFARE DESFire 系列概述" translate="no">​</a></h2>
<p>MIFARE 是 NXP（恩智浦半导体）旗下最知名的非接触式智能卡品牌，基于 <strong>ISO/IEC 14443 Type A</strong> 标准，工作频率为 <strong>13.56 MHz</strong>。经过数十年的发展，MIFARE 已成为全球交通票务、门禁控制、身份认证等领域部署最广泛的非接触式智能卡平台之一。</p>
<p><strong>DESFire</strong> 这个名字的含义非常直观：</p>
<ul>
<li class=""><strong>DES</strong> — 表示支持 DES、2K3DES、3K3DES 和 AES 硬件加密引擎</li>
<li class=""><strong>Fire</strong> — 代表高速、高性能的处理能力</li>
</ul>
<p>DESFire 系列芯片的典型应用场景包括：</p>
<ul>
<li class=""><strong>身份验证</strong> — 电子护照、国民身份证、企业员工卡</li>
<li class=""><strong>访问控制</strong> — 门禁系统、停车场管理</li>
<li class=""><strong>忠诚度计划</strong> — 会员卡、积分系统</li>
<li class=""><strong>小额支付</strong> — 电子钱包、自动售货机</li>
<li class=""><strong>交通票务</strong> — 公交、地铁、出租车一卡通</li>
</ul>
<p>DESFire 的核心架构围绕"应用-文件"模型展开：一张卡片可以承载多个独立的应用（Application），每个应用拥有独立的密钥体系和文件系统，实现了真正的多应用隔离。</p>
<!-- -->
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="二isoiec-14443-协议分层详解">二、ISO/IEC 14443 协议分层详解<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#%E4%BA%8Cisoiec-14443-%E5%8D%8F%E8%AE%AE%E5%88%86%E5%B1%82%E8%AF%A6%E8%A7%A3" class="hash-link" aria-label="Direct link to 二、ISO/IEC 14443 协议分层详解" title="Direct link to 二、ISO/IEC 14443 协议分层详解" translate="no">​</a></h2>
<p>MIFARE DESFire 完全基于 <strong>ISO/IEC 14443 Type A</strong> 标准运行。理解这个四层协议架构，是深入掌握 DESFire 通信机制的基础。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="21-四层协议架构">2.1 四层协议架构<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#21-%E5%9B%9B%E5%B1%82%E5%8D%8F%E8%AE%AE%E6%9E%B6%E6%9E%84" class="hash-link" aria-label="Direct link to 2.1 四层协议架构" title="Direct link to 2.1 四层协议架构" translate="no">​</a></h3>
<table><thead><tr><th>层次</th><th>标准编号</th><th>名称</th><th>核心职责</th></tr></thead><tbody><tr><td><strong>物理层</strong></td><td>ISO/IEC 14443-1</td><td>物理特性</td><td>卡片物理尺寸（ID-1 信用卡尺寸）、机械应力承受能力、天线布局要求</td></tr><tr><td><strong>射频/帧层</strong></td><td>ISO/IEC 14443-2</td><td>射频功率与信号接口</td><td>13.56 MHz 载波频率、调制解调方式、位编码规则、能量传输机制</td></tr><tr><td><strong>初始化/防冲突层</strong></td><td>ISO/IEC 14443-3</td><td>初始化与防冲突</td><td>卡片状态机、轮询命令、防冲突算法、UID 选择流程</td></tr><tr><td><strong>传输协议层</strong></td><td>ISO/IEC 14443-4</td><td>传输协议 (T=CL)</td><td>半双工块传输协议、帧格式、错误恢复、流量控制、多逻辑通道</td></tr></tbody></table>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="22-射频帧层通信参数">2.2 射频/帧层通信参数<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#22-%E5%B0%84%E9%A2%91%E5%B8%A7%E5%B1%82%E9%80%9A%E4%BF%A1%E5%8F%82%E6%95%B0" class="hash-link" aria-label="Direct link to 2.2 射频/帧层通信参数" title="Direct link to 2.2 射频/帧层通信参数" translate="no">​</a></h3>
<p>MIFARE DESFire 使用 <strong>Type A</strong> 接口，具体参数如下：</p>
<table><thead><tr><th>方向</th><th>调制方式</th><th>编码方式</th><th>副载波频率</th><th>数据速率</th></tr></thead><tbody><tr><td>PCD → PICC（读卡器到卡）</td><td>100% ASK</td><td>Modified Miller</td><td>—</td><td>106 / 212 / 424 / 848 kbit/s</td></tr><tr><td>PICC → PCD（卡到读卡器）</td><td>OOK 负载调制</td><td>Manchester</td><td>847.5 kHz</td><td>106 / 212 / 424 / 848 kbit/s</td></tr></tbody></table>
<ul>
<li class=""><strong>ASK（幅移键控）</strong>：读卡器通过改变射频场幅度来发送数据</li>
<li class=""><strong>Modified Miller 编码</strong>：Type A 特有的位编码方式，通过脉冲位置表示数据</li>
<li class=""><strong>OOK 负载调制</strong>：卡片通过切换负载来调制反射信号，读卡器检测副载波变化来接收数据</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="23-防冲突与-uid-选择流程">2.3 防冲突与 UID 选择流程<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#23-%E9%98%B2%E5%86%B2%E7%AA%81%E4%B8%8E-uid-%E9%80%89%E6%8B%A9%E6%B5%81%E7%A8%8B" class="hash-link" aria-label="Direct link to 2.3 防冲突与 UID 选择流程" title="Direct link to 2.3 防冲突与 UID 选择流程" translate="no">​</a></h3>
<p>Type A 采用<strong>确定性二叉树搜索</strong>防冲突算法。DESFire 使用 <strong>7 字节 UID</strong>，因此需要 <strong>2 级级联（Cascade）</strong> 完成选择。</p>
<!-- -->
<p><strong>防冲突流程关键步骤：</strong></p>
<ol>
<li class=""><strong>唤醒</strong>：读卡器发送 <code>REQA</code>（<code>0x26</code>）唤醒 IDLE 状态的卡片，或 <code>WUPA</code>（<code>0x52</code>）唤醒所有卡片</li>
<li class=""><strong>ATQA 响应</strong>：所有卡片回复 2 字节 ATQA（Answer To Request Type A），告知 UID 长度和防冲突能力</li>
<li class=""><strong>防冲突循环</strong>：读卡器发送 <code>ANTICOLLISION</code>（<code>0x93 0x20</code>），多张卡同时发送 UID，冲突位被检测到</li>
<li class=""><strong>逐位选择</strong>：读卡器通过指定已知前缀，逐步排除冲突卡片</li>
<li class=""><strong>SELECT 确认</strong>：用完整 UID 发送 <code>SELECT</code>（<code>0x93 0x70</code> + UID），被选中卡片回复 SAK（Select Acknowledge）</li>
<li class=""><strong>级联</strong>：7 字节 UID 需要两级级联（Level 1: <code>0x93</code>，Level 2: <code>0x95</code>），SAK 中 bit 5=1 表示支持 ISO 14443-4</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="24-传输协议层-tcl">2.4 传输协议层 (T=CL)<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#24-%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE%E5%B1%82-tcl" class="hash-link" aria-label="Direct link to 2.4 传输协议层 (T=CL)" title="Direct link to 2.4 传输协议层 (T=CL)" translate="no">​</a></h3>
<p>ISO 14443-4 定义了 <strong>T=CL</strong>（Contactless）块传输协议，支持三种块类型：</p>
<table><thead><tr><th>块类型</th><th>PCB 编码</th><th>功能</th></tr></thead><tbody><tr><td><strong>I-Block</strong></td><td><code>0x0N</code></td><td>信息块，承载数据（APDU 命令/响应）</td></tr><tr><td><strong>R-Block</strong></td><td><code>0x8N / 0xAN</code></td><td>接收就绪块，用于 ACK/NAK 流量控制</td></tr><tr><td><strong>S-Block</strong></td><td><code>0xC0 / 0xD0</code></td><td>管理块，用于 DESELECT、WTX（等待时间扩展）等</td></tr></tbody></table>
<p><strong>帧大小（FSCI）演进：</strong></p>
<table><thead><tr><th>芯片</th><th>最大帧大小</th><th>说明</th></tr></thead><tbody><tr><td>DESFire EV1</td><td>标准帧（~33 字节有效载荷）</td><td>固定帧大小</td></tr><tr><td>DESFire EV2</td><td>最大 128 字节</td><td>可配置 FSCI 参数</td></tr><tr><td>DESFire EV3</td><td>最大 256 字节</td><td>可配置 FSCI 参数，传输效率翻倍</td></tr></tbody></table>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="三desfire-原生命令集与-apdu-封装">三、DESFire 原生命令集与 APDU 封装<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#%E4%B8%89desfire-%E5%8E%9F%E7%94%9F%E5%91%BD%E4%BB%A4%E9%9B%86%E4%B8%8E-apdu-%E5%B0%81%E8%A3%85" class="hash-link" aria-label="Direct link to 三、DESFire 原生命令集与 APDU 封装" title="Direct link to 三、DESFire 原生命令集与 APDU 封装" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="31-原生命令帧格式">3.1 原生命令帧格式<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#31-%E5%8E%9F%E7%94%9F%E5%91%BD%E4%BB%A4%E5%B8%A7%E6%A0%BC%E5%BC%8F" class="hash-link" aria-label="Direct link to 3.1 原生命令帧格式" title="Direct link to 3.1 原生命令帧格式" translate="no">​</a></h3>
<p>DESFire 原生命令使用简洁的二进制帧格式：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">发送帧: [CMD: 1字节] [数据: 变长] [CRC16: 2字节]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">响应帧: [状态码: 1字节] [数据: 变长] [CRC16: 2字节]</span><br></div></code></pre></div></div></div>
<ul>
<li class=""><strong>CRC-16/CCITT</strong>：多项式 <code>0x1021</code>，初始值 <code>0x6363</code></li>
<li class=""><strong>状态码 <code>0x00</code></strong> = 操作成功（在 APDU 封装中对应 <code>SW1=0x91, SW2=0x00</code>）</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="32-核心原生命令完整列表">3.2 核心原生命令完整列表<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#32-%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%94%9F%E5%91%BD%E4%BB%A4%E5%AE%8C%E6%95%B4%E5%88%97%E8%A1%A8" class="hash-link" aria-label="Direct link to 3.2 核心原生命令完整列表" title="Direct link to 3.2 核心原生命令完整列表" translate="no">​</a></h3>
<table><thead><tr><th>命令码</th><th>命令名称</th><th>功能描述</th></tr></thead><tbody><tr><td><code>0x60</code></td><td>GET_VERSION</td><td>读取卡片硬件/软件版本信息（7+1+7 字节响应）</td></tr><tr><td><code>0x51</code></td><td>GET_UID</td><td>获取卡片唯一标识符（7 字节 UID）</td></tr><tr><td><code>0x5A</code></td><td>SELECT_APPLICATION</td><td>选择应用（参数：3 字节 AID）</td></tr><tr><td><code>0x0A</code></td><td>AUTHENTICATE (DES/3DES)</td><td>DES/3DES 认证（3 次握手）</td></tr><tr><td><code>0x1A</code></td><td>AUTHENTICATE (ISO)</td><td>ISO 14443-4 标准认证</td></tr><tr><td><code>0xAA</code></td><td>AUTHENTICATE (AES)</td><td>AES-128 认证（3 次握手，推荐）</td></tr><tr><td><code>0xCA</code></td><td>CREATE_APPLICATION</td><td>创建新应用（参数：AID + 密钥设置 + ISO FID）</td></tr><tr><td><code>0xDA</code></td><td>DELETE_APPLICATION</td><td>删除应用</td></tr><tr><td><code>0x6A</code></td><td>READ_DATA</td><td>从标准/备份数据文件读取数据</td></tr><tr><td><code>0x3D</code></td><td>WRITE_DATA</td><td>向标准/备份数据文件写入数据</td></tr><tr><td><code>0x3B</code></td><td>WRITE_RECORD</td><td>向记录文件写入记录</td></tr><tr><td><code>0xBB</code></td><td>READ_RECORDS</td><td>从记录文件读取记录</td></tr><tr><td><code>0xEB</code></td><td>CLEAR_RECORDS</td><td>清除记录文件</td></tr><tr><td><code>0x6C</code></td><td>GET_VALUE</td><td>读取值文件的当前值</td></tr><tr><td><code>0x0C</code></td><td>CREDIT</td><td>向值文件增加数值</td></tr><tr><td><code>0xDC</code></td><td>DEBIT</td><td>从值文件减少数值</td></tr><tr><td><code>0x1C</code></td><td>LIMITED_CREDIT</td><td>受限增加（带上限的充值）</td></tr><tr><td><code>0x5B</code></td><td>GET_FILE_SETTINGS</td><td>获取文件设置（类型、大小、访问权限）</td></tr><tr><td><code>0x5F</code></td><td>GET_FILE_IDS</td><td>获取当前应用下的所有文件 ID 列表</td></tr><tr><td><code>0x64</code></td><td>GET_KEY_SETTINGS</td><td>获取应用密钥设置</td></tr><tr><td><code>0xC4</code></td><td>CHANGE_KEY</td><td>更改密钥（需认证后执行）</td></tr><tr><td><code>0xC7</code></td><td>COMMIT_TRANSACTION</td><td>提交事务（原子操作确认）</td></tr><tr><td><code>0xA7</code></td><td>ABORT_TRANSACTION</td><td>回滚事务</td></tr><tr><td><code>0xCD</code></td><td>SET_CONFIGURATION</td><td>修改卡片配置（如随机 ID 模式、ATS 等）</td></tr><tr><td><code>0xF2</code></td><td>FORMAT_PICC</td><td>格式化整张卡（恢复出厂设置）</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="33-iso-7816-4-apdu-封装">3.3 ISO 7816-4 APDU 封装<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#33-iso-7816-4-apdu-%E5%B0%81%E8%A3%85" class="hash-link" aria-label="Direct link to 3.3 ISO 7816-4 APDU 封装" title="Direct link to 3.3 ISO 7816-4 APDU 封装" translate="no">​</a></h3>
<p>DESFire 原生命令通过以下规则封装为标准 ISO 7816-4 APDU：</p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">命令 APDU: [CLA=0x90] [INS=CMD] [P1=0x00] [P2=0x00] [Lc] [Data] [Le=0x00]</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">响应 APDU: [Data] [SW1=0x91] [SW2=状态码]</span><br></div></code></pre></div></div></div>
<p><strong>封装示例 — AES 认证：</strong></p>
<div class="custom-code-block" data-language="python"><div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># 原生格式: AA 01 [CRC16]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># APDU 封装格式:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">AUTH_AES_KEY1 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0x90</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># CLA - 固定为 0x90</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0xAA</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># INS - AUTHENTICATE AES</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0x00</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># P1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0x00</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># P2</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0x01</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># Lc = 1 字节数据</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0x01</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># Data = 密钥编号 1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0x00</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic"># Le</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">]</span><br></div></code></pre></div></div></div>
<p><strong>封装示例 — 选择应用：</strong></p>
<div class="custom-code-block" data-language="python"><div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># 原生格式: 5A A00001 [CRC16]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># APDU 封装格式:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">SELECT_APP </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0x90</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># CLA</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0x5A</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># INS - SELECT APPLICATION</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0x00</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># P1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0x00</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># P2</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0x03</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># Lc = 3 字节</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0xA0</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x00</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x01</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># Data = AID</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token number" style="color:#36acaa">0x00</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic"># Le</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">]</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="34-原生模式-vs-iso-apdu-模式对比">3.4 原生模式 vs ISO APDU 模式对比<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#34-%E5%8E%9F%E7%94%9F%E6%A8%A1%E5%BC%8F-vs-iso-apdu-%E6%A8%A1%E5%BC%8F%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to 3.4 原生模式 vs ISO APDU 模式对比" title="Direct link to 3.4 原生模式 vs ISO APDU 模式对比" translate="no">​</a></h3>
<p>DESFire 支持两种通信模式，开发者需要根据场景选择：</p>
<table><thead><tr><th>特性</th><th>原生模式 (Native)</th><th>ISO 7816-4 APDU 模式</th></tr></thead><tbody><tr><td><strong>帧格式</strong></td><td><code>[CMD] [Data] [CRC16]</code></td><td><code>[CLA=90] [INS] [P1] [P2] [Lc] [Data] [Le]</code></td></tr><tr><td><strong>校验方式</strong></td><td>CRC-16（应用层）</td><td>由 T=CL 传输层处理（块级 CRC）</td></tr><tr><td><strong>响应格式</strong></td><td><code>[Status] [Data] [CRC16]</code></td><td><code>[Data] [SW1=0x91] [SW2=Status]</code></td></tr><tr><td><strong>选择应用</strong></td><td><code>0x5A</code> SELECT APPLICATION</td><td><code>0xA4</code> SELECT FILE（ISO 标准）</td></tr><tr><td><strong>读数据</strong></td><td><code>0x6A</code> READ DATA</td><td><code>0xB0</code> READ BINARY（ISO 标准）</td></tr><tr><td><strong>写数据</strong></td><td><code>0x3D</code> WRITE DATA</td><td><code>0xD6</code> UPDATE BINARY（ISO 标准）</td></tr><tr><td><strong>读记录</strong></td><td><code>0xBB</code> READ RECORDS</td><td><code>0xB2</code> READ RECORDS（ISO 标准）</td></tr><tr><td><strong>写记录</strong></td><td><code>0x3B</code> WRITE RECORD</td><td><code>0xE2</code> APPEND RECORD（ISO 标准）</td></tr><tr><td><strong>兼容性</strong></td><td>DESFire 专有</td><td>与标准 PC/SC 读卡器驱动兼容</td></tr><tr><td><strong>推荐场景</strong></td><td>自定义读卡器、嵌入式设备</td><td>PC/SC 标准读卡器、跨平台开发</td></tr></tbody></table>
<!-- -->
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="四ev1-vs-ev2-vs-ev3-完整对比">四、EV1 vs EV2 vs EV3 完整对比<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#%E5%9B%9Bev1-vs-ev2-vs-ev3-%E5%AE%8C%E6%95%B4%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to 四、EV1 vs EV2 vs EV3 完整对比" title="Direct link to 四、EV1 vs EV2 vs EV3 完整对比" translate="no">​</a></h2>
<p>MIFARE DESFire 经历了三代演进，每一代都在安全性、性能和功能上实现了显著提升。以下是三代产品的详细对比。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="41-核心参数对比表">4.1 核心参数对比表<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#41-%E6%A0%B8%E5%BF%83%E5%8F%82%E6%95%B0%E5%AF%B9%E6%AF%94%E8%A1%A8" class="hash-link" aria-label="Direct link to 4.1 核心参数对比表" title="Direct link to 4.1 核心参数对比表" translate="no">​</a></h3>
<table><thead><tr><th>特性</th><th>EV1 (MF3ICD41)</th><th>EV2 (MF3D(H)x2)</th><th>EV3 (MF3D(H)x3)</th></tr></thead><tbody><tr><td><strong>CC 认证等级</strong></td><td>EAL 4+</td><td>EAL 5+（硬件+软件）</td><td>EAL 5+（硬件+软件）</td></tr><tr><td><strong>存储容量</strong></td><td>2 / 4 / 8 KB</td><td>2 / 4 / 8 / 16 / 32 KB</td><td>2 / 4 / 8 KB</td></tr><tr><td><strong>应用数量</strong></td><td>最多 28 个</td><td>取决于内存大小</td><td>取决于内存大小</td></tr><tr><td><strong>加密算法</strong></td><td>DES / 2K3DES / 3K3DES / AES-128</td><td>DES / 2K3DES / 3K3DES / AES-128</td><td>DES / 2K3DES / 3K3DES / AES-128</td></tr><tr><td><strong>数据传输速率</strong></td><td>最高 848 kbit/s</td><td>最高 848 kbit/s</td><td>最高 848 kbit/s</td></tr><tr><td><strong>数据保留</strong></td><td>10 年</td><td>25 年</td><td>25 年</td></tr><tr><td><strong>写入寿命</strong></td><td>100,000 次</td><td>500,000 次</td><td>1,000,000 次</td></tr><tr><td><strong>最大 FSCI 帧长</strong></td><td>—</td><td>128 字节</td><td>256 字节</td></tr><tr><td><strong>SUN 消息认证</strong></td><td>不支持</td><td>不支持</td><td>支持（兼容 NTAG DNA）</td></tr><tr><td><strong>委托应用管理</strong></td><td>不支持</td><td>MIsmartApp</td><td>MIsmartApp</td></tr><tr><td><strong>事务 MAC</strong></td><td>不支持</td><td>Transaction MAC</td><td>Transaction MAC</td></tr><tr><td><strong>多密钥集</strong></td><td>不支持</td><td>每应用最多 16 套</td><td>每应用最多 16 套</td></tr><tr><td><strong>每文件多密钥</strong></td><td>不支持</td><td>每文件最多 8 个密钥</td><td>每文件最多 8 个密钥</td></tr><tr><td><strong>虚拟卡架构</strong></td><td>不支持</td><td>Virtual Card Architecture</td><td>Virtual Card Architecture</td></tr><tr><td><strong>近场校验</strong></td><td>不支持</td><td>Proximity Check</td><td>Proximity Check</td></tr><tr><td><strong>原创性校验</strong></td><td>不支持</td><td>Originality Check</td><td>Originality Check</td></tr><tr><td><strong>事务定时器</strong></td><td>不支持</td><td>不支持</td><td>Transaction Timer</td></tr><tr><td><strong>安全消息传递</strong></td><td>不支持</td><td>EV2 Secure Messaging</td><td>EV2 Secure Messaging</td></tr><tr><td><strong>应用间文件共享</strong></td><td>不支持</td><td>支持</td><td>支持</td></tr><tr><td><strong>DAM 密钥预加载</strong></td><td>不支持</td><td>不支持</td><td>出厂预加载（AppXplorer）</td></tr><tr><td><strong>MIFARE 2GO 集成</strong></td><td>不支持</td><td>有限支持</td><td>无缝集成</td></tr><tr><td><strong>向后兼容</strong></td><td>—</td><td>EV1、D40</td><td>EV2、EV1、D40</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="42-ev1mf3icd41详解">4.2 EV1（MF3ICD41）详解<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#42-ev1mf3icd41%E8%AF%A6%E8%A7%A3" class="hash-link" aria-label="Direct link to 4.2 EV1（MF3ICD41）详解" title="Direct link to 4.2 EV1（MF3ICD41）详解" translate="no">​</a></h3>
<p>EV1 是 DESFire 系列的里程碑产品，奠定了整个 DESFire 家族的架构基础。</p>
<p><strong>文件类型支持：</strong></p>
<ul>
<li class=""><strong>标准数据文件（Standard Data File）</strong> — 任意数据的读写存储</li>
<li class=""><strong>备份数据文件（Backup Data File）</strong> — 读写事务完整性保护</li>
<li class=""><strong>数值文件（Value File）</strong> — 带有安全计数器的电子钱包</li>
<li class=""><strong>线性记录文件（Linear Record File）</strong> — 定长记录的顺序存储</li>
<li class=""><strong>循环记录文件（Cyclic Record File）</strong> — 定长记录的循环覆盖存储</li>
</ul>
<p>EV1 的局限性在于：不支持委托应用管理、事务 MAC 和多密钥集，这些能力在后续代次中逐步补齐。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="43-ev2mf3dhx2详解">4.3 EV2（MF3D(H)x2）详解<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#43-ev2mf3dhx2%E8%AF%A6%E8%A7%A3" class="hash-link" aria-label="Direct link to 4.3 EV2（MF3D(H)x2）详解" title="Direct link to 4.3 EV2（MF3D(H)x2）详解" translate="no">​</a></h3>
<p>EV2 是一次重大升级，在安全性和功能丰富度上实现了质的飞跃。</p>
<!-- -->
<p><strong>关键特性说明：</strong></p>
<ul>
<li class=""><strong>MIsmartApp（委托应用管理）</strong> — 允许将应用管理权限委托给第三方，实现多发行方协作</li>
<li class=""><strong>Transaction MAC</strong> — 为每个事务生成消息认证码，确保交易完整性和不可抵赖性</li>
<li class=""><strong>Multiple Key Sets</strong> — 每个应用最多支持 16 套密钥集，支持快速密钥滚动（Key Rollover），无需停机即可更换密钥</li>
<li class=""><strong>Multiple Keys per File Access</strong> — 每个文件最多可配置 8 个不同密钥，分别控制读、写、修改等操作权限</li>
<li class=""><strong>Virtual Card Architecture</strong> — 虚拟卡架构支持在一张物理卡片上创建多个虚拟卡，每个虚拟卡拥有独立的密钥和文件系统，实现隐私保护</li>
<li class=""><strong>Proximity Check</strong> — 通过测量射频场信号强度和时间参数，检测并防御中继攻击（Relay Attack）</li>
<li class=""><strong>Originality Check</strong> — 使用 NXP 专有机制验证卡片是否为原厂正品，防止伪造卡片</li>
<li class=""><strong>EV2 Secure Messaging</strong> — 基于 AES 的端到端安全消息传递，保护命令和响应数据的机密性与完整性</li>
<li class=""><strong>应用间文件共享</strong> — 允许不同应用之间共享文件数据，减少冗余存储</li>
<li class=""><strong>Memory Reuse in DAM</strong> — 支持格式化应用并回收其占用的内存空间</li>
<li class=""><strong>可配置 FSCI</strong> — 帧大小配置项（FSCI）支持最大 128 字节帧，提升通信效率</li>
<li class=""><strong>可选高输入电容 70pF</strong> — 支持更小尺寸的天线设计，适用于微型卡片和可穿戴设备</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="44-ev3mf3dhx3详解">4.4 EV3（MF3D(H)x3）详解<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#44-ev3mf3dhx3%E8%AF%A6%E8%A7%A3" class="hash-link" aria-label="Direct link to 4.4 EV3（MF3D(H)x3）详解" title="Direct link to 4.4 EV3（MF3D(H)x3）详解" translate="no">​</a></h3>
<p>EV3 在 EV2 的基础上进一步强化了安全防护和生态集成能力。</p>
<!-- -->
<p><strong>关键新增特性：</strong></p>
<ul>
<li class=""><strong>SUN（Secure Unique NFC）消息认证</strong> — EV3 内置了与 NTAG DNA 兼容的 SUN 消息生成能力，使 DESFire 卡片也能提供产品防伪认证功能</li>
<li class=""><strong>Transaction Timer（事务定时器）</strong> — 为事务处理引入时间约束，有效缓解中间人攻击（Man-in-the-Middle Attack）</li>
<li class=""><strong>出厂预加载 DAM 密钥</strong> — 芯片出厂时即预装默认应用管理器（DAM）密钥，支持 NXP AppXplorer 服务的即开即用</li>
<li class=""><strong>可配置 FSCI 最大 256 字节</strong> — 帧长度上限从 EV2 的 128 字节提升到 256 字节，显著提高大数据量传输效率</li>
<li class=""><strong>写入寿命提升到 1,000,000 次</strong> — 相比 EV2 的 500,000 次翻倍，适用于高频写入场景</li>
<li class=""><strong>与 MIFARE 2GO 无缝集成</strong> — 完整支持 NXP 的移动服务生态，实现手机即卡片的用户体验</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="45-协议代际变化详解">4.5 协议代际变化详解<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#45-%E5%8D%8F%E8%AE%AE%E4%BB%A3%E9%99%85%E5%8F%98%E5%8C%96%E8%AF%A6%E8%A7%A3" class="hash-link" aria-label="Direct link to 4.5 协议代际变化详解" title="Direct link to 4.5 协议代际变化详解" translate="no">​</a></h3>
<p>理解 EV1 → EV2 → EV3 的具体协议变化，对于升级迁移和兼容性设计至关重要。</p>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="451-命令集差异">4.5.1 命令集差异<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#451-%E5%91%BD%E4%BB%A4%E9%9B%86%E5%B7%AE%E5%BC%82" class="hash-link" aria-label="Direct link to 4.5.1 命令集差异" title="Direct link to 4.5.1 命令集差异" translate="no">​</a></h4>
<table><thead><tr><th>命令/特性</th><th>EV1</th><th>EV2</th><th>EV3</th></tr></thead><tbody><tr><td><strong>基础命令</strong>（GET_VERSION, SELECT, READ/WRITE 等）</td><td>✅</td><td>✅</td><td>✅</td></tr><tr><td><strong>AES 认证</strong> (<code>0xAA</code>)</td><td>✅</td><td>✅</td><td>✅</td></tr><tr><td><strong>ISO 认证</strong> (<code>0x1A</code>)</td><td>✅</td><td>✅</td><td>✅</td></tr><tr><td><strong>事务 MAC 文件</strong></td><td>❌</td><td>✅ 新增</td><td>✅</td></tr><tr><td><strong>Proximity Check 命令</strong></td><td>❌</td><td>✅ 新增</td><td>✅</td></tr><tr><td><strong>Originality Check 命令</strong></td><td>❌</td><td>✅ 新增</td><td>✅</td></tr><tr><td><strong>EV2 Secure Messaging 命令</strong></td><td>❌</td><td>✅ 新增</td><td>✅</td></tr><tr><td><strong>Virtual Card 选择命令</strong></td><td>❌</td><td>✅ 新增</td><td>✅</td></tr><tr><td><strong>Format Application（DAM 内存回收）</strong></td><td>❌</td><td>✅ 新增</td><td>✅</td></tr><tr><td><strong>SUN 消息命令</strong></td><td>❌</td><td>❌</td><td>✅ 新增</td></tr><tr><td><strong>Transaction Timer 命令</strong></td><td>❌</td><td>❌</td><td>✅ 新增</td></tr><tr><td><strong>ISO 7816-4 READ RECORDS</strong> (<code>0x62</code>)</td><td>❌</td><td>❌</td><td>✅ 新增</td></tr></tbody></table>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="452-密钥管理演进">4.5.2 密钥管理演进<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#452-%E5%AF%86%E9%92%A5%E7%AE%A1%E7%90%86%E6%BC%94%E8%BF%9B" class="hash-link" aria-label="Direct link to 4.5.2 密钥管理演进" title="Direct link to 4.5.2 密钥管理演进" translate="no">​</a></h4>
<table><thead><tr><th>密钥特性</th><th>EV1</th><th>EV2</th><th>EV3</th></tr></thead><tbody><tr><td><strong>每应用密钥集数</strong></td><td>1 套（最多 14 个密钥）</td><td>最多 16 套密钥集</td><td>最多 16 套密钥集</td></tr><tr><td><strong>每文件访问密钥数</strong></td><td>1 个/权限</td><td>最多 8 个/权限</td><td>最多 8 个/权限</td></tr><tr><td><strong>密钥滚动（Key Rollover）</strong></td><td>❌ 不支持</td><td>✅ 快速滚动</td><td>✅ 快速滚动</td></tr><tr><td><strong>密钥多样化</strong></td><td>✅（AN10922）</td><td>✅（AN10922）</td><td>✅（AN10922）</td></tr><tr><td><strong>Transaction MAC 密钥</strong></td><td>❌</td><td>✅ 独立 MAC 密钥</td><td>✅ 独立 MAC 密钥</td></tr><tr><td><strong>DAM 密钥预加载</strong></td><td>❌</td><td>❌</td><td>✅ 出厂预装</td></tr></tbody></table>
<h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="453-安全机制演进">4.5.3 安全机制演进<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#453-%E5%AE%89%E5%85%A8%E6%9C%BA%E5%88%B6%E6%BC%94%E8%BF%9B" class="hash-link" aria-label="Direct link to 4.5.3 安全机制演进" title="Direct link to 4.5.3 安全机制演进" translate="no">​</a></h4>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="46-三代演进时间线">4.6 三代演进时间线<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#46-%E4%B8%89%E4%BB%A3%E6%BC%94%E8%BF%9B%E6%97%B6%E9%97%B4%E7%BA%BF" class="hash-link" aria-label="Direct link to 4.6 三代演进时间线" title="Direct link to 4.6 三代演进时间线" translate="no">​</a></h3>
<!-- -->
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="五ntag-dnantag-424-dna">五、NTAG DNA（NTAG 424 DNA）<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#%E4%BA%94ntag-dnantag-424-dna" class="hash-link" aria-label="Direct link to 五、NTAG DNA（NTAG 424 DNA）" title="Direct link to 五、NTAG DNA（NTAG 424 DNA）" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="51-产品概述">5.1 产品概述<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#51-%E4%BA%A7%E5%93%81%E6%A6%82%E8%BF%B0" class="hash-link" aria-label="Direct link to 5.1 产品概述" title="Direct link to 5.1 产品概述" translate="no">​</a></h3>
<p>NTAG DNA（即 NTAG 424 DNA）是 NXP 推出的加密认证 NFC 标签芯片，其核心定位是<strong>产品防伪认证</strong>和<strong>品牌保护</strong>。与普通 NFC 标签不同，NTAG DNA 内置了 AES-128 硬件加密引擎，能够生成经过密码学签名的动态消息，为每个产品提供唯一的数字身份。</p>
<p>NTAG DNA 的核心价值在于：任何带有 NFC 功能的智能手机都可以读取标签中的加密消息，并通过云端验证服务确认产品的真伪，无需专用读卡设备。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="52-sunsecure-unique-nfc消息机制">5.2 SUN（Secure Unique NFC）消息机制<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#52-sunsecure-unique-nfc%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6" class="hash-link" aria-label="Direct link to 5.2 SUN（Secure Unique NFC）消息机制" title="Direct link to 5.2 SUN（Secure Unique NFC）消息机制" translate="no">​</a></h3>
<p>SUN 是 NTAG DNA 的核心技术，基于 <strong>AES CMAC</strong>（基于密码的消息认证码）算法生成加密消息。</p>
<!-- -->
<p><strong>SUN 消息的工作流程：</strong></p>
<ol>
<li class=""><strong>标签存储加密消息</strong> — NTAG DNA 内部存储了包含 CMAC 的加密 SUN 消息，该消息由共享密钥生成</li>
<li class=""><strong>手机 NFC 读取</strong> — 任何 NFC 设备（如智能手机）靠近标签即可读取 NDEF 记录</li>
<li class=""><strong>提取 CMAC</strong> — 应用从 NDEF 消息中的 SDM（Secure Dynamic Message）字段提取 CMAC</li>
<li class=""><strong>服务器验证</strong> — 应用将 CMAC、UID 和计数器值发送到验证服务器</li>
<li class=""><strong>真伪判定</strong> — 服务器使用与标签共享的 AES 密钥重新计算 CMAC，比对结果判定真伪</li>
</ol>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="53-ntag-dna-技术特性">5.3 NTAG DNA 技术特性<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#53-ntag-dna-%E6%8A%80%E6%9C%AF%E7%89%B9%E6%80%A7" class="hash-link" aria-label="Direct link to 5.3 NTAG DNA 技术特性" title="Direct link to 5.3 NTAG DNA 技术特性" translate="no">​</a></h3>
<table><thead><tr><th>特性</th><th>参数</th></tr></thead><tbody><tr><td>UID 长度</td><td>7 字节（支持随机 UID）</td></tr><tr><td>加密算法</td><td>AES-128</td></tr><tr><td>CMAC 长度</td><td>8 字节</td></tr><tr><td>NFC Forum 兼容性</td><td>Type 4 Tag</td></tr><tr><td>设备兼容性</td><td>所有 NFC 设备</td></tr><tr><td>动态 UID 计数器</td><td>支持（每次读取 UID 递增，防克隆）</td></tr><tr><td>NDEF 消息映射</td><td>支持</td></tr></tbody></table>
<p><strong>动态 UID 计数器</strong>是 NTAG DNA 防伪的关键机制之一：每次读取标签时，内部计数器递增并参与 CMAC 计算，使得每次读取生成的消息都不同。攻击者即使截获一次通信数据，也无法在后续读取中重放，有效防御克隆攻击。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="54-应用场景">5.4 应用场景<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#54-%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF" class="hash-link" aria-label="Direct link to 5.4 应用场景" title="Direct link to 5.4 应用场景" translate="no">​</a></h3>
<!-- -->
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="六taplinx--mifare-2go">六、TAPLINX / MIFARE 2GO<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#%E5%85%ADtaplinx--mifare-2go" class="hash-link" aria-label="Direct link to 六、TAPLINX / MIFARE 2GO" title="Direct link to 六、TAPLINX / MIFARE 2GO" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="61-平台概述">6.1 平台概述<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#61-%E5%B9%B3%E5%8F%B0%E6%A6%82%E8%BF%B0" class="hash-link" aria-label="Direct link to 6.1 平台概述" title="Direct link to 6.1 平台概述" translate="no">​</a></h3>
<p><strong>TAPLINX</strong> 是 NXP 提供的 MIFARE 卡片管理平台，<strong>MIFARE 2GO</strong> 是其云服务版本，面向 NFC 设备提供数字凭证的发行和管理能力。两者的结合构成了从传统卡片到移动设备的完整解决方案。</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="62-核心功能">6.2 核心功能<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#62-%E6%A0%B8%E5%BF%83%E5%8A%9F%E8%83%BD" class="hash-link" aria-label="Direct link to 6.2 核心功能" title="Direct link to 6.2 核心功能" translate="no">​</a></h3>
<ul>
<li class=""><strong>端到端云解决方案</strong> — 从凭证发行到使用管理的全流程云端化</li>
<li class=""><strong>数字凭证发行和风险管理平台</strong> — 安全、可扩展的凭证生命周期管理</li>
<li class=""><strong>支持 MIFARE DESFire 和 MIFARE Plus（安全层 3）</strong> — 覆盖 NXP 主流安全芯片</li>
<li class=""><strong>与现有 MIFARE 基础设施兼容</strong> — 无需更换现有读卡器和管理系统</li>
<li class=""><strong>支持所有 NFC 设备</strong> — 覆盖 Android、iOS 及其他 NFC 设备</li>
<li class=""><strong>"自带设备"（BYOD）策略</strong> — 用户使用自己的手机作为交通卡、门禁卡</li>
<li class=""><strong>与 Google Pay 集成</strong> — 支持移动交通票务，实现手机刷卡乘车</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="63-工作流程">6.3 工作流程<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#63-%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B" class="hash-link" aria-label="Direct link to 6.3 工作流程" title="Direct link to 6.3 工作流程" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="64-mifare-2go-与-ev3-的集成优势">6.4 MIFARE 2GO 与 EV3 的集成优势<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#64-mifare-2go-%E4%B8%8E-ev3-%E7%9A%84%E9%9B%86%E6%88%90%E4%BC%98%E5%8A%BF" class="hash-link" aria-label="Direct link to 6.4 MIFARE 2GO 与 EV3 的集成优势" title="Direct link to 6.4 MIFARE 2GO 与 EV3 的集成优势" translate="no">​</a></h3>
<p>EV3 与 MIFARE 2GO 的无缝集成带来了显著的生态优势：</p>
<ul>
<li class=""><strong>开箱即用</strong> — EV3 出厂预加载 DAM 密钥，通过 AppXplorer 服务可直接在 MIFARE 2GO 平台上管理</li>
<li class=""><strong>移动优先</strong> — 完整支持将 DESFire EV3 的应用迁移到手机 SE 中</li>
<li class=""><strong>快速部署</strong> — 运营商无需自建密钥管理和发行基础设施</li>
</ul>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="七开源替代方案与实战方式">七、开源替代方案与实战方式<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#%E4%B8%83%E5%BC%80%E6%BA%90%E6%9B%BF%E4%BB%A3%E6%96%B9%E6%A1%88%E4%B8%8E%E5%AE%9E%E6%88%98%E6%96%B9%E5%BC%8F" class="hash-link" aria-label="Direct link to 七、开源替代方案与实战方式" title="Direct link to 七、开源替代方案与实战方式" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="71-开源库与工具">7.1 开源库与工具<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#71-%E5%BC%80%E6%BA%90%E5%BA%93%E4%B8%8E%E5%B7%A5%E5%85%B7" class="hash-link" aria-label="Direct link to 7.1 开源库与工具" title="Direct link to 7.1 开源库与工具" translate="no">​</a></h3>
<table><thead><tr><th>工具/库</th><th>类型</th><th>说明</th></tr></thead><tbody><tr><td><strong>libfreefare</strong></td><td>开源 C 库</td><td>支持 MIFARE Classic / DESFire 读写操作，在 GitHub 上活跃维护</td></tr><tr><td><strong>pyscard</strong></td><td>Python 库</td><td>智能卡访问库，基于 PC/SC 接口，可配合 MIFARE 读卡器使用</td></tr><tr><td><strong>nfcpy</strong></td><td>Python 库</td><td>NFC 通信库，支持 13.56 MHz RFID/NFC 读写，适合 NTAG 系列</td></tr><tr><td><strong>RFIDDiscover</strong></td><td>NXP 官方免费工具</td><td>用于读写和调试 MIFARE 卡片，非开源但免费提供</td></tr><tr><td><strong>MIFARE Debug Tool</strong></td><td>NXP 官方调试工具</td><td>低层通信调试，辅助开发</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="72-实战方式对比">7.2 实战方式对比<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#72-%E5%AE%9E%E6%88%98%E6%96%B9%E5%BC%8F%E5%AF%B9%E6%AF%94" class="hash-link" aria-label="Direct link to 7.2 实战方式对比" title="Direct link to 7.2 实战方式对比" translate="no">​</a></h3>
<table><thead><tr><th>方式</th><th>适用芯片</th><th>难度</th><th>成本</th><th>说明</th></tr></thead><tbody><tr><td>NXP 官方 SDK + TAPLINX</td><td>DESFire EV2 / EV3</td><td>中</td><td>需 NDA</td><td>完整功能，官方技术支持</td></tr><tr><td>libfreefare + PC/SC 读卡器</td><td>DESFire EV1 / EV2</td><td>高</td><td>低（开源）</td><td>功能有限，社区维护</td></tr><tr><td>pyscard + 自行实现</td><td>所有 MIFARE</td><td>高</td><td>低</td><td>灵活但需要深入理解协议</td></tr><tr><td>nfcpy + NFC 读卡器</td><td>NTAG 系列</td><td>低</td><td>低</td><td>适合 NTAG DNA 读写验证</td></tr><tr><td>Android NFC API</td><td>所有 NFC 标签</td><td>中</td><td>低</td><td>手机端开发，支持 HCE 模拟</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="73-python-实战使用-pyscard-读取-desfire-卡">7.3 Python 实战：使用 pyscard 读取 DESFire 卡<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#73-python-%E5%AE%9E%E6%88%98%E4%BD%BF%E7%94%A8-pyscard-%E8%AF%BB%E5%8F%96-desfire-%E5%8D%A1" class="hash-link" aria-label="Direct link to 7.3 Python 实战：使用 pyscard 读取 DESFire 卡" title="Direct link to 7.3 Python 实战：使用 pyscard 读取 DESFire 卡" translate="no">​</a></h3>
<p>以下示例展示如何通过 pyscard 库连接 PC/SC 读卡器，向 MIFARE DESFire 卡片发送 APDU 命令：</p>
<div class="custom-code-block" data-language="python"><div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> smartcard</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">System </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> readers</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">from</span><span class="token plain"> smartcard</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">util </span><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> toHexString</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 获取所有读卡器</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">reader_list </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> readers</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">print</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-interpolation string" style="color:#e3116c">f"找到读卡器: </span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">{</span><span class="token string-interpolation interpolation">reader_list</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">}</span><span class="token string-interpolation string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">reader </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> readers</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">connection </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> reader</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">createConnection</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">connection</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">connect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 发送 APDU 命令（ISO 7816-4 兼容）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 获取卡片 UID</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">GET_UID </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0xFF</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0xCA</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x00</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x00</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x00</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">data</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sw1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sw2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> connection</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">transmit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">GET_UID</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">print</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-interpolation string" style="color:#e3116c">f"UID: </span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">{</span><span class="token string-interpolation interpolation">toHexString</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">(</span><span class="token string-interpolation interpolation">data</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">)</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">}</span><span class="token string-interpolation string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># 选择应用（MIFARE DESFire 原生命令通过 APDU 包装）</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">SELECT_APP </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token number" style="color:#36acaa">0x00</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0xA4</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x04</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x00</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x00</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x00</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">0x00</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">data</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sw1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> sw2 </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> connection</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">transmit</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">SELECT_APP</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">print</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-interpolation string" style="color:#e3116c">f"SW: </span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">{</span><span class="token string-interpolation interpolation">toHexString</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">(</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">[</span><span class="token string-interpolation interpolation">sw1</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">,</span><span class="token string-interpolation interpolation"> sw2</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">]</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">)</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">}</span><span class="token string-interpolation string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">connection</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">disconnect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div></div>
<p><strong>说明：</strong> MIFARE DESFire 的原生命令（如 Authenticate、Read Data 等）需要通过 ISO 7816-4 APDU 包装发送。上述示例中的 <code>GET_UID</code> 命令使用的是 PC/SC 标准的 GET DATA 指令，大多数读卡器都支持。选择应用时使用 SELECT 命令，AID 为全零表示选择 PICC 级别应用。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="74-python-实战使用-nfcpy-读取-ntag-dna">7.4 Python 实战：使用 nfcpy 读取 NTAG DNA<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#74-python-%E5%AE%9E%E6%88%98%E4%BD%BF%E7%94%A8-nfcpy-%E8%AF%BB%E5%8F%96-ntag-dna" class="hash-link" aria-label="Direct link to 7.4 Python 实战：使用 nfcpy 读取 NTAG DNA" title="Direct link to 7.4 Python 实战：使用 nfcpy 读取 NTAG DNA" translate="no">​</a></h3>
<p>以下示例展示如何通过 nfcpy 库读取 NTAG DNA 标签的 NDEF 消息：</p>
<div class="custom-code-block" data-language="python"><div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">import</span><span class="token plain"> nfc</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">def</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">on_connect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">tag</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">print</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-interpolation string" style="color:#e3116c">f"标签类型: </span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">{</span><span class="token string-interpolation interpolation">tag</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">.</span><span class="token string-interpolation interpolation builtin">type</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">}</span><span class="token string-interpolation string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">print</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-interpolation string" style="color:#e3116c">f"UID: </span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">{</span><span class="token string-interpolation interpolation">tag</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">.</span><span class="token string-interpolation interpolation">identifier</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">.</span><span class="token string-interpolation interpolation builtin">hex</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">(</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">)</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">}</span><span class="token string-interpolation string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token comment" style="color:#999988;font-style:italic"># 读取 NDEF 消息</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token keyword" style="color:#00009f">if</span><span class="token plain"> </span><span class="token builtin">isinstance</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">tag</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> nfc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">tt4</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Type4Tag</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        records </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> tag</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ndef</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">records</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token keyword" style="color:#00009f">for</span><span class="token plain"> record </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> records</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            </span><span class="token keyword" style="color:#00009f">print</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string-interpolation string" style="color:#e3116c">f"NDEF 记录: </span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">{</span><span class="token string-interpolation interpolation">record</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">.</span><span class="token string-interpolation interpolation">text </span><span class="token string-interpolation interpolation keyword" style="color:#00009f">if</span><span class="token string-interpolation interpolation"> </span><span class="token string-interpolation interpolation builtin">hasattr</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">(</span><span class="token string-interpolation interpolation">record</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">,</span><span class="token string-interpolation interpolation"> </span><span class="token string-interpolation interpolation string" style="color:#e3116c">'text'</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">)</span><span class="token string-interpolation interpolation"> </span><span class="token string-interpolation interpolation keyword" style="color:#00009f">else</span><span class="token string-interpolation interpolation"> record</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">.</span><span class="token string-interpolation interpolation">data</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">.</span><span class="token string-interpolation interpolation builtin">hex</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">(</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">)</span><span class="token string-interpolation interpolation punctuation" style="color:#393A34">}</span><span class="token string-interpolation string" style="color:#e3116c">"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">clf </span><span class="token operator" style="color:#393A34">=</span><span class="token plain"> nfc</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">ContactlessFrontend</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">clf</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">connect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">'rdwr'</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> on_connect</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">on_connect</span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div></div>
<p><strong>说明：</strong> nfcpy 通过系统级 NFC 接口（如 libnfc）与读卡器通信。对于 NTAG DNA，读取到的 NDEF 记录中包含 SDM（Secure Dynamic Message），其中嵌入了 SUN 消息和 CMAC。要验证真伪，需要将 CMAC 发送到运行验证服务的后端服务器。</p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="八安全架构总结">八、安全架构总结<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#%E5%85%AB%E5%AE%89%E5%85%A8%E6%9E%B6%E6%9E%84%E6%80%BB%E7%BB%93" class="hash-link" aria-label="Direct link to 八、安全架构总结" title="Direct link to 八、安全架构总结" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="81-完整安全链路">8.1 完整安全链路<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#81-%E5%AE%8C%E6%95%B4%E5%AE%89%E5%85%A8%E9%93%BE%E8%B7%AF" class="hash-link" aria-label="Direct link to 8.1 完整安全链路" title="Direct link to 8.1 完整安全链路" translate="no">​</a></h3>
<p>MIFARE DESFire 的安全架构覆盖了从物理层到应用层的完整链路：</p>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="82-三道双向认证流程">8.2 三道双向认证流程<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#82-%E4%B8%89%E9%81%93%E5%8F%8C%E5%90%91%E8%AE%A4%E8%AF%81%E6%B5%81%E7%A8%8B" class="hash-link" aria-label="Direct link to 8.2 三道双向认证流程" title="Direct link to 8.2 三道双向认证流程" translate="no">​</a></h3>
<p>三道双向认证（3-Pass Mutual Authentication）是 MIFARE DESFire 安全架构的核心，确保通信双方（读卡器和卡片）都持有合法密钥，并协商出唯一的会话密钥：</p>
<!-- -->
<p><strong>认证流程说明：</strong></p>
<ol>
<li class=""><strong>第一道</strong> — 读卡器向卡片发送认证请求，指定要使用的密钥编号。卡片生成随机数 B 并返回给读卡器</li>
<li class=""><strong>第二道</strong> — 读卡器生成自己的随机数 A，使用双方共享的密钥对 (A, B) 进行加密，生成 Token1 发送给卡片</li>
<li class=""><strong>第三道</strong> — 卡片解密 Token1，验证其中的随机数 B 是否与自己之前发送的一致。验证通过后，卡片使用共享密钥对 (B, A) 加密生成 Token2 发送给读卡器</li>
</ol>
<p>认证成功后，双方各自从认证过程中派生出<strong>会话密钥</strong>（Session Key），用于后续所有通信数据的加密和完整性保护。每次认证的随机数不同，因此会话密钥每次都不同，有效防止重放攻击。</p>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="83-安全特性层级总结">8.3 安全特性层级总结<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#83-%E5%AE%89%E5%85%A8%E7%89%B9%E6%80%A7%E5%B1%82%E7%BA%A7%E6%80%BB%E7%BB%93" class="hash-link" aria-label="Direct link to 8.3 安全特性层级总结" title="Direct link to 8.3 安全特性层级总结" translate="no">​</a></h3>
<!-- -->
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="总结">总结<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#%E6%80%BB%E7%BB%93" class="hash-link" aria-label="Direct link to 总结" title="Direct link to 总结" translate="no">​</a></h2>
<p>MIFARE DESFire 系列从 EV1 到 EV3 的演进，体现了 NXP 在非接触式智能卡安全领域的持续投入。每一代产品都在前代基础上增强了安全认证等级、扩展了功能特性、提升了性能参数。</p>
<ul>
<li class=""><strong>EV1</strong> 奠定了 DESFire 的应用-文件架构基础，提供了可靠的加密存储能力</li>
<li class=""><strong>EV2</strong> 引入了虚拟卡架构、事务 MAC、近场校验等高级安全特性，并通过 EAL 5+ 认证</li>
<li class=""><strong>EV3</strong> 在 EV2 基础上集成了 SUN 消息认证（与 NTAG DNA 互通）、事务定时器，并实现了与 MIFARE 2GO 的无缝集成</li>
</ul>
<p>NTAG DNA 则以轻量级的加密标签形态，为产品防伪和品牌保护提供了低成本、易部署的解决方案。配合 TAPLINX / MIFARE 2GO 云平台，NXP 构建了从芯片到云端、从物理卡片到移动设备的完整生态。</p>
<p>对于开发者而言，根据项目需求选择合适的接入方式至关重要：需要完整功能和官方支持时选择 NXP SDK + TAPLINX；需要低成本快速验证时选择 nfcpy 或 pyscard 等开源方案。</p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="九密钥管理深度解析">九、密钥管理深度解析<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#%E4%B9%9D%E5%AF%86%E9%92%A5%E7%AE%A1%E7%90%86%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90" class="hash-link" aria-label="Direct link to 九、密钥管理深度解析" title="Direct link to 九、密钥管理深度解析" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="91-密钥类型与认证命令">9.1 密钥类型与认证命令<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#91-%E5%AF%86%E9%92%A5%E7%B1%BB%E5%9E%8B%E4%B8%8E%E8%AE%A4%E8%AF%81%E5%91%BD%E4%BB%A4" class="hash-link" aria-label="Direct link to 9.1 密钥类型与认证命令" title="Direct link to 9.1 密钥类型与认证命令" translate="no">​</a></h3>
<p>MIFARE DESFire 支持多种加密算法，通过不同的认证命令码选择：</p>
<table><thead><tr><th>算法</th><th>密钥长度</th><th>认证命令码</th><th>安全性</th><th>推荐度</th></tr></thead><tbody><tr><td><strong>DES</strong></td><td>56 位（8 字节）</td><td><code>0x0A</code></td><td>低（已不推荐）</td><td>❌</td></tr><tr><td><strong>2K3DES</strong></td><td>112 位（16 字节）</td><td><code>0x0A</code></td><td>中（两个不同的 56 位密钥）</td><td>⚠️</td></tr><tr><td><strong>3K3DES</strong></td><td>168 位（24 字节）</td><td><code>0x0A</code></td><td>中高（三个不同的 56 位密钥）</td><td>⚠️</td></tr><tr><td><strong>AES-128</strong></td><td>128 位（16 字节）</td><td><code>0xAA</code></td><td>高</td><td>✅ 推荐</td></tr><tr><td><strong>ISO 14443-4</strong></td><td>取决于算法</td><td><code>0x1A</code></td><td>取决于算法</td><td>特定场景</td></tr></tbody></table>
<blockquote>
<p><strong>强烈建议</strong>：新项目统一使用 <strong>AES-128</strong> 认证（命令码 <code>0xAA</code>），DES 和 3DES 已被认为不够安全。</p>
</blockquote>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="92-密钥版本与防回滚">9.2 密钥版本与防回滚<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#92-%E5%AF%86%E9%92%A5%E7%89%88%E6%9C%AC%E4%B8%8E%E9%98%B2%E5%9B%9E%E6%BB%9A" class="hash-link" aria-label="Direct link to 9.2 密钥版本与防回滚" title="Direct link to 9.2 密钥版本与防回滚" translate="no">​</a></h3>
<p>每个密钥都关联一个 <strong>密钥版本号</strong>（Key Version，1 字节），这是密钥管理中的关键安全机制：</p>
<ul>
<li class=""><strong>版本递增规则</strong>：新密钥的版本号必须<strong>大于</strong>旧密钥版本号，否则卡片拒绝更新</li>
<li class=""><strong>防回滚攻击</strong>：即使攻击者获取了旧密钥，也无法将卡片密钥回滚到旧版本</li>
<li class=""><strong>版本协商</strong>：卡片在认证响应中返回当前密钥版本，读卡器据此选择正确的密钥</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="93-密钥变更流程changekey">9.3 密钥变更流程（ChangeKey）<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#93-%E5%AF%86%E9%92%A5%E5%8F%98%E6%9B%B4%E6%B5%81%E7%A8%8Bchangekey" class="hash-link" aria-label="Direct link to 9.3 密钥变更流程（ChangeKey）" title="Direct link to 9.3 密钥变更流程（ChangeKey）" translate="no">​</a></h3>
<p>密钥变更命令 <code>0xC4</code> 是 DESFire 中最敏感的操作之一，必须先完成认证才能执行：</p>
<!-- -->
<p><strong>APDU 格式：</strong></p>
<div class="custom-code-block"><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">CLA: 0x90</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">INS: 0xC4  (CHANGE KEY)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">P1:  KeyNo (目标密钥编号 0x00-0x0D)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">P2:  标志位</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Bit 0: 1 = 启用密钥版本自动更新</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Bit 1: 1 = 使用新密钥验证后续指令</span><br></div></code></pre></div></div></div>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="94-密钥多样化key-diversification">9.4 密钥多样化（Key Diversification）<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#94-%E5%AF%86%E9%92%A5%E5%A4%9A%E6%A0%B7%E5%8C%96key-diversification" class="hash-link" aria-label="Direct link to 9.4 密钥多样化（Key Diversification）" title="Direct link to 9.4 密钥多样化（Key Diversification）" translate="no">​</a></h3>
<p>NXP <strong>AN10922</strong> 应用笔记定义了标准的密钥多样化算法，确保每张卡、每个应用的密钥都是唯一的：</p>
<!-- -->
<p><strong>多样化方法：</strong></p>
<table><thead><tr><th>方法</th><th>算法</th><th>输入</th><th>输出</th></tr></thead><tbody><tr><td><strong>Module Diversification</strong></td><td>AES/MAC</td><td>UID</td><td>每卡唯一密钥</td></tr><tr><td><strong>Application Diversification</strong></td><td>AES/MAC</td><td>AID</td><td>每应用唯一密钥</td></tr><tr><td><strong>Key Identifier Diversification</strong></td><td>AES/MAC</td><td>KeyNo</td><td>每密钥槽唯一密钥</td></tr><tr><td><strong>综合多样化</strong></td><td>AES/MAC</td><td>UID + AID + KeyNo</td><td>每卡每应用每密钥唯一</td></tr></tbody></table>
<blockquote>
<p><strong>核心价值</strong>：即使主密钥泄露，攻击者也无法推导出具体某张卡、某个应用的密钥，因为多样化过程是不可逆的单向函数。</p>
</blockquote>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十开源-vs-闭源完整分类">十、开源 vs 闭源完整分类<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#%E5%8D%81%E5%BC%80%E6%BA%90-vs-%E9%97%AD%E6%BA%90%E5%AE%8C%E6%95%B4%E5%88%86%E7%B1%BB" class="hash-link" aria-label="Direct link to 十、开源 vs 闭源完整分类" title="Direct link to 十、开源 vs 闭源完整分类" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="101-sdk-与开发工具">10.1 SDK 与开发工具<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#101-sdk-%E4%B8%8E%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7" class="hash-link" aria-label="Direct link to 10.1 SDK 与开发工具" title="Direct link to 10.1 SDK 与开发工具" translate="no">​</a></h3>
<table><thead><tr><th>项目</th><th>分类</th><th>许可证/获取方式</th><th>说明</th></tr></thead><tbody><tr><td><strong>NXP MIFARE SDK</strong></td><td>🔒 闭源</td><td>NDA（保密协议）required</td><td>官方 SDK，含完整 DESFire 操作库和文档，需联系 NXP 签署 NDA</td></tr><tr><td><strong>libfreefare</strong></td><td>🟢 开源</td><td>LGPL v3</td><td>C 语言库（github.com/nfc-tools/libfreefare），支持 DESFire/Classic，基于 libnfc</td></tr><tr><td><strong>libnfc</strong></td><td>🟢 开源</td><td>LGPL v3</td><td>C 语言 NFC 底层库，libfreefare 的底层依赖</td></tr><tr><td><strong>nfcpy</strong></td><td>🟢 开源</td><td>BSD 2-Clause</td><td>Python NFC 库（github.com/nfcpy/nfcpy），支持 14443 通信、NDEF 读写</td></tr><tr><td><strong>pyscard</strong></td><td>🟢 开源</td><td>LGPL v2.1</td><td>Python 智能卡库，封装 PC/SC (WinSCard) 接口</td></tr><tr><td><strong>RFIDDiscover</strong></td><td>🔒 闭源</td><td>免费下载（NXP 官网）</td><td>图形化读写调试工具，免费但无源代码</td></tr><tr><td><strong>MIFARE Debug Tool</strong></td><td>🔒 闭源</td><td>NDA required</td><td>低层通信调试工具</td></tr><tr><td><strong>Android NFC API</strong></td><td>🟢 开源</td><td>Apache 2.0</td><td>Android 系统内置 NFC API，支持 HCE 模拟</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="102-技术文档与标准">10.2 技术文档与标准<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#102-%E6%8A%80%E6%9C%AF%E6%96%87%E6%A1%A3%E4%B8%8E%E6%A0%87%E5%87%86" class="hash-link" aria-label="Direct link to 10.2 技术文档与标准" title="Direct link to 10.2 技术文档与标准" translate="no">​</a></h3>
<table><thead><tr><th>文档/标准</th><th>分类</th><th>获取方式</th><th>说明</th></tr></thead><tbody><tr><td><strong>ISO/IEC 14443 标准</strong></td><td>📋 开放标准（付费）</td><td>需向 ISO 购买</td><td>国际标准，各部分单独定价</td></tr><tr><td><strong>ISO/IEC 7816-4 标准</strong></td><td>📋 开放标准（付费）</td><td>需向 ISO 购买</td><td>智能卡应用层命令标准</td></tr><tr><td><strong>NDEF 规范</strong></td><td>🟢 开放标准</td><td>NFC Forum 免费下载</td><td>NFC 数据交换格式规范</td></tr><tr><td><strong>AN10922（密钥多样化）</strong></td><td>🟢 公开</td><td>NXP 官网免费下载</td><td>无需 NDA 的应用笔记</td></tr><tr><td><strong>AN10834（PICC 选择）</strong></td><td>🟢 公开</td><td>NXP 官网免费下载</td><td>无需 NDA 的应用笔记</td></tr><tr><td><strong>DESFire EV1 数据手册</strong></td><td>⚠️ 部分公开</td><td>部分版本网上可找到</td><td>NXP 已标记为停产产品</td></tr><tr><td><strong>DESFire EV2 数据手册</strong></td><td>⚠️ 简版公开，完整版需 NDA</td><td>NXP 官网</td><td>安全机制细节需 NDA</td></tr><tr><td><strong>DESFire EV3 数据手册</strong></td><td>⚠️ 简版公开，完整版需 NDA</td><td>NXP 官网</td><td>安全机制细节需 NDA</td></tr><tr><td><strong>NTAG 424 DNA 数据手册</strong></td><td>🟢 公开</td><td>NXP 官网免费下载</td><td>含 SUN 消息、SDM 配置等技术细节</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="103-平台与服务">10.3 平台与服务<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#103-%E5%B9%B3%E5%8F%B0%E4%B8%8E%E6%9C%8D%E5%8A%A1" class="hash-link" aria-label="Direct link to 10.3 平台与服务" title="Direct link to 10.3 平台与服务" translate="no">​</a></h3>
<table><thead><tr><th>平台/服务</th><th>分类</th><th>获取方式</th><th>说明</th></tr></thead><tbody><tr><td><strong>MIFARE 2GO Cloud APIs</strong></td><td>🔒 闭源</td><td>授权许可（Licensed）</td><td>需联系 NXP 获取商业授权</td></tr><tr><td><strong>TAPLINX 管理平台</strong></td><td>🔒 闭源</td><td>授权许可</td><td>NXP 提供的云端卡片管理平台</td></tr><tr><td><strong>AppXplorer</strong></td><td>🔒 闭源</td><td>NXP 合作伙伴</td><td>EV3 出厂预加载密钥的配套服务</td></tr><tr><td><strong>Google Pay 集成</strong></td><td>🔒 闭源</td><td>Google 合作伙伴计划</td><td>移动交通票务集成</td></tr></tbody></table>
<!-- -->
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十一常见攻击与防御机制">十一、常见攻击与防御机制<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#%E5%8D%81%E4%B8%80%E5%B8%B8%E8%A7%81%E6%94%BB%E5%87%BB%E4%B8%8E%E9%98%B2%E5%BE%A1%E6%9C%BA%E5%88%B6" class="hash-link" aria-label="Direct link to 十一、常见攻击与防御机制" title="Direct link to 十一、常见攻击与防御机制" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="111-攻击类型与防御对照表">11.1 攻击类型与防御对照表<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#111-%E6%94%BB%E5%87%BB%E7%B1%BB%E5%9E%8B%E4%B8%8E%E9%98%B2%E5%BE%A1%E5%AF%B9%E7%85%A7%E8%A1%A8" class="hash-link" aria-label="Direct link to 11.1 攻击类型与防御对照表" title="Direct link to 11.1 攻击类型与防御对照表" translate="no">​</a></h3>
<table><thead><tr><th>攻击类型</th><th>攻击描述</th><th>EV1</th><th>EV2</th><th>EV3</th></tr></thead><tbody><tr><td><strong>窃听（Eavesdropping）</strong></td><td>截获射频通信数据</td><td>AES 加密防御</td><td>AES 加密防御</td><td>AES 加密防御</td></tr><tr><td><strong>中继攻击（Relay Attack）</strong></td><td>转发卡片信号到远端</td><td>❌ 无防御</td><td>✅ Proximity Check</td><td>✅ Proximity Check</td></tr><tr><td><strong>中间人攻击（MITM）</strong></td><td>拦截并篡改通信</td><td>❌ 无防御</td><td>❌ 无防御</td><td>✅ Transaction Timer</td></tr><tr><td><strong>克隆攻击（Cloning）</strong></td><td>复制卡片数据到空白卡</td><td>AES 加密防御</td><td>AES 加密防御</td><td>AES + SUN 防御</td></tr><tr><td><strong>重放攻击（Replay）</strong></td><td>重放之前截获的通信</td><td>随机数防御</td><td>随机数防御</td><td>随机数 + 计数器防御</td></tr><tr><td><strong>密钥回滚（Key Rollback）</strong></td><td>将密钥恢复到旧版本</td><td>密钥版本防御</td><td>密钥版本防御</td><td>密钥版本防御</td></tr><tr><td><strong>伪造芯片（Fake Card）</strong></td><td>使用非 NXP 芯片模拟</td><td>❌ 无防御</td><td>✅ Originality Check</td><td>✅ Originality Check</td></tr><tr><td><strong>暴力破解（Brute Force）</strong></td><td>尝试所有可能的密钥</td><td>密钥空间防御</td><td>密钥空间防御</td><td>密钥空间防御</td></tr><tr><td><strong>侧信道攻击（Side Channel）</strong></td><td>通过功耗/时序分析密钥</td><td>硬件防护</td><td>EAL 5+ 增强</td><td>EAL 5+ 增强</td></tr><tr><td><strong>故障注入（Fault Injection）</strong></td><td>通过异常条件触发错误行为</td><td>硬件防护</td><td>EAL 5+ 增强</td><td>EAL 5+ 增强</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="112-proximity-check近场校验详解">11.2 Proximity Check（近场校验）详解<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#112-proximity-check%E8%BF%91%E5%9C%BA%E6%A0%A1%E9%AA%8C%E8%AF%A6%E8%A7%A3" class="hash-link" aria-label="Direct link to 11.2 Proximity Check（近场校验）详解" title="Direct link to 11.2 Proximity Check（近场校验）详解" translate="no">​</a></h3>
<p>Proximity Check 是 EV2+ 引入的专门防御中继攻击的机制：</p>
<!-- -->
<p><strong>检测维度：</strong></p>
<ul>
<li class=""><strong>通信往返时间（Round-Trip Time）</strong>：正常通信延迟约 1-5ms，中继攻击通常 &gt; 50ms</li>
<li class=""><strong>射频场信号强度</strong>：远端卡片的场强特征与近场卡片不同</li>
<li class=""><strong>时间参数分析</strong>：综合多个时间维度的测量结果</li>
</ul>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="113-transaction-timer事务定时器详解">11.3 Transaction Timer（事务定时器）详解<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#113-transaction-timer%E4%BA%8B%E5%8A%A1%E5%AE%9A%E6%97%B6%E5%99%A8%E8%AF%A6%E8%A7%A3" class="hash-link" aria-label="Direct link to 11.3 Transaction Timer（事务定时器）详解" title="Direct link to 11.3 Transaction Timer（事务定时器）详解" translate="no">​</a></h3>
<p>EV3 独有的 Transaction Timer 为事务处理引入时间约束：</p>
<!-- -->
<blockquote>
<p><strong>与 Proximity Check 的区别</strong>：Proximity Check 检测的是物理距离（中继攻击），Transaction Timer 检测的是事务时间窗口（中间人延迟攻击），两者互补。</p>
</blockquote>
<h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="十二选型建议与实战路线图">十二、选型建议与实战路线图<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#%E5%8D%81%E4%BA%8C%E9%80%89%E5%9E%8B%E5%BB%BA%E8%AE%AE%E4%B8%8E%E5%AE%9E%E6%88%98%E8%B7%AF%E7%BA%BF%E5%9B%BE" class="hash-link" aria-label="Direct link to 十二、选型建议与实战路线图" title="Direct link to 十二、选型建议与实战路线图" translate="no">​</a></h2>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="121-芯片选型决策树">12.1 芯片选型决策树<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#121-%E8%8A%AF%E7%89%87%E9%80%89%E5%9E%8B%E5%86%B3%E7%AD%96%E6%A0%91" class="hash-link" aria-label="Direct link to 12.1 芯片选型决策树" title="Direct link to 12.1 芯片选型决策树" translate="no">​</a></h3>
<!-- -->
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="122-开发路线图建议">12.2 开发路线图建议<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#122-%E5%BC%80%E5%8F%91%E8%B7%AF%E7%BA%BF%E5%9B%BE%E5%BB%BA%E8%AE%AE" class="hash-link" aria-label="Direct link to 12.2 开发路线图建议" title="Direct link to 12.2 开发路线图建议" translate="no">​</a></h3>
<table><thead><tr><th>阶段</th><th>目标</th><th>推荐方案</th><th>预估时间</th></tr></thead><tbody><tr><td><strong>阶段 1：原型验证</strong></td><td>快速验证 NFC 通信可行性</td><td>nfcpy + USB NFC 读卡器</td><td>1-2 周</td></tr><tr><td><strong>阶段 2：功能开发</strong></td><td>实现完整的读写操作</td><td>pyscard + PC/SC 读卡器</td><td>2-4 周</td></tr><tr><td><strong>阶段 3：安全加固</strong></td><td>实现 AES 认证、密钥管理</td><td>NXP SDK（需 NDA）或 libfreefare</td><td>2-4 周</td></tr><tr><td><strong>阶段 4：生产部署</strong></td><td>大规模发卡、密钥管理</td><td>TAPLINX / MIFARE 2GO</td><td>4-8 周</td></tr><tr><td><strong>阶段 5：移动扩展</strong></td><td>支持手机 NFC</td><td>Android NFC API + HCE</td><td>4-8 周</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="123-硬件选型参考">12.3 硬件选型参考<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#123-%E7%A1%AC%E4%BB%B6%E9%80%89%E5%9E%8B%E5%8F%82%E8%80%83" class="hash-link" aria-label="Direct link to 12.3 硬件选型参考" title="Direct link to 12.3 硬件选型参考" translate="no">​</a></h3>
<table><thead><tr><th>读卡器/设备</th><th>接口</th><th>支持芯片</th><th>价格区间</th><th>推荐场景</th></tr></thead><tbody><tr><td><strong>ACR122U</strong></td><td>USB</td><td>DESFire / NTAG / Classic</td><td>$30-50</td><td>开发调试、原型验证</td></tr><tr><td><strong>Sony RC-S380</strong></td><td>USB</td><td>DESFire / NTAG / FeliCa</td><td>$40-60</td><td>Android 兼容开发</td></tr><tr><td><strong>NXP PN532 模块</strong></td><td>SPI/I2C/UART</td><td>DESFire / NTAG / Classic</td><td>$5-15</td><td>嵌入式集成</td></tr><tr><td><strong>NXP CLRC663</strong></td><td>SPI/I2C/UART</td><td>DESFire EV2/EV3 全功能</td><td>$3-8</td><td>量产嵌入式设备</td></tr><tr><td><strong>Android 手机</strong></td><td>内置 NFC</td><td>所有 NFC 标签</td><td>—</td><td>移动端应用、HCE</td></tr></tbody></table>
<h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="124-关键注意事项">12.4 关键注意事项<a href="https://rainlib.vercel.app/en/blog/nfc-mifare-desfire-ev1-ev2-ev3-ntag-dna-taplinx#124-%E5%85%B3%E9%94%AE%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9" class="hash-link" aria-label="Direct link to 12.4 关键注意事项" title="Direct link to 12.4 关键注意事项" translate="no">​</a></h3>
<ol>
<li class=""><strong>NDA 策略</strong>：如果项目需要 EV2/EV3 的高级安全特性（Secure Messaging、Proximity Check 等），建议尽早联系 NXP 签署 NDA 获取完整技术文档</li>
<li class=""><strong>密钥管理基础设施</strong>：生产环境必须建立完善的密钥管理体系，包括主密钥保护、多样化策略、密钥轮换计划</li>
<li class=""><strong>向后兼容性</strong>：EV3 完全向后兼容 EV2 和 EV1，但 EV2 的部分特性（如 Proximity Check）在 EV1 上不可用</li>
<li class=""><strong>开源方案局限</strong>：libfreefare 等开源库对 EV2/EV3 的新特性支持有限，复杂项目建议使用 NXP 官方 SDK</li>
<li class=""><strong>合规性要求</strong>：支付类应用需符合 PCI DSS 等安全标准，交通票务需符合当地行业标准</li>
</ol>]]></content:encoded>
            <category>NFC</category>
            <category>MIFARE</category>
            <category>Security</category>
            <category>IoT</category>
            <category>NXP</category>
        </item>
    </channel>
</rss>