WebRTC 直播模块实战01
WebRTC 直播模块实战
本篇目标:
- 写观众端 viewer.html:加入房间、接收 offer、回传 answer
- 接收 ontrack 播放远端视频
- 解释多观众下为何主播要维护多个 PeerConnection
- 给你一套“常见失败排查清单”
1. 观众端:viewer.html
创建 public/viewer.html:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>WebRTC 直播 - 观众拉流</title>
<style>
body { font-family: Arial; padding: 16px; }
video { width: 640px; background: #000; border-radius: 8px; }
input, button { padding: 8px 10px; margin: 6px 0; }
.log { white-space: pre-wrap; background:#f6f6f6; padding:10px; border-radius:8px; height:260px; overflow:auto; }
</style>
</head>
<body>
<h2>观众端(拉流)</h2>
<div>
房间号:<input id="roomId" value="room1" />
<button id="btnJoin">加入房间观看</button>
</div>
<video id="remoteVideo" autoplay playsinline></video>
<div class="log" id="log"></div>
<script>
const remoteVideo = document.getElementById("remoteVideo");
const logEl = document.getElementById("log");
function log(...args) {
logEl.textContent += args.join(" ") + "\n";
logEl.scrollTop = logEl.scrollHeight;
}
let ws;
let clientId;
let roomId;
let publisherId;
let pc = null;
const rtcConfig = {
iceServers: [
{ urls: "stun:stun.l.google.com:19302" }
]
};
function connectWS() {
return new Promise((resolve, reject) => {
ws = new WebSocket(`ws://${location.host}`);
ws.onopen = () => resolve();
ws.onerror = (e) => reject(e);
ws.onmessage = async (evt) => {
const msg = JSON.parse(evt.data);
if (msg.type === "welcome") {
clientId = msg.clientId;
log("[WS] welcome clientId =", clientId);
return;
}
if (msg.type === "joined") {
publisherId = msg.publisherId;
log("[WS] joined as viewer, room =", msg.roomId, "publisherId =", publisherId || "(none yet)");
return;
}
if (msg.type === "publisher_left") {
log("[ROOM] 主播已离开");
cleanup();
return;
}
if (msg.type === "signal") {
const fromId = msg.fromId; // 通常是 publisherId
const payload = msg.payload;
await handleSignal(fromId, payload);
return;
}
};
});
}
async function ensurePeerConnection() {
if (pc) return pc;
pc = new RTCPeerConnection(rtcConfig);
// 收到远端媒体(这一步就是“拉流”)
pc.ontrack = (e) => {
// e.streams[0] 是包含音视频轨道的 MediaStream
remoteVideo.srcObject = e.streams[0];
log("[MEDIA] ontrack received, stream id =", e.streams[0].id);
};
pc.onicecandidate = (e) => {
if (e.candidate) {
ws.send(JSON.stringify({
type: "signal",
roomId,
targetId: publisherId,
payload: { kind: "ice", candidate: e.candidate }
}));
}
};
pc.onconnectionstatechange = () => {
log("[PC] state =", pc.connectionState);
};
return pc;
}
async function handleSignal(fromId, payload) {
// 第一次收到 offer 时,记住主播是谁
if (!publisherId) publisherId = fromId;
if (payload.kind === "offer") {
log("[SIGNAL] offer received from", fromId);
const pc = await ensurePeerConnection();
await pc.setRemoteDescription(new RTCSessionDescription(payload.sdp));
log("[PC] setRemoteDescription(offer) OK");
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({
type: "signal",
roomId,
targetId: fromId,
payload: { kind: "answer", sdp: answer }
}));
log("[SIGNAL] answer sent");
return;
}
if (payload.kind === "ice") {
try {
await ensurePeerConnection();
await pc.addIceCandidate(new RTCIceCandidate(payload.candidate));
} catch (e) {
log("[PC] addIceCandidate error", e.message);
}
return;
}
}
function cleanup() {
if (pc) {
pc.close();
pc = null;
}
remoteVideo.srcObject = null;
}
async function joinAsViewer() {
roomId = document.getElementById("roomId").value.trim();
if (!roomId) {
log("[ERR] 房间号不能为空");
return;
}
await connectWS();
ws.send(JSON.stringify({ type: "join", role: "viewer", roomId }));
log("[OK] 已加入房间,等待主播 offer...");
}
document.getElementById("btnJoin").onclick = () => joinAsViewer().catch(e => log("[ERR]", e.message));
</script>
</body>
</html>
2. 为什么多观众下:主播必须开多个 PeerConnection?
因为在 P2P 架构中:
- 每个观众都在不同网络环境
- ICE 打洞路径不同
- 所以主播需要:“每个观众一个连接”
这也是 P2P 直播的天然瓶颈:观众越多,主播上行压力越大。
3. 运行步骤
1)启动服务器
node server.js
2)打开主播端
http://localhost:3000/publisher.html- 点:打开摄像头 → 进入房间
3)打开观众端(可以开多个标签页模拟多人)
http://localhost:3000/viewer.html- 点:加入房间观看
4. 常见问题排查
4.1 摄像头打不开
- 必须在
http://localhost或https下运行 - 浏览器权限没开:地址栏小锁点开允许
4.2 观众一直黑屏、没有 ontrack
- 检查主播是否真的
addTrack() - 看 console 是否有 SDP/ICE 报错
- 打开
chrome://webrtc-internals/(Chrome 超强调试页)
4.3 本机 OK,局域网/跨网不通
- 说明 NAT 穿透失败,需要 TURN
- 企业网/对称 NAT 常见
- 后续升级:加 TURN(coturn)或直接上 SFU
4.4 声音回声
- 主播本地预览要
muted - 观众播放不要 muted(否则没声)

浙公网安备 33010602011771号