导读: 本文主要基于 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 数据向下传递,后续具体流程参考数据流向章节。

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

关于 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::VoiceChannelvoid VoiceChannel::UpdateMediaSendRecvState_w() {  //***************    bool send = IsReadyToSendMedia_w();  media_channel()->SetSend(send);  }
// cricket::WebRtcVoiceMediaChannelvoid 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::AudioStatevoid 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::VoiceChannelvoid VoiceChannel::UpdateMediaSendRecvState_w() {  //***************    bool recv = IsReadyToReceiveMedia_w();  media_channel()->SetPlayout(recv);  }
// cricket::WebRtcVoiceMediaChannelvoid WebRtcVoiceMediaChannel::SetPlayout(bool playout) { //***************    return ChangePlayout(desired_playout_);}
// cricket::WebRtcVoiceMediaChannelvoid 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::AudioReceiveStreamvoid AudioReceiveStream::Start() {   //***************    audio_state()->AddReceivingStream(this);}
//webrtc::internal::AudioStatevoid 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 模块处理,处理完最后是给设备播放。

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

WebRTC ADM 源码流程分析相关推荐

  1. 技术宝典 | WebRTC ADM 源码流程分析

    导读: 本文主要基于 WebRTC release-72 源码及云信音视频团队积累的相关经验而成,主要分析以下问题: ADM(Audio Device Manager)的架构如何?ADM(Audio ...

  2. android 虚拟按键源码流程分析

    android 虚拟按键流程分析 今天来说说android 的虚拟按键的源码流程.大家都知道,android 系统的状态栏,虚拟按键,下拉菜单,以及通知显示,keyguard 锁屏都是在framewo ...

  3. springcloud ribbon @LoadBalance负载均衡源码流程分析

    一.编写示例 1.服务端 pom.xml <properties><java.version>1.8</java.version><spring-cloud. ...

  4. AQS 源码流程分析

    导读: 我们日常开发中,经常会碰到并发的场景,在 Java 中语言体系里,我们会想到 ReentrantLock.CountDownLatch.Semaphore 等工具,但你是否清楚它们内部的实现原 ...

  5. Android 9.0系统恢复出场设置源码流程分析

    前言 作为Framework层的开发人员,如果我们想让系统恢复出厂设置,一般有一下三种方式: 1.在[系统设置页面]进入[恢复出厂设置页面],点击[恢复出厂设置]按钮. 2.直接通过adb发送恢复出厂 ...

  6. Fabric源码流程分析之Orderer篇

    导言: 本文使用fabric1.1版本,此时有小朋友会问了,fabric都出1.4.2了你怎么还在看1.1呢!首先fabric自1.0以后大的架构基本没有变化,小版本升级只是功能性上更加丰满了,当然最 ...

  7. eureka源码流程分析

    这是euraka官网的架构图 从上面图中可以看到eureka的功能 服务注册 服务续约 服务同步 服务下线 远程调用 一.服务注册 这个服务提供者需需要把自己的实例注册到注册中心中(就相当于相亲时把自 ...

  8. MyBatis源码流程分析

    mybatis核心流程三大阶段 Mybatis的初始化  建造者模式 建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象.这种类型的设计模式属于创建型模式,它提 ...

  9. 【Android 插件化】Hook 插件化框架 ( 从源码角度分析加载资源流程 | Hook 点选择 | 资源冲突解决方案 )

    Android 插件化系列文章目录 [Android 插件化]插件化简介 ( 组件化与插件化 ) [Android 插件化]插件化原理 ( JVM 内存数据 | 类加载流程 ) [Android 插件 ...

最新文章

  1. 导入导出Android手机文件
  2. 下拉列表JComboBox,列表框JList
  3. 如何将Numpy加速700倍?用 CuPy 呀
  4. aMSN 0.97问题解决一则
  5. c#将字符串转换为数组_pandas入门: 时间字符串转换为年月日
  6. Machine Vision 浅谈
  7. 斯坦福 CS228 概率图模型中文讲义 五、马尔科夫随机场
  8. 如何将一个完整项目推到码云_怎么将本地项目放到码云(gitee)上面?图文详解
  9. 2种方式(线程间通信/互斥锁)实现两个线程,一个线程打印1-52,另一个线程打印字母A-Z,打印顺序为12A34B56C......5152Z...
  10. Visio2016绘制框图的基本操作方法
  11. 图论(Tarjan算法与无向图)
  12. SpringBoot电影网站源码(含数据库)
  13. Resin配置https
  14. Android虚拟机
  15. C语言初阶小练习(1)
  16. C语言实现来实现字符串反转,只有单词顺序反转,组成单词的字母不反转
  17. 火车头不能用mysql_火车头采集器发布失败常见问题汇总
  18. 原来Oracle也不喜欢“蜀黍”
  19. [转]从 .NET 开发人员的角度理解 Excel 对象模型
  20. GEE遥感云大数据在林业中的应用

热门文章

  1. Mac回收站清空还能恢复吗?2个方法快速找回废纸篓清空文件
  2. 创建型模型-单例模式
  3. 亚马逊选品是单一产品好还是诸多产品好呢?
  4. 《鹰猎长空》探析日本电影业在东西方文化间的摇摆
  5. mysql主从三个线程工作顺序_MySQL主从介绍、准备工作、配置主、配置从、测试主从同步...
  6. 台湾屏东大学校徽设计被指抄袭 校方:征选过程严谨
  7. 仿个人税务 app html5_假个税APP蹭热点窃信息防不胜防,你千万别下载错了!
  8. FPA Function Point Analysis 功能点分析培训免费视频地址(by陈勇)
  9. 小白网工成长笔记之OSPF(1)
  10. 关于ST-link驱动的问题