3

技术干货 | WebRTC ADM 源码流程分析

 2 years ago
source link: https://segmentfault.com/a/1190000041448344
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文主要基于 WebRTC release-72 源码及云信音视频团队积累的相关经验而成,主要分析以下问题: ADM(Audio Device Manager)的架构如何?ADM(Audio Device Manager)的启动流程如何?ADM(Audio Device Manager)的数据流向如何?本文主要是分析相关的核心流程,以便于大家有需求时,能快速地定位到相关的模块。

文|陈稳稳 网易云信资深音视频客户端开发工程师

一、ADM 基本架构

ADM 的架构分析

WebRTC 中,ADM(Audio Device Manager)的行为由 AudioDeviceModule 来定义,具体由 AudioDeviceModuleImpl 来实现。

1.png

从上面的架构图可以看出 AudioDeviceModule 定义了 ADM 相关的所有行为(上图只列出了部分核心,更详细的请参考源码中的完整定义)。从 AudioDeviceModule 的定义可以看出 AudioDeviceModule 的主要职责如下:

初始化音频播放/采集设备;

启动音频播放/采集设备;

停止音频播放/采集设备;

在音频播放/采集设备工作时,对其进行操作(例如:Mute , Adjust Volume);

平台内置 3A 开关的调整(主要是针对 Android 平台);

获取当前音频播放/采集设备各种与此相关的状态(类图中未完全体现,详情参考源码)

AudioDeviceModule 具体由 AudioDeviceModuleImpl 实现,二者之间还有一个 AudioDeviceModuleForTest,主要是添加了一些测试接口,对本文的分析无影响,可直接忽略。AudioDeviceModuleImpl 中有两个非常重要的成员变量,一个是 audio_device_,它的具体类型是 std::unique_ptr,另一个是 audio_device_buffer_,它的具体类型是 AudioDeviceBuffer。

其中 audio_device_ 是 AudioDeviceGeneric 类型,AudioDeviceGeneric 是各个平台具体音频采集和播放设备的一个抽象,由它承担 AudioDeviceModuleImpl 对具体设备的操作。涉及到具体设备的操作,AudioDeviceModuleImpl 除了做一些状态的判断具体的操作设备工作都由 AudioDeviceGeneric 来完成。AudioDeviceGeneric 的具体实现由各个平台自己实现,例如对于 iOS 平台具体实现是 AudioDeviceIOS,Android 平台具体实现是 AudioDeviceTemplate。至于各个平台的具体实现,有兴趣的可以单个分析。这里说一下最重要的共同点,从各个平台具体实现的定义中可以发现,他们都有一个 audio_device_buffer 成员变量,而这个变量与前面提到的 AudioDeviceModuleImpl 中的另一个重要成员变量 audio_device_buffer_,其实二者是同一个。AudioDeviceModuleImpl 通过 AttachAudioBuffer() 方法,将自己的 audio_device_buffer_ 对象传给具体的平台实现对象。

audio_device_buffer_ 的具体类型是 AudioDeviceBuffer,AudioDeviceBuffer 中的 play_buffer_、rec_buffer_ 是 int16_t  类型的 buffer,前者做为向下获取播放 PCM 数据的 Buffer,后者做为向下传递采集 PCM 数据的 Buffer,具体的 PCM 数据流向在后面的数据流向章节具体分析,而另一个成员变量 audio_transport_cb_,类型为 AudioTransport,从 AudioTransport 接口定义的中的两个核心方法不难看出他的作用,一是向下获取播放 PCM 数据存储在 play_buffer_,另一个把采集存储在 rec_buffer_ 中的 PCM 数据向下传递,后续具体流程参考数据流向章节。

关于 ADM 扩展的思考

