清華大作業指導:一人單刷雨課堂需要多少工作量?快手工程師詳解如何兩周搞定
昨天,清華自動化大一學生的 C++大作業霸占了知乎榜首,該作業要求學生寫一個類似于「雨課堂」的網絡教學軟件(雷課堂),可以共享屏幕、語音直播、在線答題……其實現難度、工作量似乎都超出了大一學生的能力范圍,連清華特獎得主、阿里 P6 也表示無法單獨完成。離提交 deadline 只剩五六周,這個作業真能寫完嗎?
在昨天的討論區,我們看到大部分評論都是對于這一作業的吐槽。不過也有人提醒大家,為什么不試一試呢?畢竟,作業還是要交的。
作為雨課堂的技術支持方,快手的音視頻工程師范威給出了自己的專業回答。范威有著十多年的工作經驗,在春節后深度參與了雨課堂和快手項目的聯合開發,為雨課堂提供了音視頻底層技術支持。
根據范威的介紹,總共需要 2 周開發時間,就能擁有一個雷課堂。當然,前提是「這些知識你都學過」,還要有「豐富的踩坑經驗」。
小伙伴們是不是心動了?下面來看具體分析。
一個人單擼一個雷課堂需要多少工作量?
雨課堂是清華大學研發的一款在線教育 APP,可以支持老師在線授課、分享 PPT,學生與老師進行語音互動。從音視頻角度來看,這種在線授課的形式其實就是一個標準的視頻會議場景。
要說如何從頭開始擼一個視頻會議軟件,需要分前端和后端來說。這里前端指的是終端,后端指的是媒體服務器。前端的主要功能是負責音視頻通信,后端的主要功能是負責媒體流的轉發。
先說說前端。前端模塊包括音視頻采集、前處理、編解碼、收發包等功能模塊。目前開源的視頻會議項目以 webrtc 最為流行,其代碼里有 80w+行之多。要想單手從頭擼一個視頻會議終端,雖說不需要 80w 行代碼那么多,但是還是有其難度的。
采集模塊
首先是平臺的支持。iOS、Android、Windows、Mac、Linux,不同平臺提供的音視頻技術都不盡相同。音視頻的采集和前處理,需要根據不同的平臺和機型進行適配,有些算法可以采用平臺的能力,而有些算法需要通過軟件進行處理。
先來說一下采集和前處理模塊。視頻通過調用系統 API 完成攝像頭數據的采集,這里不詳細說,感興趣的同學可以自行查閱官方文檔。視頻前處理包括各種美顏濾鏡算法,由于教育場景下這個功能不是剛需故而跳過。
這里說一下音頻前處理。在實時語音通話時,從麥克風直接采集到的音頻是包含自己說話的聲音以及對方說話的聲音的。這是因為本地的揚聲器播放出對方的聲音也會被麥克風采集進去。如果不加任何處理就發送出去的話,對方會從揚聲器里聽到自己說話,這種情況稱為聲學回聲(Echo)。為了得到一個比較自然的通話效果,需要對麥克風采集的音頻數據進行處理,消除掉對方說話的部分,只保留本地的聲音。這個過程叫做回聲消除(AEC,Acoustic Echo Cancellation)。AEC 算法最常用的方式是采用自適應濾波器來產生一個模擬回聲,然后再從麥克風采集信號中將這個模擬回聲抵消掉,達到消除回聲的目的。
單線 AEC 架構
在實際項目中,還會對信號做一個 NLP(非線性濾波)來消除殘余回聲,同時為了增強信噪比還會做聲學降噪和自動增益,統稱為 3A(AEC/AGC/ANS)。目前幾乎所有的智能手機和 Mac 平臺都有硬件的 3A 算法模塊,其中蘋果的設備調校的比較好,而 Android 手機的 3A 效果良莠不齊,通常需要通過軟件自己實現。最快的實現方式是采用系統提供的 3A 算法,這里算 1 個人天。
編碼模塊
接下來是編解碼模塊。編解碼是將經過前處理的音視頻原始數據進行壓縮,以達到網絡傳輸的目的。由于網絡資源的限制,原始的音視頻數據量太大,不能直接在網絡上進行傳播,必須先經過壓縮。壓縮分為無損壓縮和有損壓縮。音視頻數據通常采用有損壓縮的方式,可以做到非常大的壓縮比。視頻編碼算法常用的有 H.264/H.265/VP8/VP9/AV1/AVS2 等等,音頻的壓縮算法有 AAC/Speex/Opus/G.711/G.729 等?,F在通常采用的視頻壓縮算法是 H.264/H.265,而音頻算法則大都采用 Opus。
壓縮算法的細節非常繁雜,目前大多采用比較成熟的開源項目來實現,比如 x264,x265,ffmpeg 以及 libopus。蘋果設備也提供了內置的硬件視頻編解碼器 videotoolbox 可以支持 H.264/H.265 的實時編解碼,而 Android 則有 MediaCodec 提供相同的能力。在桌面平臺上,Intel 和 NVIDIA 的很多芯片提供了 qsv 和 nvenc 功能,用于實現桌面端的硬件視頻編解碼。有了編碼器之后,還需要對編碼參數進行正確的配置,以適合實時通信的場景。主要影響音視頻通話體驗的參數就是碼率,在其他參數不變的條件下,碼率越大音視頻質量就越好,而使用的網絡帶寬也越大。
這里為了簡化實現,使用系統提供的編解碼器實現,算 1 個人天。
傳輸模塊
經過編碼之后的音視頻數據已經小了很多,可以進行網絡傳輸了。Internet 網絡傳輸協議分為 TCP 和 UDP 兩種方式。TCP 協議是可靠傳輸,保證數據的完整性和有序性,但是缺點是在公網傳輸時速度比較慢,延時比較大。而 UDP 協議是不可靠協議,數據只管發,不能保證一定能夠到達對方,但是優點是發送速度快,延時低。因此在實時音視頻通信里,都會優先使用 UDP 協議進行數據發送。UDP 數據是以數據包為單位進行發送的,每次發送一個包,最大包大小不能超過 64K 字節。但是由于 IP 層的分片路由限制,通常一個 UDP 數據包的大小都會限制在一個 MTU(Max Transmission Unit)以內。以太網的 MTU 為 1500 字節,因此每個 UDP 包的大小大多都會限制在 1K 字節左右。而編碼后的視頻數據相對于這個大小還是太大了,需要對視頻數據進行進一步的分包才能進行發送。
由于 UDP 協議的特性是不可靠傳輸,因此數據包達到的先后順序也沒有保證。為了讓對方收到的音視頻數據的先后順序跟發送端一致,需要在接收端對 UDP 包進行排序。在視頻會議上,通常會采用 RTP 協議對分包之后的數據包進行一層封裝,每個 RTP 包都包含一個 RTP 頭,里面為每個 RTP 包分配了一個序列號。這個序列號是有序遞增的,因此接收端可以通過收到的 RTP 包的序列號對數據包進行排序,同時也可以知道哪些序號的數據包沒有收到,從而向發送端請求重發。
上面提到 UDP 在網絡傳輸過程中可能會丟包。由于音視頻編碼后的數據需要完整接收才能進行正常解碼,因此采用 UDP 協議傳輸的 RTP 包需要能夠處理丟包恢復。丟包恢復的方式有兩種,一種是 FEC(前向糾錯),一種是 ARQ(自動重傳),通常項目上這兩個方法會同時采用。
FEC 是對一組 RTP 包進行冗余編碼,產生出一些冗余包,冗余包包含了這一組 RTP 包的信息,在丟包的時候可以利用冗余包里的數據,恢復出這一組 RTP 包數據。常用的 FEC 算法包括 RS、卷積碼、噴泉碼等。FEC 的好處是不會引入額外的延時,冗余包和數據包一起發送給對端,對端通過接收到的數據包和冗余包嘗試恢復,沒有額外的交互時間,但由于網絡丟包的隨機性,并不是每一個 FEC 包都能夠被利用,這樣降低了整體的帶寬利用率
ARQ 則是精準的請求丟失的 RTP 包,讓發送端重新發送。在 RTP 協議里,可以通過 NACK 來實現重傳請求,攜帶上請求重傳的 RTP 序列號。發送端接收到 NACK 請求后,會重新發送該序列號對應的 RTP 包到對端。NACK 和重傳包的傳輸引入了額外的延時,因此 ARQ 會導致音視頻通信的延時增加,但是帶寬利用率比較高。
對于差一些的網絡,網絡的帶寬并沒有那么高,如果發送端編碼的音視頻數據超過了其發送的上行帶寬,就會導致網絡擁塞,產生丟包和卡頓。為了防止網絡擁塞的發生,發送端需要對自己的上行網絡帶寬進行預測,并反饋給編碼器,調整編碼器的碼率不要超過帶寬上限。這個算法叫做帶寬估計。帶寬估計是實時音視頻通信非常重要的一個算法,其準確性會很大程度上影響用戶體驗。帶寬估計算法的策略有很多,webrtc 中采用的是 google 提出的 GCC(Google Congestion Control)算法。
整個傳輸模塊沒有現成的開源項目可用,要么自己擼要么參考 webrtc 的實現,大概需要 3-5 天。
緩沖隊列
到這里,發送端模塊基本就介紹完了,下面說一些接收端要做的部分。接收端在接收到 RTP 數據包之后,首先根據 RTP 的序列號進行排序,如果有丟包,則通過 FEC 和 ARQ 進行恢復和重傳。得到完整有序的 RTP 包之后,對 RTP 包進行重組,組合成編碼后的音視頻數據。由于網絡傳輸的不穩定性,收到的數據并不是均勻的,有可能一會兒接收的快,一會兒接收的慢,造成數據接收的波動。這種現象被稱為網絡抖動(Jitter)。如果這時候直接進行解碼播放,那么會導致視頻忽快忽慢,聲音出現變聲的現象。為了平滑這種網絡抖動,接收端需要有一個緩沖隊列,將接收到的音視頻數據放入到緩沖隊列中,然后再勻速的從隊列中取出,從而得到比較平滑的音視頻數據進行播放。
從緩沖隊列取出的數據就可以進行解碼了。解碼就是解壓縮的過程,將發送端壓縮的數據還原成原始的音視頻數據,才能在本地進行播放。一些編碼算法(例如 H.264/H.265/Opus)為了提高壓縮比,在壓縮時對于前后連續的兩幀音視頻數據做了參考,這樣就導致采用這類壓縮算法的編碼數據在解碼的時候存在參考關系的依賴,只有前一個數據被正確解碼,后一個數據才能也正確的解碼。但是由于網絡丟包,即使采用了 FEC 和 ARQ 等丟包恢復策略,仍然有部分音視頻數據無法完整的達到接收端。這時候如果強行解碼,那么視頻會出現花屏,而聲音會出現爆音。為了解決這個問題,對于不連續的視頻數據,接收端需要向發送端請求編碼一個關鍵幀視頻數據。這個關鍵幀數據在解碼的時候不會參考其他視頻數據幀,同時可以被后續的編碼視頻幀參考,這樣可以解決后續視頻的參考關系問題。而音頻可以通過 PLC(Packet Loss Concealment)算法,根據波形產生出丟失的音頻數據。
這里可以參考 webrtc 的 neteq 隊列來實現(沒錯,只有 webrtc 開源,所以同學們沒有別的參考),大概需要 3 天
本地播放
最后就是本地播放解碼出的音視頻數據。在播放的時候,音視頻數據在網絡傳輸上并不一定是相同的速度,因此可能會產生音畫不同步的問題。這里還需要一個音視頻同步模塊,來控制音視頻播放的速度,保證聲音和視頻可以對齊。在對齊的時候,由于人耳對于音頻的快慢變化更加敏感,所以總是調整視頻的速度來對齊音頻。在每個音視頻數據中,都會帶一個時間戳(timestamp),這個時間戳是音頻和視頻數據采集時生成的,相同的時間戳的音視頻應該同時播放才能保證音畫同步。因此在播放視頻幀的時候,需要對比當前播放的音頻的時間戳,調整視頻播放的速度。
視頻在屏幕上播放的時候,其顯示的分辨率大小可能與實際編碼的分辨率大小不一致。視頻分辨率代表的是視頻像素點的個數,分辨率越高越清晰。對于現在的手機和顯示器來說,大多支持 HDPI,通過更加密集的像素點得到更加清晰細膩的圖像。而攝像頭采集和編碼的視頻分辨率并不會特別高,那么在顯示的時候需要對視頻進行放大。不同的視頻放大算法對于清晰的影響比較大,默認的 linear 放大算法會導致圖像比較模糊,采用復雜的放大算法,比如 bicubic,lanzcos,spline 等,可以得到更加清晰的畫面,而一些特殊內容采用特定的算法會得到更好的視覺效果(比如人臉部分采用 softcubic 可以達到美顏效果)。
這里不考慮實現效果的話,用最簡單的 opengl 渲染視頻,大概 2 天。
這樣,一個簡簡單單的實時音視頻通信的終端部分就開發完了,再配上一個炫酷拉風的界面,就可以食用了。但是這里只能實現兩個人之間的通信,而在線授課的場景可是一個老師對一群學生。如何實現多人之間的實時通信呢?這就需要后端的媒體服務器作中轉來實現。
媒體后端
這里需要簡單介紹一下多人實時音視頻通信的網絡拓撲結構。對于三人以上的實時通信,由于接收端同時會接收多個人傳過來的音視頻數據,因此如果沒有服務器中轉的話,需要采用網狀拓撲結構,每個終端的音視頻流都需要同時發送給其他所有終端,網絡帶寬成倍增加。為了解決這個問題,就需要采用星型拓撲結構,所有的終端將自己的音視頻數據發送給中央服務器,再由服務器來做轉發。這個服務器就是視頻會議的后端,暨媒體服務器。
媒體服務器的實現方式有兩種。一種叫做 MCU 模式,一種叫 SFU 模式。
MCU 模式是服務器將所有人的音視頻數據在服務端進行解碼,然后合成為一個新的音視頻流。這個視頻流是由所有人的視頻組合出來的,而音頻則是將所有人的音頻混合,之后重新進行音視頻編碼,再發送給所有終端。這樣終端播放出來的音視頻流就是一個合并好的音視頻流。這種模式對于媒體服務器的性能消耗很大,因此一臺服務器并不能支持很多的終端。
SFU 模式則只是做 RTP 包的轉發,并不做解碼和合流的工作。每個終端同時會接收到多個人的音視頻流,每一組音視頻流需要獨立進行處理和解碼,然后在本地混合后播放出來。這種模式的 SFU 性能消耗比較低,能夠支持的并發很高。
如果不考慮高并發和架構設計,最簡單的實現大概 3 天。
把這樣一個媒體服務器部署到網絡上,然后由它來轉發所有終端的音視頻數據包,這樣就可以實現多人之間的音視頻通信了。
總共需要 2 周開發時間,一個最最簡單的雷課堂就實現完成了。
當然,這里只是為了完成作業從功能角度來實現。在實際項目中,需要考慮到性能和架構優化,增強穩定性和音視頻質量,服務端高并發和快速部署,各種算法參數調優和策略優化。沒有幾十人團隊精細打磨 2-3 年,以及專業的音視頻質量測試實驗室,不可能做到業界頂尖水準。