L0-echo-demo
最小可运行的 WebRTC 示例:单页面内创建两个 RTCPeerConnection 互相直连,跳过信令和 NAT 穿透,专注理解三件事:
MediaStream怎么从摄像头流入RTCPeerConnection。- SDP Offer/Answer 长什么样。
- 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,浏览器要求摄像头权限。
必须用
localhost或https://,否则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. 实验建议
跑通之后,建议自己改这几处看现象:
- 把
addTrack移到createOffer之后 → SDP 没有 m-line。 - 把
pcA.onicecandidate注释掉 →iceConnectionState永远checking。 - 把
audio:true删掉 → SDP 里只有一条m=video。 console.log(offer.sdp)看完整 SDP,对照 SDP 结构详解。- 打开
chrome://webrtc-internals看实时 stats。
7. 验证清单
跑成功的标志:
- 左边
<video>显示自己的摄像头画面。 - 右边
<video>显示同样的画面(来自 pcB)。 - 状态栏显示
connected。 - DevTools 控制台打印出两份 SDP,各自含一条
m=audio和一条m=video。 - 点击 Print Stats 打印
inbound-rtp的framesPerSecond大约 30。
8. 常见错误
| 现象 | 原因 |
|---|---|
NotAllowedError | 用户拒绝授权 |
getUserMedia is not a function | 用了 http:// 非 localhost 的地址 |
<video> 黑屏但状态 connected | 忘了 playsinline 或 autoplay |
状态卡 checking | 注释掉了 onicecandidate |