欢迎光临
我们一直在努力

基于 WebRTC 打造视频会议系统(三):直接驾驭 MediaEngine — 绕过 PeerConnection 的音视频通道构建

前言

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);
// …
};

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


第三步:自定义 BaseChannel

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

3.1 去掉 SDP 相关逻辑

标准 BaseChannel 的大量代码是处理 SDP 协商的,如 SetRemoteContent、SetLocalContent 等。我们的 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 会:

  • 注册 RTP Demuxer Sink(按 SSRC 接收 RTP 包)
  • 连接 SignalReadyToSend / SignalWritableState 信号
  • 连接 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 — 音频桥接

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

    • 视频:直接通过 SetVideoSend 将 VideoTrackSource 连接到 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:

  • 保留 MediaEngine:通过 WebRtcMediaEngineFactory 创建完整的媒体引擎
  • 使用 ChannelManager:直接创建 VoiceChannel 和 VideoChannel
  • 注入自定义传输层:通过 RtpTransportInterface 注入 KCP 传输
  • 简化参数配置:用 MediaChannelOptions 替代 SDP 协商
  • 音频桥接:用 LocalAudioSinkAdapter 连接 AudioTrack 和 VoiceChannel
  • Call 对象:管理带宽估计和拥塞控制,支持自定义算法注入
  • 这种"取其精华、去其糟粕"的方式,既享受了 WebRTC 多年优化的音视频处理能力,又摆脱了 PeerConnection 和 SDP 的束缚。

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

    赞(0)
    未经允许不得转载:171主机测评 » 基于 WebRTC 打造视频会议系统(三):直接驾驭 MediaEngine — 绕过 PeerConnection 的音视频通道构建
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址