前言

WebRTC 最有价值的部分不是 PeerConnection,而是它强大的 MediaEngine——包含 OPUS/VP8/H264 编解码器、AEC 回声消除、NS 噪声抑制、抖动缓冲、带宽估计等精心调优的组件。标准使用方式通过 PeerConnection 调用这些能力,但 PeerConnection 的 SDP 协商模型不适合 SFU 架构。

本文将介绍如何绕过 PeerConnection,直接使用 WebRTC 的 ChannelManager 和 MediaEngine 来构建自定义的音视频通道。


PeerConnection 做了什么?

理解如何绕过 PeerConnection,首先要理解它做了什么:

PeerConnection
├── SDP 协商
│   ├── CreateOffer / CreateAnswer
│   ├── SetLocalDescription / SetRemoteDescription
│   └── 编解码能力协商
│
├── 传输管理
│   ├── ICE Agent (候选收集/连通性检查)
│   ├── DTLS Transport (加密握手)
│   └── SRTP Transport (加密传输)
│
├── 轨道管理
│   ├── AddTrack / RemoveTrack
│   └── RtpSender / RtpReceiver
│
└── Channel 管理
    ├── VoiceChannel (音频通道)
    └── VideoChannel (视频通道)

对于 SFU 视频会议,我们只需要其中的 Channel 管理轨道管理,其余部分我们用自研的传输层替代。


核心类关系

┌───────────────────────────────────────────────────────┐
│                  MediaConnectionFactory                │
│                                                       │
│  ┌────────────────┐  ┌───────────────────────────┐    │
│  │  MediaEngine   │  │     ChannelManager        │    │
│  │ (编解码/AEC/NS)│  │  (通道生命周期管理)       │    │
│  └────────────────┘  └──────────┬────────────────┘    │
│                                 │                     │
│                    ┌────────────┼────────────┐        │
│                    ▼            ▼            ▼        │
│              VoiceChannel  VideoChannel  Call         │
│                                                       │
│  三线程:signaling_thread / worker_thread / network   │
└───────────────────────────────────────────────────────┘
          │
          │ CreateConnection
          ▼
┌───────────────────────────────────────────────────────┐
│                   MediaConnection                      │
│                                                       │
│  audio_channel_ (VoiceChannel)                        │
│  video_channel_ (VideoChannel)                        │
│  stream_tracks_ (SSRC → TrackData 映射)              │
│  rtp_transport_ (RTP 传输接口)                        │
└───────────────────────────────────────────────────────┘

第一步:创建 MediaEngine

MediaEngine 是 WebRTC 的核心,包含音频和视频引擎。创建方式如下:

// create_media_connection_factory.cc

rtc::scoped_refptr<MediaConnectionFactory>
CreateMediaConnectionFactory(...) {
    // 1. 创建音频处理模块(含 AEC3 回声消除)
    webrtc::AudioProcessingBuilder builder;
    builder.SetEchoControlFactory(
        absl::make_unique<webrtc::EchoCanceller3Factory>());
    auto audio_processing = builder.Create();

    // 2. 创建 MediaEngine
    std::unique_ptr<cricket::MediaEngineInterface> media_engine =
        cricket::WebRtcMediaEngineFactory::Create(
            default_adm,                    // 音频设备模块
            audio_encoder_factory,          // 音频编码器工厂
            audio_decoder_factory,          // 音频解码器工厂
            std::move(video_encoder_factory), // 视频编码器工厂
            std::move(video_decoder_factory), // 视频解码器工厂
            nullptr,                        // 音频 Mixer
            audio_processing);              // 音频处理模块

    // 3. 创建 Call 工厂和事件日志工厂
    auto call_factory = webrtc::CreateCallFactory();
    auto event_log_factory = webrtc::CreateRtcEventLogFactory();

    // 4. 组装依赖并创建 MediaConnectionFactory
    MediaConnectionFactoryDependencies dependencies;
    dependencies.media_engine = std::move(media_engine);
    dependencies.call_factory = std::move(call_factory);
    dependencies.event_log_factory = std::move(event_log_factory);
    // ...
    return MediaConnectionFactory::CreateConnectionFactory(std::move(dependencies));
}