从 WebRTC ADM 的实现来看,WebRTC 只实现对应了各个平台具体的硬件设备,并没什么虚拟设备。但是在实际的项目,往往需要支持外部音频输入/输出,就是由业务上层 push/pull 音频数据(PCM ...),而不是直接启动平台硬件进行采集/播放。在这种情况下,虽然原生的 WebRTC 不支持,但是要改造也是非常的简单,由于虚拟设备与平台无关,所以可以直接在 AudioDeviceModuleImpl 中增加一个与真实设备 audio_device_ 对应的Virtual Device(变量名暂定为virtual_device_),virtual_device_ 也跟 audio_device_ 一样,实现 AudioDeviceGeneric 相关接口,然后参考 audio_device_ 的实现去实现数据的“采集”(push)与 “播放”(pull),无须对接具体平台的硬件设备,唯一需要处理的就是物理设备 audio_device_ 与虚拟设备 virtual_device_ 之间的切换或协同工作。

二、ADM 设备的启动

ADM 设备的启动时机并无什么特殊要求,只要 ADM 创建后即可,不过 WebRTC 的 Native 源码中会在 SDP 协商好后去检查一下是否需要启动相关的 ADM 设备,如果需要就会启动相关的 ADM 设备,采集与播放设备的启动二者是完全独立的,但流程大同小异,相关触发代码如下,自上而下阅读即可。

以下是采集设备启动的触发源码(前面几步还有其他触发入口,但后面是一样的,这里只做核心流程展示):

//cricket::VoiceChannel
void VoiceChannel::UpdateMediaSendRecvState_w() {
//*
bool send = IsReadyToSendMedia_w();
media_channel()->SetSend(send);
}

