0 導讀
隨着各個行業的互聯網化進程不斷演進,融合通信在越來越多的場景中得到應用,例如金融場景的視頻面籤、醫療場景的遠程會診、企業協作場景的多人視頻會議等。在這些場景中,通過微信小程序實現音視頻互通,可以降低用戶溝通成本,提升業務運營效率,同時幫助企業或組織進一步打通微信社羣中的溝通障礙,以一種輕量化的敏捷運營模式輕鬆實現微信生態中的客戶轉化閉環。因此,小程序音視頻必將受到越來越多企業和組織的青睞。
那麼,一個能夠支持微信小程序之間實現音視頻通話,同時能夠支持微信小程序跨平臺互聯互通的媒體服務器需要完成哪些事情?
1 RTMPGateway
微信在6.5.21版本通過小程序開放了實時音視頻能力,開發者們可以使用組件 < live-pusher > 實現基於 RTMP 的直播推流(錄製),在新版本中加入了 RTC 模式,用於實時音視頻通話上行,使用組件 < live-player > 實現基於 RTMP 的直播拉流(播放),RTC 模式則用於實時音視頻通話下行。可以看出,微信小程序的音視頻是基於 RTMP 協議的,同時,微信小程序的音視頻只是提供了終端上的能力,並沒有實現媒體服務器,那麼如何實現微信小程序之間的音視頻通話,同時實現和多平臺之間互聯互通?答案是肯定的,我們需要自己開發一個媒體服務器。
在網易雲信融合通信場景下,
- 微信小程序端使用 RTMP 協議,接入邊緣媒體網關,即 RTMPGateway;
- RTMPGateway 支持 RTMP 協議,完成微信小程序間的媒體轉發;
- 同時,RTMPGateway 將 RTMP 協議轉換成 RTP 協議,轉發給雲信邊緣媒體服務器,完成與雲信 SDK、標準 WebRTC 終端的互聯互通。
2 關於 RTMP 連接
上文提到 RTMP,這裏我們再簡單介紹一下,RTMP(Real Time Messaging Protocol,即實時消息傳送協議),是 Adobe 公司開發的一個基於 TCP 的應用層協議,被廣泛應用於直播領域。
RTMP 協議是基於 TCP 的,而在 TCP 連接建立完成後,無論是發佈還是播放一個 RTMP 協議的流媒體,都還需要經過以下幾個步驟:
RTMP 握手(Handshake)
- 握手開始於客戶端發送 C0、C1 塊;服務器收到 C0 或 C1 後發送 S0 和 S1。
- 客戶端收齊 S0 和 S1 後,開始發送 C2;服務器收齊 C0 和 C1 後,開始發送 S2。
- 客戶端和服務器分別收到 S2 和 C2 後,握手完成。
這就是一個完整的握手過程。
在實際工程應用中,一般是客戶端先將 C0、C1 塊同時發出,服務器在收到 C1 之後同時將 S0、 S1、 S2 發給客戶端。之後客戶端向服務器端發送 C2 塊,簡單握手完成。
Flash 播放器連接服務器時,若服務器只支持簡單握手,則無法播放 H.264 和 AAC 的流,可能是 Adobe 的限制,Adobe 將簡單握手改爲了有一系列加密算法的複雜握手(complex handshake)。
簡單握手的包是隨機的1536字節(S1/S2/C1/C2),複雜握手則是需要進行摘要和加密,此處不再贅述。
建立網絡連接(NetConnection)
- 客戶端發送命令消息(connect)到服務器,請求與一個服務器建立連接。
- 服務器接收消息後,發送確認窗口大小協議,設置帶寬協議,設置塊大小協議消息到客戶端。
- 客戶端處理設置帶寬協議消息後,發送確認窗口大小協議消息到服務器端。
- 服務端向客戶端發送“流開始”(Stream Begin)。
- 服務器發送(_result),通知客戶端連接的狀態。
建立網絡流(Create Stream)
- 客戶端發送命令消息(CreateStream)命令到服務器端。
- 服務器端接收消息後,發送(_result),通知客戶端流的狀態。
可以看出,建立一個 RTMP 連接,需要完成複雜的協議交互,並且這些協議交互都是同步的。那麼,如何實現一個異步的 RTMP 協議棧?
對於一個需要處理大量連接的 Server,無非兩種策略:多線程或使用異步 Socket。
例如對於 RTMPDump 提供的基於同步 Socket 的 librtmp,只能使用多線程處理多個連接,但是多線程的併發數上限明顯,同時需要處理複雜的鎖和同步等。Winlin 則是在 SRS 中使用 st 協程在單線程中實現了異步的 RTMP 協議棧。
而在雲信的 RTMPGatway 中,我們使用狀態機的方式,同樣在單線程實現了異步的 RTMP 協議棧。針對每一條 RTMP 連接,RTMPGatway 均會記錄連接目前所處狀態,以跟蹤整個連接的建立。
3 關於媒體協議封裝
爲了實現微信小程序與雲信 SDK、標準 WebRTC 終端的互聯互通,RTMPGatway 需要實現 RTMP 協議和 RTP 協議的轉換。
RTMP 封裝 AAC
微信小程序推拉流使用 RTMP 協議,音頻使用 AAC 編碼。
使用 RTMP 推送 AAC 直播流,首先需要發送“AAC sequence header”,其中包含重要的編碼信息,沒有它解碼器將無法解碼。AAC sequence header 存放的是 AudioSpecificConfig 結構,其結構描述非常複雜(詳見“ISO-14496-3 Audio”),可以簡化爲下表:
在發送這個 header 需要在前面分別加上1個字節(8bits)的 AudioTags 數據,其中每 bit 表示的意義如下圖:
其中 SoundData 的組成如下:
當數據的第一個字節爲0時,後面跟 AAC sequence header;當數據的第一個字節爲1時,後面跟AAC 數據。
RTMP 封裝 H.264
微信小程序使用 RTMP 協議進行推拉流,使用 H.264 進行視頻編碼。
使用 RTMP 推送 H.264 直播流,首先需要發送"AVC sequence header",同樣包含重要的編碼信息,沒有它解碼器將無法解碼。AVC sequence header 就是 AVCDecoderConfigurationRecord 結構(詳見“ISO-14496-15 AVC file format”),簡化爲下表:
在發送這個 Header 需要在前面分別加上1個字節(8bits)的 VideoTags 數據,其中每 bit 表示的意義如下圖:
發送的爲 avc 數據,所以 CodecID(後 4bit)的值爲 7;videodata 的數據打包方式爲 AVCVIDEOPACKET,具體的信息見下圖:
RTP 協議
RTP( Real-time Transport Protocol,即實時傳輸協議),爲語音、圖像、傳真等多種需要實時傳輸的多媒體數據提供端到端的實時傳輸服務,服務質量則由 RTCP(Real-time Transport Control Protocol,即實時傳輸控制協議)來提供。
RTP 協議頭信息包括 RTP 固定頭以及 RTP 擴展頭,如下圖:
RTP 固定頭
RTP 擴展頭
若 RTP 固定頭中的擴展比特位置 1,則一個長度可變的頭擴展部分被加到 RTP 固定頭之後,如果有 CSRC 列表,則在 CSRC 列表之後。頭擴展包含 16 比特的長度域,指示擴展項中 32 比特字的個數,不包括 4 個字節擴展頭(因此零是有效值)。
RTP 固定頭之後只允許有一個頭擴展。爲允許多個互操作實現獨立生成不同的頭擴展,或某種特定實現有多種不同的頭擴展,擴展項的前 16 比特用以識別標識符或參數。這 16 比特的格式由具體實現的上層協議定義。基本的 RTP 說明並不定義任何頭擴展本身。
IETF 針對 RFC3550 在檔次方面定義了一系列擴展協議,一些總結如下:
- RFC3550 - RTP: A Transport Protocol for Real-Time Applications (RTP):定義最基本的RTP/RTCP報文格式和收發規則;
- RFC3551 - RTP Profile for Audio and Video Conferences with Minimal Control :定義音視頻會議最基本的音視頻數據負載格式、編碼和傳輸,是其它檔次的基礎;
- RFC3711 - The Secure Real-time Transport Protocol (SRTP):定義了RTP在安全方面的增強,如加密、認證和重放保護;
- RFC4585 - Extended RTP Profile for Real-time Transport Control Protocol (RTCP)-Based Feedback (RTP/AVPF) :定義了RTP基於RTCP在及時反饋方面的增強,如定義NACK,PLI,SLI等RTCP報文;
- RFC5124 - Extended Secure RTP Profile for Real-time Transport Control Protocol (RTCP)-Based Feedback (RTP/SAVPF):綜合RTP/SAVP和RTP/AVPF的安全性和及時反饋性的最全面的檔次。
RFC 3550 定義五種 RTCP 報文,類型在報文頭部的PT域定義。
SR 報文用於發送端報告本端的數據發送統計信息和數據接收統計信息,RR 報文用於報告本端的數據接收統計信息,SDES 報文用於報告本端的描述性信息,BYE 在本端離開會話時發送,而 APP 則是特定於應用的數據。IETF 根據實際需求對 RTCP 的報文類型進行擴展,定義了一系列協議,此處不再贅述。
RTP 封裝 H.264
針對 H.264 的封裝,WebRTC 選擇了使用 RFC3984 的 Non-Interleaved 封裝方案。
Single NAL Unit Packet
Single NAL Unit Packet 是 RTP 最基本的打包方式,其中,
- forbidden_bit:禁止位,初始爲0,當網絡發現 NAL 單元有比特錯誤時可設置該比特爲 1,以便接收方糾錯或丟掉該單元。
- nal_reference_bit:nal 重要性指示,標誌該 NAL 單元的重要性,值越大,越重要,解碼器在解碼處理不過來的時候,可以丟掉重要性爲 0 的 NALU。
- Type:NAL 單元中的 RBSP 數據結構的類型,其中 0 未指,1-19 在 H.264 協議中有定義,20-23 爲 264 協議指定的保留位,24-29 在 RFC3984 中進行了指定。
Type 後面的數據爲 RBSP 的數據,需要注意的是:編碼器的每個 slice 或者每幀頭一般會有由0x000001 或者 0x00000001 作爲起始頭,在 RTP 封裝中需要去掉。此外在 H.264 裸碼流數據後面可能還會帶有 padding 的數據由 RTP 頭的 padding 位決定。
STAP-A
STAP-A 的作用是可以把多個 nal 單元封裝在一個 RTP 包裏面進行傳輸,需要注意:-A 的格式都是不允許跨幀的,也就是 nal 單元的時間戳必須是相同的。常見的場景是 sps 和 pps 兩個小包被合併封裝。
RTP 頭後面僅跟着 STAP-A 的頭,由 F、NRI 和 Type 組合而成,佔一個字節,這裏的 Type 爲 24。後面兩個字節爲第一個 nalu 單元的長度,後面跟第一個 nalu 數據同 Single NAL Unit 的封裝一致,第一個數據結束後,跟着第二個 nalu 的長度,佔 2 個字節,依次類推。
FU-A
FU-A 的作用是把一個原始大的 nalu 切成多個數據包進行傳輸,主要使用場景在 slice 比較大的情況下。FU-A 比較特殊,有 FU-A 起始包、FU-A 包(如果只切兩個包可能沒有)和 FU-A 結束包組成。
- FU indicator 佔一個字節,由 F、NRI 和 Type 組合而成,這裏的 Type 爲28。
- FU header 佔一個字節:
- S: 佔1位如果是1表示當前這個包是 FU-A 的起始包
- E: 佔1位如果是1表示當前這個包是 FU-A 的結束包
- R: 佔1位,保留位,爲0
- Type: 實際包含 nalu 的類型
4 關於協議轉換
幀完整性判斷
爲了實現微信小程序與雲信 SDK、標準 WebRTC終端的互聯互通,在協議轉換時,無論是 RTMP 到 RTP 還是 RTP 到 RTMP,對於視頻都需要首先拿到 H.264 數據,最重要則是對幀完整性的判斷。
在 RTMPGateway 收到 RTP 視頻包後,則需要先組成完整的幀才能再轉封裝成 RTMP 協議,失敗則進行I幀請求。而僅根據 RTP 頭部中的M比特判斷完整性是不準確的,在 RTMPGateway 中,幀的完整性判斷還需要滿足 :
- 本幀包個數 = 本幀末包序 - 本幀首包序 + 1
- 本幀首包序 = 上一幀末包序 + 1
下面以例子說明:
- 幀1幀2正常進入轉封裝。
- 幀3超時,進行I幀請求,上一幀末包序置19。
- 幀4判斷完整,非I幀,上一幀末包序置22。
- 幀5判斷完整,且爲I幀,進入轉封裝。
- 幀6超時,且丟失M包,上一幀末包序置27。
- 幀7一定超時,上一幀末包序置31。
- 幀8判斷完整,且爲I幀,進入轉封裝。
音頻轉碼
對於音頻,因爲微信小程序使用 AAC 編碼,雲信邊緣媒體服務器支持 Opus 編碼,因此在完成協議解封裝後,還需要完成音頻的轉碼。在雲信 RTMPGateway 中,我們採用了獨立的音頻轉碼線程組,減輕邏輯處理線程的壓力的目的。每個轉碼任務將被分配到固定的音頻轉碼線程,線程根據任務數量進行負載均衡。
關於小程序用戶和雲信雲端用戶管理
RTMPGateway 與小程序端間的信令採用 WebSocket 傳輸協議,此時 RTMPGateway 是 WebSocket 的服務端,接收來自小程序端的連接請求;RTMPGateway 與媒體服務器之間的信令同樣採用 WebSocket 傳輸協議,此時 RTMPGateway 作爲 WebSocket 的客戶端,向媒體服務器發起連接請求。
- 微信小程序端登錄後除了在 RTMPGateway 創建對應的 mini-user 對象,還需要模擬成一個對應的 mini-fakeClient 向媒體服務器登錄加入會議;
- 同時,RTMPGateway 在創建房間時,會針對這個房間模擬出一個特殊的用戶 room-fakeClient 加入房間;
- 在廣播房間及流信息時,如新的 NRTC 用戶加入,媒體服務器會向所有的用戶(包括mini-fakeClient、room-fakeClient)。對於 RTMPGateway,則只需要處理 room-fakeClient 收到的媒體服務器主動下發的信令,達到信令去重目的;
- 當新的 NRTC 用戶加入房間,room-fakeClient 將收到廣播信令,RTMPGateway 同步管理該 NRTC 用戶(nrtc-user)狀態。例如當收到該用戶的發佈流的信令後,將由 room-fakeClient 向媒體服務器發起訂閱音視頻流的請求。
6 寫在最後的話
一個完整的微信小程序網關服務架構,除了完成上述的 RTMP 協議棧、媒體協議轉封裝、房間業務管理等基礎模塊,我們還需要考慮:
- RTMP 協議是基於 TCP 協議的,而 TCP 協議自身的一些擁塞算法,在弱網環境下對網絡的退避策略過於激進,因此需要我們去尋求解決方案。例如在 RTMPGateway 中,對於 RTMP 下行在服務器引入 BBR 算法,通過直接修改內核 TCP 協議機制代碼,達到更高的帶寬利用率,降低卡頓。
- 關於 RTP 解封裝後的音視頻時間戳計算,可以有多種方案。例如在收到 SR 之前使用系統時間代替 NTP 時間,收到 SR 之後進行系統時間和 NTP 時間的誤差修正;也可以在收到 SR 之前,將收到的第一個 RTP 包時間設置爲起始時間,並使用固定採樣頻率根據公式計算時間戳,收到 SR 之後則先計算真實採樣頻率,再根據公式計算時間戳。爲了進一步解決音畫同步問題,在 RTMPGateway 中,對於 RTP 解封裝後的音視頻數據,將分別進入對應的 buffer,通過控制兩個buffer的讀取速度,以達到音畫同步以及控制時延的目的。
由於篇幅關係,更多細節不再展開,以上便是本次分享的全部內容。隨着應用場景越來越多元化,比如企業內部 APP 移動工作臺,系統集成電話呼叫功能,智能硬件,諸如智能門禁,智能機器人等將會對全終端的互通能力提出更高的要求,網易雲信在這條賽道的探索會將持續進行,盡力滿足用戶不同場景的需求,真正做到助力用戶內生長。
歡迎持續關注我們,瞭解更多網易雲信關於微信小程序網關服務的技術探索和實踐。
作者介紹
本森,網易雲信資深音視頻服務端開發工程師,負責雲信流媒體服務器、小程序網關服務器、 WebRTC 網關服務器及直播源站的開發工作,是帥的。