WebRTC 动态多路媒体流协商最佳实践(工程级)
适用场景:远程控制、多屏监控、会议系统
核心目标:在连接建立时不预设媒体数量,实现“先连信令,后加媒体”的弹性架构。
一、核心设计理念
1.1 为什么不能一次性协商所有流?
|
问题 |
说明 |
|---|---|
|
❌ 未知性 |
主控端无法预知被控端有 1 个还是 N 个屏幕 |
|
❌ 动态性 |
摄像头、屏幕可在连接过程中随时增减 |
|
❌ 资源浪费 |
预创建 Transceiver 会增加 SDP 复杂度与内存占用 |
|
❌ 架构僵化 |
媒体逻辑与信令强耦合,难以扩展 |
✅ 结论:
媒体流 ≠ 连接状态
连接先建立,媒体后协商
1.2 标准做法:两阶段握手(Two-Phase Negotiation)
✅ 第一阶段:信令连接(无媒体)
-
主控端发起 仅含 DataChannel(或空媒体)的 Offer
-
目的:
-
打通 ICE / DTLS
-
建立稳定的 P2P 信令通道
-
不关心媒体数量
-
✅ 第二阶段:媒体协商(动态添加)
-
被控端枚举本地媒体源(屏幕 / 摄像头)
-
调用
AddTrack -
触发
OnRenegotiationNeeded -
创建新的 Offer(携带真实媒体)
-
主控端接收并响应 Answer
-
自动触发
OnAddTrack
二、信令时序(逻辑视图)

三、工程实现详解
3.1 主控端(Controller):轻量级发起
webrtc::PeerConnectionInterface::RTCConfiguration config;
config.sdp_semantics = webrtc::SdpSemantics::kUnifiedPlan;
auto pc = peer_connection_factory_->CreatePeerConnection(
config, nullptr, nullptr, this);
// 仅创建 DataChannel,不添加媒体
pc->CreateDataChannel("control", nullptr);
pc->CreateOffer(this, webrtc::PeerConnectionInterface::RTCOfferAnswerOptions{});
✅ 关键点:
-
不调用
AddTrack -
不创建 Video Transceiver
-
仅用于建立通道
3.2 被控端(Controlled):动态媒体协商
// 收到 Controller 的初始 Offer
pc->SetRemoteDescription(std::move(offer));
// 枚举本地媒体
for (auto& track : GetAllVideoTracks()) {
pc->AddTrack(track, { "stream_" + track->id() });
}
✅ 关键回调:OnRenegotiationNeeded
void OnRenegotiationNeeded() override {
pc_->CreateOffer(this,
webrtc::PeerConnectionInterface::RTCOfferAnswerOptions{});
}
📌 WebRTC 会自动:
-
为每个 Track 创建 Transceiver
-
生成正确的
m=video行 -
触发 Offer 创建
3.3 主控端:接收媒体并处理
void OnAddTrack(
rtc::scoped_refptr<webrtc::RtpReceiverInterface> receiver,
const std::vector<rtc::scoped_refptr<webrtc::MediaStreamInterface>>& streams) override {
auto track = receiver->track();
for (auto& stream : streams) {
AttachVideo(stream, track);
}
}
✅ 不要依赖 m-line 顺序
✅ 通过 mid/ msid识别流
四、关键工程注意事项
4.1 线程安全(非常重要)
|
操作 |
要求 |
|---|---|
|
SetRemoteDescription |
信令线程 |
|
CreateOffer |
信令线程 |
|
AddTrack |
信令线程 |
✅ 正确方式:
m_pSignalingThread->PostTask([this, msg]() {
HandleSignalingMessage(msg);
});
✅ Lambda 中捕获 shared_ptr,防止对象析构
4.2 幂等性保护
if (!m_bTracksAssigned) {
AssignTracksToConnection();
m_bTracksAssigned = true;
}
📌 防止:
-
重复 AddTrack
-
多个相同 Sender
-
多次触发 Renegotiation
4.3 SDP 与兼容性
|
项目 |
要求 |
|---|---|
|
SDP Semantics |
✅ Unified Plan |
|
m-line 顺序 |
❌ 不可依赖 |
|
流识别 |
✅ mid / msid |
4.4 常见错误排查
|
现象 |
原因 |
|---|---|
|
AddTrack 失败 |
信令线程错误 / 重复添加 |
|
OnAddTrack 未触发 |
Answer 为 |
|
视频黑屏 |
缺少 |
|
多路流混乱 |
按索引而非 track.id 绑定 |
五、总结
“先信令,后媒体”
“以 Track 为中心,不以 Stream 为中心”
✅ 支持任意数量屏幕
✅ 支持热插拔摄像头
✅ 不浪费带宽与资源
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)