JS实现WebRTC低延迟直播:从推流到拉流的完整方案
本文提供一套完整的JavaScript实现WebRTC超低延迟直播方案。基于浏览器原生API与标准化WHIP/WHEP信令协议,详细讲解从摄像头媒体采集、推流端SDP协商,到流媒体服务对接、拉流端音视频渲染及断网自愈的全套落地代码与实操步骤。
构建端到端延时低于 500 毫秒的超低延迟直播系统,核心在于跳过传统的 RTMP/HLS TCP 拥塞缓冲机制,直接基于浏览器原生支持的 WebRTC 技术栈。本文提供一套基于现代 JavaScript 与标准 WHIP/WHEP 信令规范的完整推拉流方案,不依赖任何第三方重量级 SDK,通过原生 RTCPeerConnection 与流媒体服务器建立轻量、稳定的音视频链路。
为什么一对多直播不能直接用 P2P Mesh
初学者在接触 WebRTC 时,往往首先看到双人视频通话的 P2P 示例。但在直播场景中,主播面对的是成百上千名观众。如果采用浏览器与每个观众点对点直连(Mesh 网状拓扑),主播的上行带宽消耗与 CPU 编码开销将随观众数量呈线性暴增。家用网络通常仅支持 3 到 5 路高清推流,超出后便会出现严重的丢包与卡顿。
因此,工业级 WebRTC 直播必须引入流媒体服务器作为 SFU(Selective Forwarding Unit,选择性转发单元):
- 推流端(主播):仅与服务器建立一条上行 PeerConnection,上传一路高清音视频流。
- 流媒体服务器:负责 SRTP 数据包解密、路由复制,并维持与全网观众的连接。
- 拉流端(观众):各自与服务器建立一条下行 PeerConnection,仅接收音视频数据,不上传本地媒体。
信令协议标准化:WHIP 与 WHEP
在以往的 WebRTC 实现中,每个流媒体服务器(如 Janus、MediaSoup)都有自己私有的 WebSocket 或 JSON-RPC 信令协议,导致推流和拉流代码极难通用。IETF 推出的 WHIP(RFC 9339)与 WHEP 规范统一了信令流程,将复杂的 SDP 协商简化为标准 HTTP POST 请求:
- 客户端创建本地
RTCPeerConnection并生成 SDP Offer。 - 客户端向服务器的推流(WHIP)或拉流(WHEP)URL 发送标准 HTTP POST,请求体为纯文本 SDP。
- 服务器校验通过后返回 HTTP 201 状态码,响应体包含服务器生成的 SDP Answer。
- 客户端调用
setRemoteDescription,底层自动完成 DTLS 加密握手与 UDP 连通性检测,流媒体通道即刻建立。
推流端实现:采集、配置与 WHIP 推流
推流端运行在主播浏览器中,核心步骤包括:调用 getUserMedia 捕获音视频、将音轨与视轨添加至 RTCPeerConnection、等待 ICE 候选收集完毕以生成包含完整路由信息的 SDP,最后通过 HTTP POST 提交至服务器。
推流端完整代码 (whip-publisher.js)
/**
* WebRTC WHIP 推流客户端
* @param {string} whipUrl - 流媒体服务器 WHIP 推流接口地址
* @param {HTMLVideoElement} previewVideo - 本地监看 video 标签
*/
export async function startPublish(whipUrl, previewVideo) {
// 1. 获取本地音视频流
const stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true, noiseSuppression: true },
video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }
});
// 绑定本地预览,静音避免回声
previewVideo.srcObject = stream;
previewVideo.muted = true;
await previewVideo.play();
// 2. 实例化 PeerConnection
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
bundlePolicy: 'max-bundle'
});
// 3. 将本地音视频轨道加入 PeerConnection
stream.getTracks().forEach(track => {
pc.addTrack(track, stream);
});
// 4. 生成 SDP Offer 并等待 ICE 候选收集完成 (Non-trickle ICE)
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const completeOfferSdp = await new Promise((resolve) => {
if (pc.iceGatheringState === 'complete') {
resolve(pc.localDescription.sdp);
} else {
const checkState = () => {
if (pc.iceGatheringState === 'complete') {
pc.removeEventListener('icegatheringstatechange', checkState);
resolve(pc.localDescription.sdp);
}
};
pc.addEventListener('icegatheringstatechange', checkState);
// 超时兜底:若 1500ms 仍未结束,直接使用已有候选
setTimeout(() => resolve(pc.localDescription.sdp), 1500);
}
});
// 5. 向流媒体服务器发起 WHIP HTTP POST 请求
const response = await fetch(whipUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
body: completeOfferSdp
});
if (response.status !== 200 && response.status !== 201) {
throw new Error(`推流失败,服务器返回状态码: ${response.status}`);
}
// 6. 获取服务器生成的 Answer SDP 并设为远端描述
const answerSdp = await response.text();
await pc.setRemoteDescription({
type: 'answer',
sdp: answerSdp
});
console.log('WebRTC 推流成功建立!');
return { pc, stream };
}
createOffer() 之后立刻发送 SDP,SDP 内部可能尚未包含主机 IP 与端口(candidate 属性),容易导致 NAT 穿透握手失败。上述代码通过监听 icegatheringstatechange 等待完整收集后再 POST,大幅提升建连成功率。
拉流端实现:WHEP 握手与音视频渲染
拉流端无需向用户索要摄像头或麦克风权限。核心在于创建 RTCPeerConnection 并使用 addTransceiver 显式声明仅接收(recvonly)视频与音频,生成拉流 Offer,与服务器 WHEP 接口完成交换后,在 ontrack 事件中获取下发的媒体流。
拉流端完整代码 (whep-player.js)
/**
* WebRTC WHEP 拉流播放器
* @param {string} whepUrl - 流媒体服务器 WHEP 拉流接口地址
* @param {HTMLVideoElement} remoteVideo - 用于展示远端直播画面的 video 标签
*/
export async function startPlay(whepUrl, remoteVideo) {
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
bundlePolicy: 'max-bundle'
});
// 1. 显式声明下行方向:只接收音视频,不发送本地媒体
pc.addTransceiver('video', { direction: 'recvonly' });
pc.addTransceiver('audio', { direction: 'recvonly' });
// 2. 监听远端媒体轨道到达事件
pc.ontrack = (event) => {
if (event.streams && event.streams[0]) {
remoteVideo.srcObject = event.streams[0];
}
};
// 3. 生成拉流端 SDP Offer 并等待 ICE 候选收集
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const completeOfferSdp = await new Promise((resolve) => {
if (pc.iceGatheringState === 'complete') {
resolve(pc.localDescription.sdp);
} else {
const checkState = () => {
if (pc.iceGatheringState === 'complete') {
pc.removeEventListener('icegatheringstatechange', checkState);
resolve(pc.localDescription.sdp);
}
};
pc.addEventListener('icegatheringstatechange', checkState);
setTimeout(() => resolve(pc.localDescription.sdp), 1500);
}
});
// 4. HTTP POST 提交 WHEP 请求
const response = await fetch(whepUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
body: completeOfferSdp
});
if (response.status !== 200 && response.status !== 201) {
throw new Error(`拉流失败,服务器返回状态码: ${response.status}`);
}
// 5. 设置远端 Answer SDP
const answerSdp = await response.text();
await pc.setRemoteDescription({
type: 'answer',
sdp: answerSdp
});
// 6. 播放远端视频并妥善处理浏览器自动播放受限策略
try {
await remoteVideo.play();
} catch (err) {
console.warn('非静音自动播放受限,切换为静音起播:', err);
remoteVideo.muted = true;
await remoteVideo.play();
}
// 7. 网络异常状态自愈监听
pc.onconnectionstatechange = () => {
if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') {
console.warn('WebRTC 连接断开,3秒后自动尝试重连...');
setTimeout(() => {
pc.close();
startPlay(whepUrl, remoteVideo).catch(console.error);
}, 3000);
}
};
return pc;
}
本地环境验证与服务器配合 (以 SRS 为例)
为了验证上述纯前端 JS 代码,需要一台具备标准 WHIP/WHEP 支持的流媒体服务器。国内开源成熟度最高的 SRS(Simple Realtime Server)从 5.0 起原生支持 WHIP 与 WHEP。在本地开发测试时,通过一条 Docker 命令即可完成搭建:
# 启动支持 WebRTC / WHIP / WHEP 的 SRS 容器
docker run --rm -it -p 1985:1985 -p 8080:8080 -p 8000:8000/udp \
-e CANDIDATE="127.0.0.1" \
ossrs/srs:5 \
./objs/srs -c conf/rtc.conf
参数说明:
1985:API 端口与 WHIP/WHEP HTTP 信令端口。8000/udp:WebRTC 媒体传输端口(UDP)。测试公网或局域网时需将CANDIDATE环境变量替换为本机的真实局域网 IP 或公网 IP。8080:静态页面与控制台端口。
在前端调用推流和拉流方法时,传入对应的流地址格式如下:
- 推流地址 (WHIP):
http://127.0.0.1:1985/rtc/v1/whip/?app=live&stream=test - 拉流地址 (WHEP):
http://127.0.0.1:1985/rtc/v1/whep/?app=live&stream=test
localhost 和 127.0.0.1 之外,调用 navigator.mediaDevices.getUserMedia 必须在 HTTPS 协议下运行。如果部署在局域网 IP 或域名,推流页面必须配置 SSL/TLS 证书,否则无法调起摄像头。
生产落地高频踩坑与关键优化
1. 视频编解码器匹配(H.264 与 VP8)
Safari 对 VP8 的硬件解码支持有限,而在某些 Android 机型上 H.264 Baseline Profile 支持更完备。若要保证 iOS Safari 与 Chrome 观众都能顺畅拉流,建议优先采用 H.264。可以通过修改 RTCRtpSender.setParameters() 或在 SDP 中调整编码优先级来实现优先选择 H.264。
2. 延迟与卡顿的权衡:JitterBuffer 调节
WebRTC 内部自带自适应抖动缓冲区(JitterBuffer)。在网络抖动时,播放器为了保证画面连贯性会自动拉大缓冲区,可能导致延时从 200ms 攀升到 800ms。
现代 Chromium 浏览器支持通过 receiver.jitterBufferTarget 显式指定期望的抖动缓冲毫秒数:
// 针对实时控制或极限低延时场景,将目标缓冲区设为 100ms
pc.getReceivers().forEach(receiver => {
if (receiver.track && receiver.track.kind === 'video' && 'jitterBufferTarget' in receiver) {
receiver.jitterBufferTarget = 100; // 毫秒
}
});
3. 浏览器自动播放限制(Autoplay Policy)的兜底
拉流端如果不加处理直接调用 video.play(),由于包含有声声轨,极易被 Chrome 等浏览器的自动播放策略拒绝(抛出 NotAllowedError)。
业界标准处理逻辑是:优先尝试正常播放;若捕获到拒绝错误,立刻将 video.muted = true 静音重试起播,并在页面中央渲染一个“点击恢复声音”的交互浮层,由用户触发点击动作后解除静音。
方案总结
通过采用“浏览器采集 + 原生 RTCPeerConnection + WHIP 推流 + SFU 转发 + WHEP 拉流”的架构,开发者无需集成复杂的第三方黑盒 SDK,仅用百余行现代原生 JavaScript 即可实现稳定、跨平台、端到端延时低于 500 毫秒的高性能直播系统。这一方案不仅天然兼容跨平台标准,也为实时连麦互动、远程低延时监控及在线互动课堂等场景提供了坚实的基础能力。
JavaScript零基础入门:从环境搭建到运行第一段代码
2026-11-05 15:29:58
HTML链接中javascript:void(0)的作用及现代替代方案
2026-11-05 10:07:47
在线调试 JS 时怎么引入 jQuery 或 Vue 等第三方库
2026-11-05 08:53:56
原生JavaScript实现简易颜色选择器原理与代码
2026-11-03 23:22:19
JS原生表单序列化:FormData与URLSearchParams用法详解
2026-11-03 23:13:21







