
本文详解如何在 webrtc 中构建真正的单向视频通话(仅发送方采集视频,接收方不请求摄像头权限),规避过时 api 和 sdp 顺序错误,通过规范化的 transceiver 管理与免显式协商流程达成稳定、合规的一-way 视频传输。
本文详解如何在 webrtc 中构建真正的单向视频通话(仅发送方采集视频,接收方不请求摄像头权限),规避过时 api 和 sdp 顺序错误,通过规范化的 transceiver 管理与免显式协商流程达成稳定、合规的一-way 视频传输。
实现 WebRTC 单向视频通话的核心目标是:发送端采集并发送视频流,接收端仅接收并渲染,且全程不触发无关的媒体设备权限请求(如接收端调用 getUserMedia())。早期依赖已废弃的 offerToReceiveVideo 选项或手动管理 m= 行顺序的方式不仅不可靠,还易引发 setLocalDescription 失败、STUN 地址不兼容等错误。
现代标准做法应严格遵循 RTCPeerConnection 规范 与 MDN 最佳实践,关键在于 以 transceiver 为媒介声明意图,并交由浏览器自动处理协商逻辑,避免手动调用 createOffer/createAnswer 引发的 SDP 不一致问题。
✅ 正确实现步骤(双向角色分离)
1. 接收端(Listener):主动声明“只收不发”
接收端无需调用 getUserMedia(),只需在创建连接后,预先添加 recvonly 视频 transceiver:
const pc = new RTCPeerConnection(config);
// 声明:我只接收视频,不发送
pc.addTransceiver('video', { direction: 'recvonly' });
// 可选:添加音频 recvonly(如需单向音视频)
// pc.addTransceiver('audio', { direction: 'recvonly' });
// 设置远程描述后,浏览器将自动在 offer 中生成对应 m=video 行
pc.ontrack = (event) => {
if (event.track.kind === 'video') {
document.getElementById('remote-video').srcObject = event.streams[0];
}
};⚠️ 注意:addTransceiver 必须在 setRemoteDescription 之前调用,否则 transceiver 不会被纳入初始 offer。
2. 发送端(Sender):仅发送,不预留接收能力
发送端正常采集本地视频流,但不添加任何 recvonly transceiver:
const pc = new RTCPeerConnection(config);
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
// 将视频轨道添加到连接(自动创建 sendonly transceiver)
stream.getVideoTracks().forEach(track => pc.addTrack(track, stream));
// 音频可按需添加(如需双向音频)
// const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
// audioStream.getAudioTracks().forEach(track => pc.addTrack(track, audioStream));3. 协商流程:交由浏览器自动管理(推荐)
完全避免手动 createOffer/createAnswer 调用。利用事件驱动与隐式协商机制:
- 接收端收到 offer 后,直接调用 setRemoteDescription(offer);
- 浏览器检测到存在 recvonly transceiver 且无对应本地轨道,会自动生成合法 answer(含 a=recvonly 属性与匹配的 m-line 顺序);
- 发送端收到 answer 后,调用 setRemoteDescription(answer),完成握手。
// 接收端示例(监听信令)
socket.on('offer', async (offer) => {
await pc.setRemoteDescription(offer); // ✅ 浏览器自动生成正确 answer
const answer = await pc.createAnswer(); // 此处 createAnswer 由浏览器保障一致性
await pc.setLocalDescription(answer);
socket.emit('answer', answer);
});? 核心原理:addTransceiver(..., { direction: 'recvonly' }) 显式声明接收意图,使浏览器在生成 SDP 时固定 m-line 顺序与媒体属性;而省略手动 createOffer 可避免因多次协商导致的 m= 行 ID 错位(即你遇到的 “m-lines order mismatch” 错误)。
❌ 常见误区与修复
-
错误:接收端调用 getUserMedia() → 触发无意义的摄像头权限弹窗;
修复:彻底移除接收端所有 getUserMedia() 调用。 -
错误:发送端也调用 addTransceiver('video', { direction: 'recvonly' }) → 造成双向接收冲突;
修复:仅接收端声明 recvonly,发送端仅 addTrack。 -
错误:在 setRemoteDescription 后再 addTransceiver → transceiver 不参与当前协商;
修复:确保 addTransceiver 在首次 setRemoteDescription 前完成。
✅ 最终效果验证
- 接收端页面打开无摄像头权限请求;
- 发送端视频正常传输至接收端 <video> 元素;
- Chrome DevTools → chrome://webrtc-internals 中可见:接收端 transceiver direction 为 recvonly,发送端为 sendonly;
- SDP 中 m=video 行顺序稳定,无 701 STUN server address is incompatible 等 ICE 错误。
遵循此模式,即可构建轻量、合规、可维护的单向 WebRTC 视频通道,适用于监控预览、直播观众端、远程教学接收侧等典型场景。

