使用内置编解码器

如果不指定自定义编解码器,可以使用 WebRTC 内置的:

auto factory = MediaConnectionFactoryInterface::CreateConnectionFactory();
// 内部自动使用:
// - webrtc::CreateBuiltinAudioEncoderFactory()  → OPUS 编码
// - webrtc::CreateBuiltinAudioDecoderFactory()  → OPUS 解码
// - webrtc::CreateBuiltinVideoEncoderFactory()  → VP8/H264 编码
// - webrtc::CreateBuiltinVideoDecoderFactory()  → VP8/H264 解码

第二步:理解 ChannelManager

ChannelManager 是 WebRTC 内部管理音视频通道的核心类。在标准 WebRTC 中,它由 PeerConnectionFactory 内部使用,但我们直接使用它:

class ChannelManager {
public:
    // 创建音频通道
    VoiceChannel* CreateVoiceChannel(
        webrtc::Call* call,
        const cricket::MediaConfig& media_config,
        RtpTransportInterface* rtp_transport,     // ← 注入我们的传输层
        webrtc::MediaTransportInterface* media_transport,
        const std::string& content_name,
        const cricket::AudioOptions& options);

    // 创建视频通道
    VideoChannel* CreateVideoChannel(
        webrtc::Call* call,
        const cricket::MediaConfig& media_config,
        RtpTransportInterface* rtp_transport,     // ← 注入我们的传输层
        webrtc::MediaTransportInterface* media_transport,
        const std::string& content_name,
        const cricket::VideoOptions& options);

    // 销毁通道
    void DestroyVoiceChannel(VoiceChannel* channel);
    void DestroyVideoChannel(VideoChannel* channel);

    // 查询支持的编解码能力
    void GetSupportedAudioSendCodecs(vector<AudioCodec>* codecs);
    void GetSupportedVideoCodecs(vector<VideoCodec>* codecs);
    // ...
};

关键点CreateVoiceChannelCreateVideoChannel 接受 RtpTransportInterface* 参数,这就是我们注入自研传输层的入口!


第三步:自定义 BaseChannel

WebRTC 的 cricket::BaseChannel 是 VoiceChannel 和 VideoChannel 的基类。我们重写了这个类,主要改动:

3.1 去掉 SDP 相关逻辑

标准 BaseChannel 的大量代码是处理 SDP 协商的,如 SetRemoteContentSetLocalContent 等。我们的 BaseChannel 去掉了这些,改为直接通过 MediaChannelOptions 设置参数:

// 标准 WebRTC 的方式(SDP 协商)
channel->SetRemoteContent(content_description, SdpType::kOffer, error);

// 我们的方式(直接设置参数)
MediaChannelOptions options;
options.SetAudioCodec(codecs);
options.set_rtp_header_extensions(extensions);
channel->SetSendOption(&options);
channel->SetRecvOption(&options);

3.2 注入 RtpTransport

BaseChannel 通过 SetRtpTransport 接收我们自定义的传输层:

bool BaseChannel::SetRtpTransport(RtpTransportInterface* rtp_transport) {
    // 断开旧的传输层
    DisconnectFromRtpTransport();

    rtp_transport_ = rtp_transport;

    // 连接新的传输层
    ConnectToRtpTransport();

    return true;
}

连接传输层后,BaseChannel 会:

  1. 注册 RTP Demuxer Sink(按 SSRC 接收 RTP 包)
  2. 连接 SignalReadyToSend / SignalWritableState 信号
  3. 连接 SignalSentPacket 信号(用于带宽估计)

3.3 RTP 收发

// 发送:BaseChannel → RtpTransport → MediaNetwork → KcpSocket
bool BaseChannel::SendPacket(rtc::CopyOnWriteBuffer* packet,
                             const rtc::PacketOptions& options) {
    return rtp_transport_->SendRtpPacket(packet, options);
}

