跳到主要内容

L0-echo-demo

最小可运行的 WebRTC 示例:单页面内创建两个 RTCPeerConnection 互相直连,跳过信令和 NAT 穿透,专注理解三件事:

  1. MediaStream 怎么从摄像头流入 RTCPeerConnection
  2. SDP Offer/Answer 长什么样。
  3. ICE 候选是什么、怎么交换。

1. 这个 demo 教会你什么

  • 看到完整 SDP(点按钮在控制台打印)。
  • 看到 host / srflx / relay 候选的形态(仅 host,因为同页面)。
  • 看到 iceConnectionState 状态机的真实切换。
  • 看到 track 事件里 MediaStream 是怎么到对端的。

2. 它

  • 真实的信令协议(这里两个 PC 直接互相交换 SDP)。
  • 跨网络穿透(同页面 = 完美连通)。
  • TURN(同页面不需要)。

下一步去看 L1-1v1-call/,那里加了 WebSocket 信令和 coturn。

3. 跑起来

# 本目录已经是纯静态,直接起任意静态服务器即可
npx http-server -p 5173 -c-1
# 或
python3 -m http.server 5173
# 或
caddy file-server --listen :5173

打开 http://localhost:5173/ ,点 Start,浏览器要求摄像头权限。

必须用 localhosthttps://,否则 getUserMedia 直接被拒。

4. 文件清单

文件作用
index.html两个 <video> 元素 + 控制按钮
main.js全部 WebRTC 逻辑(约 130 行)
style.css简单布局

5. 关键代码导读

// 创建两个 PeerConnection,模拟两端
const pcA = new RTCPeerConnection({ iceServers: [] });
const pcB = new RTCPeerConnection({ iceServers: [] });

// 把 A 的本地候选喂给 B,反之亦然(这就是"信令"在做的事)
pcA.onicecandidate = (e) => e.candidate && pcB.addIceCandidate(e.candidate);
pcB.onicecandidate = (e) => e.candidate && pcA.addIceCandidate(e.candidate);

// B 收到 A 推过来的 Track,挂到第二个 video
pcB.ontrack = (e) => { remoteVideo.srcObject = e.streams[0]; };

// 采集摄像头,加到 A
const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
stream.getTracks().forEach(t => pcA.addTrack(t, stream));

// Offer/Answer:浏览器内建的 SDP 协商
const offer = await pcA.createOffer();
await pcA.setLocalDescription(offer);
await pcB.setRemoteDescription(offer);
const answer = await pcB.createAnswer();
await pcB.setLocalDescription(answer);
await pcA.setRemoteDescription(answer);

整个过程没有任何服务端。这是理解 WebRTC 三件套最干净的方式。

6. 实验建议

跑通之后,建议自己改这几处看现象:

  1. addTrack 移到 createOffer 之后 → SDP 没有 m-line。
  2. pcA.onicecandidate 注释掉 → iceConnectionState 永远 checking
  3. audio:true 删掉 → SDP 里只有一条 m=video
  4. console.log(offer.sdp) 看完整 SDP,对照 SDP 结构详解
  5. 打开 chrome://webrtc-internals 看实时 stats。

7. 验证清单

跑成功的标志:

  • 左边 <video> 显示自己的摄像头画面。
  • 右边 <video> 显示同样的画面(来自 pcB)。
  • 状态栏显示 connected
  • DevTools 控制台打印出两份 SDP,各自含一条 m=audio 和一条 m=video
  • 点击 Print Stats 打印 inbound-rtpframesPerSecond 大约 30。

8. 常见错误

现象原因
NotAllowedError用户拒绝授权
getUserMedia is not a function用了 http:// 非 localhost 的地址
<video> 黑屏但状态 connected忘了 playsinlineautoplay
状态卡 checking注释掉了 onicecandidate