项目实战:直播系统(二)——WHIP推流与WHEP拉流实现
上一章我们把直播系统的整体架构搭起来了。今天咱们来啃两块硬骨头:WHIP推流客户端和WHEP拉流播放器。说白了,就是让主播能把自己的画面声音推上去,观众能流畅地看直播。
我个人习惯把这两个模块放在一起讲,因为它们本质上是WebRTC的"发"和"收"两端。你搞懂了推流,拉流就是反过来走一遍流程。但这里有个坑——延迟控制和QoS优化,才是真正拉开专业和业余差距的地方。
WHIP推流客户端实现
WHIP(WebRTC-HTTP ingestion protocol)是IETF的标准协议。它让浏览器或客户端通过HTTP信令,把音视频流推给服务器。我当年第一次接触时觉得这玩意儿挺绕,后来发现核心就三步:
- 创建PeerConnection
- 采集本地音视频
- 通过HTTP交换SDP
来看代码。这是推流客户端的核心逻辑:
class WHIPPusher {
constructor(whipEndpoint) {
this.endpoint = whipEndpoint;
this.pc = null;
this.stream = null;
}
async start() {
// 1. 采集本地流
this.stream = await navigator.mediaDevices.getUserMedia({
video: { width: 1280, height: 720, frameRate: 30 },
audio: { echoCancellation: true, noiseSuppression: true }
});
// 2. 创建PeerConnection
this.pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
// 3. 添加轨道
this.stream.getTracks().forEach(track => {
this.pc.addTrack(track, this.stream);
});
// 4. 创建Offer并发送
const offer = await this.pc.createOffer();
await this.pc.setLocalDescription(offer);
// 5. 通过HTTP POST发送给WHIP服务器
const response = await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
body: offer.sdp
});
const answerSdp = await response.text();
await this.pc.setRemoteDescription({
type: 'answer',
sdp: answerSdp
});
console.log('WHIP推流已启动');
}
stop() {
this.stream?.getTracks().forEach(t => t.stop());
this.pc?.close();
}
}
这里有个细节:WHIP服务器返回的HTTP响应里,Location头部会带一个资源URL。你可以用这个URL来做后续的推流控制,比如暂停或停止。嗯,这个设计挺巧妙的,把WebRTC的状态管理映射到了RESTful API上。
WHEP拉流播放器实现
WHEP(WebRTC-HTTP egress protocol)是WHIP的镜像协议。观众端通过HTTP获取SDP,然后建立WebRTC连接接收流。说白了,就是把推流的"发"变成"收"。
我建议你直接复用WHIP的代码结构,把方向反过来就行:
class WHEPPlayer {
constructor(whepEndpoint) {
this.endpoint = whepEndpoint;
this.pc = null;
this.videoElement = null;
}
async play(videoElement) {
this.videoElement = videoElement;
this.pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
// 接收远程流
this.pc.ontrack = (event) => {
if (event.track.kind === 'video') {
this.videoElement.srcObject = event.streams[0];
}
};
// 创建Offer(注意:WHEP是客户端发Offer)
const offer = await this.pc.createOffer();
await this.pc.setLocalDescription(offer);
const response = await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
body: offer.sdp
});
const answerSdp = await response.text();
await this.pc.setRemoteDescription({
type: 'answer',
sdp: answerSdp
});
}
stop() {
this.pc?.close();
this.videoElement?.srcObject?.getTracks().forEach(t => t.stop());
}
}
延迟控制与QoS优化
直播最怕什么?卡顿和延迟。我见过不少团队,推流拉流都通了,但延迟3秒以上,观众体验极差。这里我分享几个实战中验证过的优化手段。
1. 码率自适应
网络状况是动态变化的。你不能固定一个码率。我习惯用RTCRtpSender.getStats()实时监控发送质量,然后动态调整编码参数:
async function adaptiveBitrateControl(sender) {
const stats = await sender.getStats();
let roundTripTime = 0;
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.state === 'succeeded') {
roundTripTime = report.currentRoundTripTime;
}
});
// RTT > 200ms 说明网络差,降码率
if (roundTripTime > 0.2) {
const params = sender.getParameters();
params.encodings[0].maxBitrate = 500000; // 降到500kbps
await sender.setParameters(params);
console.log('网络变差,降码率到500kbps');
}
}
核心思路:不要等用户抱怨卡顿了再调整。用RTT、丢包率、抖动这些指标做主动预测。我一般在RTT超过150ms时就触发降码率,等恢复后再升回去。
2. NACK与FEC的平衡
WebRTC默认用NACK(丢包重传)来恢复丢包。但直播场景下,重传会增加延迟。我建议这样配置:
| 场景 | 推荐策略 | 说明 |
|---|---|---|
| 低延迟直播(<500ms) | FEC + 少量NACK | 前向纠错提前恢复,减少重传等待 |
| 普通直播(1-3s) | NACK为主 | 带宽利用率高,延迟可接受 |
| 弱网环境 | FEC + 降分辨率 | 先保证流畅,再考虑清晰度 |
你想想看,FEC虽然会多消耗一些带宽,但能避免重传带来的RTT等待。在实时互动场景里,这个取舍是值得的。
3. JitterBuffer优化
接收端的抖动缓冲是延迟的隐形杀手。默认的jitter buffer会为了平滑而引入额外延迟。我建议在直播场景下调小它:
// 在创建PeerConnection时设置
const pc = new RTCPeerConnection({
iceServers: [...],
// 减小抖动缓冲
rtcpMuxPolicy: 'require',
// 启用BWE(带宽估计)
bwe: 'transport-cc'
});
// 接收端可以设置playoutDelayHint
const receiver = pc.getReceivers()[0];
if (receiver) {
receiver.playoutDelayHint = 0.2; // 200ms缓冲
}
知识体系总览
下面这张图把WHIP推流、WHEP拉流和QoS优化的关系串起来了。你可以看到,推流端和拉流端通过信令服务器交换SDP,然后建立P2P连接。QoS优化贯穿整个链路。
这张图里,WHIP和WHEP通过信令服务器交换SDP,建立媒体通道。QoS优化不是某个点的任务,而是从采集到渲染全链路都要考虑。我习惯把码率自适应放在推流端,把jitter buffer优化放在拉流端,NACK/FEC两端都要配合。
总结一下:WHIP推流和WHEP拉流的核心就是SDP的HTTP交换。QoS优化的本质是在延迟和流畅度之间找平衡。没有银弹,只有根据你的场景做取舍。我建议你先把基础流程跑通,再逐步加入优化策略,每次只改一个参数,观察效果。