
我是來(lái)自聲網(wǎng)的SDK資深架構師,負責整個(gè)前端API。聲網(wǎng)在全球部署了軟件定義的實(shí)時(shí)網(wǎng) SD-RTN?,它為開(kāi)發(fā)者提供了實(shí)時(shí)音視頻專(zhuān)用網(wǎng)絡(luò )服務(wù)。之前有一位演講人說(shuō) API 很重要。確實(shí)是這樣的。
我會(huì )從這 4 個(gè)方面簡(jiǎn)要介紹一下我們的架構經(jīng)驗:
- RTC 場(chǎng)景現在面臨的問(wèn)題和挑戰;
- 重點(diǎn)介紹一下架構和API的設計和思想;
- 如何對架構上進(jìn)行重構或代碼改進(jìn),從而更好地控制媒體和網(wǎng)絡(luò );
- 為了 SDK 的低延遲、高性能、高并發(fā),我們做了哪些探索。
考慮到大家對 RTC 領(lǐng)域不是太了解,我先簡(jiǎn)單介紹一下。其實(shí)它是一個(gè)很傳統的實(shí)時(shí)音視頻場(chǎng)景,現在最主流的技術(shù)是由谷歌提供 WebRTC,利用它,你可以通過(guò)瀏覽器與另一個(gè)人進(jìn)行實(shí)時(shí)音視頻的通話(huà)。聲網(wǎng)也參考了一些 WebRTC 的設計,從最開(kāi)始的一對一通話(huà),然后到一對一多通話(huà),到現在一個(gè)頻道可以支持上百萬(wàn)的用戶(hù),其中也有很多技術(shù)挑戰。
問(wèn)題與挑戰
首先,從場(chǎng)景角度講,我們會(huì )遇到的問(wèn)題和挑戰有哪些呢?
- 傳統的 RTC 場(chǎng)景:現在我們可以看到很多場(chǎng)景,例如說(shuō) 4K 高清視頻,如果傳統的SDK不做改善的話(huà),傳輸一個(gè) 4K 視頻,對它的內存、CPU等各方面都會(huì )帶來(lái)極大的挑戰。
- 娛樂(lè )社交和在線(xiàn)教育:現在不光需要打開(kāi) Web 瀏覽器、攝像頭,還需要打開(kāi)本地的播放器,傳輸本地播放器的內容。
- 云游戲加速:現在很多廠(chǎng)商還在開(kāi)發(fā)云游戲,游戲運行于服務(wù)端,數據以音視頻、指令等形式傳輸至手機,手機僅僅負責渲染,其中最大的挑戰就是延時(shí),如果從服務(wù)端到手機的傳輸延時(shí)超過(guò) 200ms 的話(huà),游戲體驗會(huì )變得很差,這就需要一個(gè)類(lèi)似于聲網(wǎng)的實(shí)時(shí)碼流加速傳輸網(wǎng)絡(luò )。
- SIP/PSTN:SIP傳統的網(wǎng)絡(luò )電話(huà),在全球有大量的業(yè)務(wù)需求,通過(guò)網(wǎng)絡(luò )的流量來(lái)達到整個(gè) RTC 的效果。
- WebRTC 加速:如果在中國和美國之前通過(guò)公網(wǎng) P2P 溝通,卻缺少一個(gè)底層網(wǎng)絡(luò )網(wǎng)和SDK的介入的話(huà),其實(shí)是很難工作的。一個(gè)沒(méi)有任何 QoS(服務(wù)質(zhì)量)保障的連接,通話(huà)會(huì )很糟。
這些都是我們在 RTC 領(lǐng)域會(huì )遇到的場(chǎng)景,而 WebRTC 一類(lèi)的開(kāi)源引擎是遠不能達到我們對場(chǎng)景的技術(shù)要求的,需要一個(gè)具備網(wǎng)絡(luò )傳輸、音視頻編解碼等能力的 SDK 來(lái)實(shí)現。
面對這樣的場(chǎng)景需求,SDK 需要具備哪些特性呢?
首先是合理的架構設計,它有兩個(gè)特點(diǎn):第一點(diǎn)是媒體和網(wǎng)絡(luò )是獨立控制的。因為在類(lèi)似 PSTN、云游戲加速傳輸的場(chǎng)景中,它的媒體數據是由自己處理的,僅需要我們提供網(wǎng)絡(luò )傳輸加速的能力。但像 4K 音視頻的實(shí)時(shí)傳輸,從采集、編碼、渲染到傳輸,都需要 SDK 來(lái)完成。所以對于不同場(chǎng)景,SDK 就需要提供不同層次和不同模塊的接口。
第二是面向對象的 API 設計。關(guān)于 WebRTC 有個(gè)小故事,P2P 連接的協(xié)商過(guò)程是通過(guò) SDP 協(xié)議做的,而整個(gè)能力協(xié)商的過(guò)程通過(guò)交換 offer 和 answer 就可以快速握手。最初這種設計認為協(xié)商過(guò)于復雜,一般的工程師搞不懂,所以并沒(méi)有開(kāi)放接口讓開(kāi)發(fā)者控制SDP相關(guān)內容。微軟在進(jìn)入 RTC 領(lǐng)域后,基于 WebRTC 貢獻了 ORTC 項目,它 API 設計則是面向對象的。他們曾經(jīng)有過(guò)這樣一個(gè)看法,如果可以開(kāi)放更多面向底層、面向對象的 API,開(kāi)發(fā)者可以根據自己的場(chǎng)景需要來(lái)搭建。這也是面向對象 API 設計的重要性。
現在很多提供 API 的公司都強調一點(diǎn),叫做易用性,十幾行代碼就可以讓你實(shí)現某個(gè)功能。因為以前開(kāi)發(fā)者的能力普遍還沒(méi)有那么強,也不清楚 RTC 場(chǎng)景是怎樣的,所以我們通過(guò)這種簡(jiǎn)單的方式,讓任何一個(gè)小白開(kāi)發(fā)者都可以輕松做出一個(gè) App。隨著(zhù)這些年的發(fā)展,場(chǎng)景變得越來(lái)越復雜,開(kāi)發(fā)者的能力也越來(lái)越強,我們完全可以提供面向對象的 API,讓開(kāi)發(fā)者自己通過(guò)它們構建自己想要的場(chǎng)景。
除了合理的架構設計,還要支持豐富的媒體傳輸能力,具備低延時(shí)、高性能、高并發(fā)的特性等。這些我稍后會(huì )詳細分析。

