适用场景:远程控制、多屏监控、会议系统

核心目标:在连接建立时不预设媒体数量,实现“先连信令,后加媒体”的弹性架构。


一、核心设计理念

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 为 inactive

视频黑屏

缺少 a=msid或 ICE 未完成

多路流混乱

按索引而非 track.id 绑定


五、总结

“先信令,后媒体”​

“以 Track 为中心,不以 Stream 为中心”

✅ 支持任意数量屏幕

✅ 支持热插拔摄像头

✅ 不浪费带宽与资源

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