// 接收:KcpSocket → MediaNetwork → RtpTransport → RtpDemuxer → BaseChannel
void BaseChannel::OnRtpPacket(const webrtc::RtpPacketReceived& packet) {
    // 分发给 MediaEngine 处理
    media_channel_->OnRtpPacket(packet);
}

第四步:MediaConnection 实现

MediaConnection 是一个媒体连接实例,封装了 VoiceChannel 和 VideoChannel:

class MediaConnection : public MediaConnectionInterface {
public:
    bool Initialize(const Configuration& config,
                    RtpTransportInterface* transport) override {
        rtp_transport_ = transport;

        // 创建音频通道
        if (config.enable_audio) {
            audio_channel_ = factory_->channel_manager()->CreateVoiceChannel(
                call_.get(), media_config, rtp_transport_,
                nullptr, "voice", audio_options_);
            audio_channel_->Enable(true);
        }

        // 创建视频通道
        if (config.enable_video) {
            video_channel_ = factory_->channel_manager()->CreateVideoChannel(
                call_.get(), media_config, rtp_transport_,
                nullptr, "video", video_options_);
            video_channel_->Enable(true);
        }

        return true;
    }

private:
    rtc::scoped_refptr<MediaConnectionFactory> factory_;
    std::unique_ptr<webrtc::Call> call_;
    std::unique_ptr<webrtc::RtcEventLog> event_log_;
    VideoChannel* video_channel_ = nullptr;
    VoiceChannel* audio_channel_ = nullptr;
    StreamTrackMap stream_tracks_;
};

AddTrack 实现

bool MediaConnection::AddTrack(
    rtc::scoped_refptr<webrtc::MediaStreamTrackInterface> track,
    uint32_t ssrc) {

    if (track->kind() == webrtc::MediaStreamTrackInterface::kAudioKind) {
        // 音频轨道
        cricket::StreamParams params;
        params.add_ssrc(ssrc);
        params.cname = audio_channel_->content_name();

        // 1. 添加发送流
        audio_channel_->AddSendStream(params);

        // 2. 创建音频适配器,桥接 AudioTrack 和 VoiceChannel
        auto audio_adapter = std::make_unique<LocalAudioSinkAdapter>();
        audio_channel_->SetAudioSend(ssrc, true, &audio_options_, audio_adapter.get());

        // 3. 保存 TrackData
        stream_tracks_[ssrc] = new MediaTrackData(std::move(audio_adapter), track);

    } else {
        // 视频轨道
        cricket::StreamParams params;
        params.add_ssrc(ssrc);
        params.cname = video_channel_->content_name();

        // 1. 添加发送流
        video_channel_->AddSendStream(params);

        // 2. 设置视频源
        video_channel_->SetVideoSend(ssrc, &video_options_, track->video_track());

        // 3. 保存 TrackData
        stream_tracks_[ssrc] = new MediaTrackData(nullptr, track);
    }

    return true;
}

AddRecvStream 实现

bool MediaConnection::AddRecvStream(cricket::MediaType type,
                                     const cricket::StreamParams& sp) {
    if (type == cricket::MediaType::MEDIA_TYPE_VIDEO) {
        return video_channel_->AddRecvStream(sp);
    } else {
        return audio_channel_->AddRecvStream(sp);
    }
}

bool MediaConnection::SetSink(uint32_t ssrc,
                               rtc::VideoSinkInterface<webrtc::VideoFrame>* sink) {
    return video_channel_->SetSink(ssrc, sink);
}

第五步:LocalAudioSinkAdapter — 音频桥接

音频轨道和视频轨道的发送方式有所不同:

  • 视频:直接通过 SetVideoSendVideoTrackSource 连接到 VideoChannel
  • 音频:需要通过 AudioSource 桥接,因为 WebRTC 的音频发送使用 cricket::AudioSource 接口

LocalAudioSinkAdapter 实现了这个桥接:

// 同时实现两个接口
class LocalAudioSinkAdapter : public webrtc::AudioTrackSinkInterface,   // 接收 AudioTrack 数据
                              public cricket::AudioSource {             // 提供给 VoiceChannel
public:
    // AudioTrack 的数据回调
    void OnData(const void* audio_data, int bits_per_sample,
                int sample_rate, size_t number_of_channels,
                size_t number_of_frames) override {
        // 转发给 VoiceChannel 的 Sink
        rtc::CritScope lock(&lock_);
        if (sink_) {
            sink_->OnData(audio_data, bits_per_sample, sample_rate,
                          number_of_channels, number_of_frames);
        }
    }

    // VoiceChannel 注册 Sink
    void SetSink(cricket::AudioSource::Sink* sink) override {
        rtc::CritScope lock(&lock_);
        sink_ = sink;
    }

private:
    cricket::AudioSource::Sink* sink_ = nullptr;
    rtc::CriticalSection lock_;
};

数据流向:

AudioDeviceModule → AudioTrack → AudioTrackSinkInterface::OnData
                                          │
                                          ▼
                               LocalAudioSinkAdapter
                                          │
                                          ▼
                              cricket::AudioSource::Sink::OnData
                                          │
                                          ▼
                                  VoiceChannel → 编码 → RTP

第六步:Call 对象

webrtc::Call 是 WebRTC 的全局呼叫管理对象,负责:

  • 带宽估计:GCC(Google Congestion Control)算法
  • 码率分配:在音视频流之间分配可用带宽
  • 丢包统计:收集丢包率和 RTT 信息
  • 拥塞控制:支持注入自定义拥塞控制算法
// 创建 Call
std::unique_ptr<webrtc::Call> MediaConnectionFactory::CreateCall_w(
    webrtc::RtcEventLog* event_log) {
    webrtc::Call::Config config(event_log);
    config.network_controller_factory = injected_network_controller_factory_.get();
    // 注入自定义拥塞控制算法
    return std::unique_ptr<webrtc::Call>(
        call_factory_->CreateCall(config));
}

注入自定义拥塞控制

WebRTC 提供了 NetworkControllerFactoryInterface 接口,允许注入自定义拥塞控制算法:

struct MediaConnectionFactoryDependencies {
    // ...
    std::unique_ptr<webrtc::NetworkControllerFactoryInterface>
        network_controller_factory;  // ← 注入自定义 GCC/BBR 等
};

线程安全

WebRTC 的三线程模型必须严格遵守,否则会导致数据竞争:

Signaling Thread (信令线程)
├── MediaClient 操作
├── MediaConnection 操作
├── AddTrack / RemoveTrack
└── SetSendOption / SetRecvOption

Worker Thread (工作线程)
├── MediaEngine 操作
├── 编解码
├── 音视频处理
└── Call / RtcEventLog

Network Thread (网络线程)
├── Socket 收发
├── KCP 更新
├── RTP 收发
└── RtpTransport 分发

跨线程调用规则:

// Signaling → Worker:使用 Invoke 同步调用
worker_thread()->Invoke<void>(RTC_FROM_HERE, [this] {
    // 在 Worker 线程上执行
});

// Network → Signaling:使用 Post 异步通知
thread_->Post(RTC_FROM_HERE, this, MSG_ON_REMOVE_STREAM, data);

// Network → Worker:使用 Post 异步通知
worker_thread_->Post(RTC_FROM_HERE, ...);

小结

本文介绍了如何绕过 PeerConnection,直接使用 WebRTC 的 MediaEngine:

  1. 保留 MediaEngine:通过 WebRtcMediaEngineFactory 创建完整的媒体引擎
  2. 使用 ChannelManager:直接创建 VoiceChannel 和 VideoChannel
  3. 注入自定义传输层:通过 RtpTransportInterface 注入 KCP 传输
  4. 简化参数配置:用 MediaChannelOptions 替代 SDP 协商
  5. 音频桥接:用 LocalAudioSinkAdapter 连接 AudioTrack 和 VoiceChannel
  6. Call 对象:管理带宽估计和拥塞控制,支持自定义算法注入

这种"取其精华、去其糟粕"的方式,既享受了 WebRTC 多年优化的音视频处理能力,又摆脱了 PeerConnection 和 SDP 的束缚。

下一篇将介绍信令协议的设计和推拉流模型的实现。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