技术宝典 | WebRTC ADM 源码流程分析
導讀:?本文主要基于 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::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 模塊處理,處理完最后是給設備播放。
作者介紹?
陳穩穩,網易云信資深音視頻客戶端開發工程師,主要負責 Android 音視頻的開發及適配。
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的技术宝典 | WebRTC ADM 源码流程分析的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 网易云信携手武汉三好教育,共筑教育援疆的
 - 下一篇: 融合通信常见问题2月刊 | 云信小课堂