架構與API設計
先說(shuō)一下傳輸 SDK 的分層。如上圖,SDK 的分層最底下是網(wǎng)絡(luò )層。最早之前的一些網(wǎng)絡(luò )傳輸都是基于 TCP 的,TCP 和 UDP 之間的區別,我就不說(shuō)了,但是對于媒體的實(shí)時(shí)傳輸來(lái)講,在有網(wǎng)絡(luò )丟包時(shí),TCP 的延時(shí)會(huì )非常大,完全不能滿(mǎn)足實(shí)時(shí)互動(dòng)的要求,所以最核心的是說(shuō)媒體其實(shí)是不需要,就是在網(wǎng)絡(luò )上丟包的情況下,TCP現在幾乎所有的媒體實(shí)時(shí)傳輸都是基于 UDP 實(shí)現的,包括比較新的 QUIC 協(xié)議,底層也是基于 UDP的。
Transport(UDP)上面是擁塞控制與網(wǎng)絡(luò )連接控制,這是 RTC 領(lǐng)域最重要的一個(gè)技術(shù)環(huán)節和算法模塊。目的是要在比較復雜錯綜的網(wǎng)絡(luò )環(huán)境下,實(shí)現更靈活的網(wǎng)絡(luò )控制。
然后是 Media stream 層,它類(lèi)似于一個(gè) RTP 的協(xié)議,更多是面向媒體流,這一層有時(shí)間戳和一些標準的協(xié)議。
再上面就是 Media Engine。Media Engine有兩層,一層是編解碼器,一層是輸出編碼后的數據,比如 VP8、VP9,也包括一些傳統的編碼碼率。
再往上是 Frame YUV/PCM。WebRTC 一般只能傳YUV和PCM的數據。這里講一個(gè)小的故事,很多中國的開(kāi)發(fā)者會(huì )把 WebRTC 當成一個(gè) SDK 用,其實(shí) WebRTC 根本算不上是一個(gè) SDK,它僅僅是一個(gè) Media Engine。Media Engine 和 SDK 最主要的差別是什么呢?Media Engine僅僅是提供了一個(gè)功能,比如說(shuō)像谷歌自己也有 RTC 的功能,它僅僅是把 WebRTC 的代碼當成一個(gè)功能模塊來(lái)使用,Chromium 才是一個(gè)真正的 SDK。
說(shuō)完網(wǎng)絡(luò )與對象的簡(jiǎn)單分層,我們來(lái)一起看一下對象的建模。

