基于 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 的束缚。
下一篇将介绍信令协议的设计和推拉流模型的实现。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)