技術乾貨 | WebRTC ADM 原始碼流程分析

網易雲信發表於2022-02-23

導讀:

本文主要基於 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 音視訊的開發及適配。

相關文章