我們去分析一個(gè)業(yè)務(wù)場(chǎng)景,或者是去設計一個(gè) API,最重要是要了解你控制的對象是什么。首先,我們一般的輸入源有攝像頭、屏幕共享、錄音設備,以及文件或客戶(hù)自定義數據,對于這些對象,我們通過(guò) Audio Source 和 Video Source 作為管理,既可以管理 YUV/PCM 這種原始采集數據,也可以管理類(lèi)似 H264/VP8 這種編碼后數據。這些數據源可以產(chǎn)生媒體流,對于媒體流對象,我們用 Video Track 或者 Audio Track 來(lái)管理,對于本地發(fā)布流和遠端訂閱的流,用 local 和 remote 作為區分。而最重要的模塊自然就是網(wǎng)絡(luò ),我們抽象為一個(gè)叫 RTC Connection 的對象,負責網(wǎng)絡(luò )連接到我們的 SD-RTN? 上。每一個(gè) Connection 都有且只有一個(gè) local user 負責媒體流的發(fā)布和訂閱。除此以外,video 和 audio 的處理模塊也都對象化處理,如 video filter、audio filter、audio device manager 等。把媒體流發(fā)布到這個(gè) Connection 上,你可以進(jìn)行遠端的通話(huà)了。
在這里我們可以看到面向對象 API 的一些優(yōu)點(diǎn)。你可以在其中創(chuàng )建多個(gè)對象,對應這個(gè)圖來(lái)講就是可以創(chuàng )建多個(gè) Local Video Track,能同時(shí)有幾個(gè)或幾千個(gè) RTC Connection,可以同時(shí)與多人建立連接,或者創(chuàng )建更多頻道。
從我們的理解來(lái)講,API 的設計還有一個(gè)非常重要的地方。很多初級開(kāi)發(fā)者都會(huì )覺(jué)得 API 僅僅是把 SDK 的功能體現給使用者。而在我們看來(lái),好的 API 設計“能自己講故事”。當別人看過(guò)你 30%的 API 之后,就能知道你整個(gè)架構和設計理念是什么,它能成為架構師與開(kāi)發(fā)者對話(huà)的一個(gè)渠道。如果發(fā)送編碼數據和發(fā)送原始數據 是完全兩套API的style,就會(huì )給開(kāi)發(fā)者帶來(lái)困惑。所以在 API 的設計之中,架構要做的不僅僅是展現功能,還將你的API 設計理念通過(guò) API 傳達給使用者。

