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

云儿
• 阅读 2207

导读:

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

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

一、ADM 基本架构

ADM 的架构分析

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

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

从上面的架构图可以看出 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,当然最终是透过上面的架构层层调用具体平台相应的实现,详细流程如下图:
技术干货 | WebRTC ADM 源码流程分析

关于设备的停止

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

三、ADM 音频数据流向

音频数据的发送

技术干货 | WebRTC 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),再往后就是发送给网络。到此整个发送过程结束。

数据的接收与播放

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

 

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

作者介绍

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

点赞
收藏
评论区
推荐文章
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
亚瑟 亚瑟
4年前
Flutter - 深入理解Flutter引擎启动
基于Flutter1.5,从源码视角来深入剖析flutter引擎的启动流程,相关源码目录见文末附录一、Flutter引擎启动工作1.1Flutter启动概览Flutter作为一款跨平台的框架,可以运行在Android、iOS等平台,Android为例讲解如何从Android应用启动流程中衔接到Flutter框架,
Easter79 Easter79
3年前
SQL注入 payload 记录
使用REGEXP盲注payloadselectuser()fromuserswhereuser_id1and(select(user)fromuserswhereuser_id1)REGEXP"^adm.";来源https://www.se
Stella981 Stella981
3年前
Flink Kafka 端到端 Exactly
摘要:本文基于Flink1.9.0和Kafka2.3版本,对Flinkkafka端到端ExactlyOnce进行分析及 notifyCheckpointComplete顺序,主要内容分为以下两部分:1.Flinkkafka两阶段提交源码分析TwoPhaseCommitSinkFuncti
Stella981 Stella981
3年前
SQL注入 payload 记录
使用REGEXP盲注payloadselectuser()fromuserswhereuser_id1and(select(user)fromuserswhereuser_id1)REGEXP"^adm.";来源https://www.se
Stella981 Stella981
3年前
Spring Security教程之Spring Security实现访问控制
在SpringSecurity中,实现访问控制或权限控制是非常容易实现的,请看下面的代码片段:123  <http autoconfig"true"    <intercepturl pattern"/admin\" access"ROLE\_ADMIN" /  </http它的意思是,只有“ROLE\_ADM
Wesley13 Wesley13
3年前
Activiti架构分析及源码详解
Activiti架构分析及源码详解\TOC\引言工作流引擎,应用于解决流程审批和流程编排方面等问题,有效的提供了扩展性的支撑。而目前来说,工作流领域也有了相对通行化的标准规范,也就是BPMN2.0。支持这个规范的开源引擎主要有:Activiti,flowable,Jbpm4等。本文着重对Activit
Stella981 Stella981
3年前
K8S之adm集群证书过期和集群升级一并解决
作者:李毓k8s的adm安装方式有一个巨坑,就是证书过期问题。其中涉及到的证书有apiserver,kubelet,etcd,proxy等等证书。这个问题在二进制安装方式是不存在的,因为可以手动更改证书。但是由于adm是自动安装,所以需要后期处理。目前的解决方式一般有三种,第一种是集群升级,通过升级k8s,间接的把证书也升级了。第二种是
Stella981 Stella981
3年前
Kafka服务端之网络连接源码分析
\简介上次我们通过分析KafkaProducer的源码了解了生产端的主要流程,今天学习下服务端的网络层主要做了什么,先看下KafkaServer的整体架构图!\file\(https://img2018.cnblogs.com/blog/1803159/201909/180315920190915190751890431077904.
Stella981 Stella981
3年前
Spring Cloud Alibaba 实战(十一)
欢迎关注全是干货的技术公众号:JavaEdge本文主要内容:如何实现用户认证与授权?实现的三种方案,全部是通过画图的方式讲解.以及三种方案的对比最后根据方案改造Gateway和扩展Feign0相关源码(https://www.oschina.net/action/GoToLink?urlht
Wesley13 Wesley13
3年前
THINKPHP日常用到的基础知识
一、分组模式相关1、何时用分组模式一般情况下,在有前台跟后台甚至用户中心的时候,用到分组模式用的比较多2、如何使用分组模式项目文件夹里面会有个Conf文件夹,这里用于系统的配置,一般情况下只有一个config.php,但用到分组模式后,我们在里面一般会建几个文件夹,分别用于前后台的配置,比方说我们建立了Adm