关于音视频:技术干货-WebRTC-ADM-源码流程分析

52次阅读

共计 6027 个字符,预计需要花费 16 分钟才能阅读完成。

导读:

本文次要基于 WebRTC release-72 源码及云信音视频团队积攒的相干教训而成,次要剖析以下问题:ADM(Audio Device Manager)的架构如何?ADM(Audio Device Manager)的启动流程如何?ADM(Audio Device Manager)的数据流向如何?本文次要是剖析相干的外围流程,以便于大家有需要时,能疾速地定位到相干的模块。

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

一、ADM 根本架构

ADM 的架构剖析

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

从下面的架构图能够看出 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,当然最终是透过下面的架构层层调用具体平台相应的实现,具体流程如下图:

对于设施的进行

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

三、ADM 音频数据流向

音频数据的发送

 

上图是音频数据发送的外围流程,次要是外围函数的调用及线程的切换。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),再往后就是发送给网络。到此整个发送过程完结。

数据的接管与播放

 

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

作者介绍

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

正文完
 0