舉一個(gè)例子。我們怎么實(shí)現與遠端用戶(hù)的通話(huà)。首先你要創(chuàng )建一個(gè) Connection,你作為一個(gè) Local User 想要發(fā)布流就需要一個(gè) Local Track,這時(shí)候你需要調用 Publish Track 把 Local Track 發(fā)送到 Connection 上,這樣遠端的用戶(hù)就能看到你了。同樣的,你也可以去訂閱遠端用戶(hù)(Remote Users)的流,他的 Remote Track 會(huì )通過(guò) Connection 發(fā)送到 Local Users 這一端。這就是一個(gè)完整的“故事”。在聽(tīng)完這個(gè)“故事”之后,如果有一天你想傳輸你的攝像頭數據,對你來(lái)講,它仍然是一個(gè) Track,只是 Source 不同了。只有會(huì )講“故事”的 API,才能讓用戶(hù)理解如何去靈活使用。
另外,還有很重要的一點(diǎn),就是不要創(chuàng )造新的名詞,應該符合全球定義的標準。我們在定義 API 的時(shí)候,就會(huì )大量地翻閱一些國際標準,比如 W3C 的,這些都是符合開(kāi)發(fā)者認知體系的。
媒體和網(wǎng)絡(luò )控制
接下來(lái),我們講講架構設計里面的一些具體實(shí)現。
我不知道大家是否聽(tīng)過(guò) SOLID 法則。在講它之前,我們要講講為什么說(shuō) WebRTC 只是一個(gè)功能模塊。當你去玩一些開(kāi)源項目,谷歌提供的能力也好,WebRTC 的開(kāi)源代碼也罷,你可能會(huì )發(fā)現它的適用場(chǎng)景非常單一,它只是適合 P2P 或者跟一些服務(wù)器打交道。
作為一個(gè) SDK,要講功能開(kāi)放給開(kāi)發(fā)者,就必須要實(shí)現一個(gè) Pipeline。從最簡(jiǎn)單的 Pipeline 來(lái)講,有 5 個(gè) SOLID 法則:
- 單一責任法則。假如你有一個(gè) 100 人的團隊,每個(gè)團隊都有自己的任務(wù),有做降噪的,有做視頻編碼的,好的架構是讓這些人只需要專(zhuān)注于自身的功能模塊的實(shí)現,代碼如何寫(xiě),算法如何改進(jìn),而不需要去考慮其它模塊中的業(yè)務(wù)。
- 開(kāi)閉法則。當你需要開(kāi)發(fā)一個(gè)新功能的時(shí)候,不需要去修改之前的代碼,這是好的架構。
- 模塊可替換。作為一個(gè)好的 SDK 架構,SDK 中的任何接口和模塊都是可以被無(wú)縫替換的。
- 接口隔離。用戶(hù)可以清楚找到控制對象或者接口,而不需要理解很多不感興趣的接口。
最后,依賴(lài)反轉是特別重要的一點(diǎn)。任何API 都需要面向接口編程,這樣一來(lái),用戶(hù)就不需要去理解模塊內部是如何實(shí)現的,只需要看接口就行了。

我們的 Pipeline 如上圖所示。綠色的是接收端,中間通過(guò) Agora SD-RTN?進(jìn)行傳輸。我們會(huì )將一些算法、引擎等用 Pipeline 的方式進(jìn)行組織。基于 SOLID 法則,我們面向各種場(chǎng)景的應用,代碼會(huì )變得越來(lái)越快、越來(lái)越方便,算法專(zhuān)家也不用去了解其他模塊,只專(zhuān)注于手上的工作。

舉個(gè)例子,我們有一個(gè)叫做 Media Player Kit 的組件,它支持本地媒體播放和多流互動(dòng)(詳見(jiàn)我們此前的文章),如上圖是它的架構。Media Player 可以支持本地媒體播放,也可以將本地視頻流發(fā)送到遠端。如果你還記得“API需要講統一的故事“,就能想象到,Media Player 是一個(gè)媒體數據源,可以提供 video track 和 audio track,如果將這些 track 加上 renderer,就可以本地播放,如果把這個(gè) track 發(fā)布到 RTC Connection 就可以和遠端用戶(hù)共享了。
Pipeline 就像一個(gè)管道一樣,一般來(lái)說(shuō) Pipeline 都是單向的,從管道的入口到出口,但其實(shí)Pipeline 里最核心的一些控制是通過(guò)負向反饋來(lái)做的,這也是控制理論經(jīng)典的話(huà)題。

在 RTC 領(lǐng)域里,有一個(gè)很核心的 Pipeline 叫“帶寬估計”,它可以實(shí)時(shí)監控當前網(wǎng)絡(luò )是否有擁塞,當發(fā)現有擁塞的之后,會(huì )立即反饋預估的帶寬值到 Video Quality Controller 模塊,動(dòng)態(tài)調整碼流、幀率,以保證音視頻流的實(shí)時(shí)體驗。如上圖所示,Video Quality Controller模塊同時(shí)還會(huì )監聽(tīng) CPU 狀態(tài),因為低端手機,遇到較高幀率、分辨率的視頻會(huì )容易遇到 CPU 的性能瓶頸,從而出現卡頓。Video Quality Controller 模塊會(huì )基于收到的帶寬估計和 CPU 狀態(tài)信息來(lái)動(dòng)態(tài)改變編碼碼率,比如你現在發(fā)送的是 2M 的碼流,但是遇到了網(wǎng)絡(luò )擁塞,那么就會(huì )降低一些畫(huà)質(zhì),改為發(fā)送 1M 的碼流,能保證通話(huà)是流暢的。
在架構中,策略層和功能層是要嚴格區分的。從上圖來(lái)講,實(shí)線(xiàn)的部分就是數據通道,它提供了視頻的采集、編碼、傳輸功能,而下方的模塊則是策略層,負責根據網(wǎng)絡(luò )及設備情況來(lái)反饋給功能模塊,調整其中的碼率、幀率這樣的參數。
低延時(shí)、高性能、高并發(fā)
除此弱網(wǎng)對抗的算法等常規方法以外,我們還可以在開(kāi)發(fā)工具層面來(lái)進(jìn)一步優(yōu)化網(wǎng)絡(luò )延時(shí)。就好像萊特兄弟造飛機一樣。他們做的最重要的一項設計就是風(fēng)洞。這飛機真正試飛前就可以進(jìn)行充分的測試。我們也一樣,在此方面也花費了很多精力。我們做了配套的性能調查工具、系統工具,比如perf性能瓶頸的查找,熱點(diǎn)代碼的定位等,以此來(lái)做到 SDK 的白盒化。我們通過(guò)這些工具來(lái)不斷優(yōu)化SDK 的各項指標,包括延時(shí)、弱網(wǎng)對抗、內存優(yōu)化、CPU 優(yōu)化等。
以分段延時(shí)為例,如果以光的速度來(lái)計算,從中國到美國直線(xiàn)傳輸大概需要 30ms。我們聲網(wǎng)在全球的平均延時(shí)可以達到 76ms。下圖是一個(gè)傳輸的分段延時(shí)示意圖。我們通過(guò)工具來(lái)對每段延時(shí)生成清晰的報表。這些監測數據讓我們能有針對性地優(yōu)化不同的模塊。

同時(shí),我們還要對弱網(wǎng)對抗算法進(jìn)行不斷的驗證和優(yōu)化。我們會(huì )模擬丟包、模擬延遲,我們在算法上會(huì )關(guān)注碼率跟蹤速度、帶寬預估準確度。如下圖所示,紅線(xiàn)是我們的預估值,黑線(xiàn)是驗證的數值,兩者越接近,說(shuō)明碼率控制得越好。

在高性能方面,我們提出了內存池和線(xiàn)程池的概念。
我們需要根據系統內存情況,自動(dòng)調整內存池的大小,不同大小的空閑隊列需要自動(dòng)進(jìn)行負載均衡,同時(shí)要有效地減少 malloc/free 調用次數、頁(yè)錯誤數量。確保 SDK 在低內存環(huán)境中的可用性。
在某些服務(wù)器推流的場(chǎng)景下,高并發(fā)可以極大的降低用戶(hù)的服務(wù)器使用成本。如果每一路通話(huà)或者推流都需要一個(gè)進(jìn)程實(shí)例的話(huà),在并發(fā)情況下,CPU 會(huì )消耗在線(xiàn)程切換上。在我們的 SDK 中,我們可以通過(guò)線(xiàn)程的方式多開(kāi)實(shí)例,可以極大地降低線(xiàn)程梳理,從而提高并發(fā)量。我們也進(jìn)行了一些測試,業(yè)界其他產(chǎn)品在相同機器環(huán)境下,并發(fā)路只有 600 路,而我們聲網(wǎng)的最大并發(fā)數可以到達 3400 路。

在我們的SDK中,線(xiàn)程是通過(guò)統一的線(xiàn)程池管理的,這種做法既讓研發(fā)功能模塊中,降低并發(fā)編程模型的復雜度,有可以讓我們的線(xiàn)程數目受控,比如如果模塊或者功能團隊需要新的線(xiàn)程,需要提出申請,SDK通過(guò)注入的方式,將線(xiàn)程給予模塊使用。這對于 SDK 的性能改善會(huì )很有幫助。來(lái)源:聲網(wǎng) Agora