// cricket::WebRtcVoiceMediaChannel
void WebRtcVoiceMediaChannel::SetSend(bool send) {
//*
for (auto& kv : send_streams_) {

kv.second->SetSend(send);

//cricket::WebRtcVoiceMediaChannel::WebRtcAudioSendStream
void SetSend(bool send) {
//*

UpdateSendState();

//cricket::WebRtcVoiceMediaChannel::WebRtcAudioSendStream
void UpdateSendState() {
//*

if (send_ && source_ != nullptr && rtp_parameters_.encodings[0].active) {
  stream_->Start();
} else {  // !send || source_ = nullptr
  stream_->Stop();
}

// webrtc::internal::WebRtcAudioSendStream
void AudioSendStream::Start() {
//*
audio_state()->AddSendingStream(this, encoder_sample_rate_hz_,

                              encoder_num_channels_);

// webrtc::internal::AudioState
void AudioState::AddSendingStream(webrtc::AudioSendStream* stream,

                              int sample_rate_hz,
                              size_t num_channels) {

//*
//检查下采集设备是否已经启动,如果没有,那么在这启动
auto* adm = config_.audio_device_module.get();
if (!adm->Recording()) {

if (adm->InitRecording() == 0) {
  if (recording_enabled_) {
    adm->StartRecording();
  }
} else {
  RTC_DLOG_F(LS_ERROR) << "Failed to initialize recording.";
}

}
}
从上面采集设备启动的触发源码可以看出,如果需要发送音频,不管前面采集设备是否启动,在 SDP 协商好后,一定会启动采集设备。如果我们想把采集设备的启动时机掌握在上层业务手中,那么只要注释上面 AddSendingStream 方法中启动设备那几行代码即可,然后在需要的时候自行通过 ADM 启动采集设备。

以下是播放设备启动的触发源码(前面几步还有其他触发入口,但后面是一样的,这里只做核心流程展示):

//cricket::VoiceChannel
void VoiceChannel::UpdateMediaSendRecvState_w() {
//*
bool recv = IsReadyToReceiveMedia_w();
media_channel()->SetPlayout(recv);
}

// cricket::WebRtcVoiceMediaChannel
void WebRtcVoiceMediaChannel::SetPlayout(bool playout) {
//*
return ChangePlayout(desired_playout_);
}

// cricket::WebRtcVoiceMediaChannel
void WebRtcVoiceMediaChannel::ChangePlayout(bool playout) {
//*
for (const auto& kv : recv_streams_) {

kv.second->SetPlayout(playout);

//cricket::WebRtcVoiceMediaChannel::WebRtcAudioReceiveStream
void SetPlayout(bool playout) {
//*

if (playout) {
  stream_->Start();
} else {
  stream_->Stop();
}

// webrtc::internal::AudioReceiveStream
void AudioReceiveStream::Start() {
//*
audio_state()->AddReceivingStream(this);
}

//webrtc::internal::AudioState
void AudioState::AddReceivingStream(webrtc::AudioReceiveStream* stream) {
//*
// //检查下播放设备是否已经启动,如果没有,那么在这启动
auto* adm = config_.audio_device_module.get();
if (!adm->Playing()) {

if (adm->InitPlayout() == 0) {
  if (playout_enabled_) {
    adm->StartPlayout();
  }
} else {
  RTC_DLOG_F(LS_ERROR) << "Failed to initialize playout.";
}

}
}
从上面播放设备启动的触发源码可以看出,如果需要播放音频,不管前面播放设备是否启动,在 SDP 协商好后,一定会启动播放设备。如果我们想把播放设备的启动时机掌握在上层业务手中,那么只要注释上面 AddReceivingStream 方法中启动设备那几行代码即可,然后在需要的时候自行通过 ADM 启动播放设备。

当需要启动 ADM 设备时,先调用 ADM 的 InitXXX,接着是 ADM 的 StartXXX,当然最终是透过上面的架构层层调用具体平台相应的实现,详细流程如下图:
2.png

关于设备的停止

了解了 ADM 设备的启动,那么与之对应的停止动作,就无需多言。如果大家看了源码,会发现其实停止的动作及流程与启动基本上是一一对应的。

三、ADM 音频数据流向

音频数据的发送

3.png

上图是音频数据发送的核心流程,主要是核心函数的调用及线程的切换。PCM 数据从硬件设备中被采集出来,在采集线程做些简单的数据封装会很快进入 APM 模块做相应的 3A 处理,从流程上看 APM 模块很靠近原始 PCM 数据,这一点对 APM 的处理效果有非常大的帮助,感兴趣的同学可以深入研究下 APM 相关的知识。之后数据就会被封装成一个 Task,投递到一个叫 rtp_send_controller 的线程中,到此采集线程的工作就完成了,采集线程也能尽快开始下一轮数据的读取,这样能最大限度的减小对采集的影响,尽快读取新的 PCM 数据,防止 PCM 数据丢失或带来不必要的延时。

接着数据就到了 rtp_send_controller 线程,rtp_send_controller 线程的在此的作用主要有三个,一是做 rtp 发送的拥塞控制,二是做 PCM 数据的编码,三是将编码后的数据打包成 RtpPacketToSend(RtpPacket)格式。最终的 RtpPacket 数据会被投递到一个叫 RoundRobinPacketQueue 的队列中,至此 rtp_send_controller 线程的工作完成。

后面的 RtpPacket 数据将会在 SendControllerThread 中被处理,SendControllerThread 主要用于发送状态及窗口拥塞的控制,最后数据通过消息的形式(type: MSG_SEND_RTP_PACKET)发送到 Webrtc 三大线程之一的网络线程(Network Thread),再往后就是发送给网络。到此整个发送过程结束。

数据的接收与播放

4.png

上图是音频数据接收及播放的核心流程。网络线程(Network Thread)负责从网络接收 RTP 数据,随后异步给工作线程(Work Thread)进行解包及分发。如果接收多路音频,那么就有多个 ChannelReceive,每个的处理流程都一样,最后未解码的音频数据存放在 NetEq 模块的 packet_buffer_ 中。与此同时播放设备线程不断的从当前所有音频 ChannelReceive 获取音频数据(10ms 长度),进而触发 NetEq 请求解码器进行音频解码。对于音频解码,WebRTC 提供了统一的接口,具体的解码器只需要实现相应的接口即可,比如 WebRTC 默认的音频解码器 opus 就是如此。当遍历并解码完所有 ChannelReceive 中的数据,后面就是通过 AudioMixer 混音,混完后交给 APM 模块处理,处理完最后是给设备播放。

陈稳稳,网易云信资深音视频客户端开发工程师,主要负责 Android 音视频的开发及适配。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK