基於Nginx的媒體服務器技術-線上公開課

國內應用比較多的開源流媒體服務器nginx-rtmp-module一直存在功能少、集羣化難度大等問題。在LiveVideoStack線上分享中,PingOS 開源項目組開發工程師、UCloud RTC研發工程師朱建平詳細介紹了基於nginx-rtmp-module的PingOS流媒體服務器在http-flv、http-ts、hls+、多進程、轉推、回源以及集羣化部署方面的技術實現細節。

 

文 / 朱建平

整理 / LiveVideoStack

直播回放https://www2.tutormeetplus.com/v2/render/playback?mode=playback&token=006643cdea15499d96f19ab676924e88

1. Nginx流媒體擴展:http-flv、http-ts、hls+

最初始的nginx-rtmp-module相關模型與包括SRS在內的多數流媒體服務器實際上是一樣的(1個生產者,n個消費者)。Nginx存一個問題:它僅僅做了RTMP的消費模型,如果想擴展 http-flv或http-ts的形式會較爲困難。由於rtmp-session僅供RTMP協議使用,如果想擴展http-flv,首先我們需要了解其基礎分發模型(如上圖所示):所有的生產者與消費者都會被掛載到同一個stream中,生產者負責從網絡端接收數據,消費者從buffer中獲取數據對外發送。

如果是發送flv數據,那麼可以保留原有rtmp-session,當服務器收到一個HTTP請求時,創建一個rtmp-session,此session與網絡不相關,僅僅是邏輯上的session。然後將這個session注入stream當中,如果是以消費者的角色注入進stream當中,則可以實現獲取數據並往外分發。假如此時服務器收到的是http-flv的請求,就可以創建一個邏輯上的session,並把它注入stream中,此時理論上我們可以獲得的是rtmp的數據。但我們需要的是flv的數據,由於flv數據與rtmp數據相似,我們可以通過tag-header的方式非常簡單的將rtmp數據還原成flv數據。根據上述思路,在生產者和消費者模型中,消費者可以通過創建http-fake-session的形式來複用以前的分發流程並實現http-flv協議。我們對其進行擴展,創建一個http-fake-session作爲生產者,並讓http-fake-session與一個http client進行關聯,關聯之後http client負責從遠程服務器端下載數據傳遞給生產者,生產者就可以把這些數據通過分發模型分發給下面的rtmp-session。這樣也就間接實現了一個http回源的功能。通過上述思路我們就能夠快速地實現http-flv的播放與拉流。同樣,我們可以根據上述思路繼續擴展協議。假如我們在收到一個http請求之後,創建一個同樣的rtmp-fake-session(邏輯上的session,與網絡不相關),我們把它以消費者的角色插入到 stream當中。這樣就可以從stream當中獲取到需要向下分發的數據。需要注意的是:stream中最初保存的是rtmp數據而不是ts數據,無法直接獲取ts數據。

1.1 http-flv在Nginx中的實現

基於Nginx實現http-flv需要注意以下幾點細節:首先該實現複用了Nginx的分發模型以及http功能模塊。(Nginx對http協議棧的支持更加完善,包括http1.0、http1.1協議)在部分線上業務中,客戶可能需要在下載http-flv時添加後綴,按照以往的實踐邏輯我們會在代碼當中過濾後綴。如果遇見更爲複雜,如修改是否需要開啓http chunked編碼的需求,我們就只能修改代碼。而如果是基於Nginx通過複用http的現有模塊來實現http-flv,我們就可以通過nginx-http-rewrite功能來實現這些操作。因此使用nginx-http的原生功能來開發http-flv可以帶來更多好處,如顯著降低代碼量。

在這裏我曾經看到過一種情況:即複用了http模塊,但沒有複用rtmp的分發流程。這樣就會導致我們需要將分發流程在http-flv中重新再做一遍,對業務的控制就會變得非常複雜。

舉個例子,假如此時有人請求播放,需要將消息通知給業務服務器。此時,如果rtmp與http-flv兩種協議的實現是分開的,那麼意味着如果兩者都被觸發,就需要分別向業務服務器進行彙報。於是我們就需要付出雙倍的代碼與邏輯維護工作,這無疑會顯著增加開發與維護成本。因此,最簡單的實現方案就是flv不做任何與業務相關的處理,僅在下發的時候進行格式轉換,相當於rtmp分發時只發 rtmp格式的數據,而flv分發時只需要將rtmp的數據打上flv的tag-header,然後再進行下發,這樣就省去了業務層的開發。

http-flv播放實現

圖中展示的是rtmp的緩存對於rtmp和http-flv這兩個協議的支持。http-flv和rtmp二者共用一套緩存,其實rtmp本身傳輸的就是flv的數據,只不過是把tag-header給拋掉了。

http-flv的下發與rtmp的下發唯一的區別點在於send函數不同:http-flv調用的是http的send函數,rtmp下發時調用的是原生的send函數,在下發前需要添加各自的協議頭。二者共用一塊內存可以達到節省內存的效果,並且實現業務統一,降低開發成本。

http-flv回源的實現

圖中展示的是http-flv回源在nginx中的實現。http-flv回源實現的思路與http-flv的播放實現思路類似:即在需要回源的時候創建一個http client,http client所做的事情就是把http數據下載到本地。在下載數據到本地之前http client需要先創建一個rtmp fake session並將其作爲生產者注入stream當中。而後http client開始從網絡上下載數據並且將下載到的fIv數據拆成rtmp數據。爲什麼要拆成rtmp數據?這是因爲rtmp的推流過來的緩存數據類型是rtmp,因此從網上下載到的flv數據需要做一次拆分,拆成rtmp的數據,然後放入緩存。最終根據實際要求將數據轉成rtmp或flv的格式。這樣按照http-flv播放中rtmp fake session的邏輯,也就能夠快速的實現http-flv的回源操作。

1.2 http-ts在Nginx中的實現

圖中展示的是http-ts在Nginx中的實現。其實現思想與http-flv的實現基本一致,僅僅是在操作上有所不同,不同點在於http-ts需要一個獨立的buffer進行緩存。由於http-ts與http-flv的數據格式相差較大,對於flv數據到rtmp來說,只需要將數據拆成一個個小塊,並在前面添加一個header。即使flv數據的最一幀或者一個分塊缺少也不用補齊。

但是ts數據不同,它的要求比較嚴格,每一分塊必須爲188字節,其中包括ts header以及有效載荷部分。並且如果數據庫大小不足188個字節,則需要補齊。而rtmp的數據塊沒有嚴格固定要求其長度大小。對於ts數據來說,要想將flv數據轉成ts數據,這個過程是需要消耗一些計算量的。由於ts數據和flv的數據格式相差太大,因此在這裏我們將ts的buffer與rtmp的buffer完全獨立開。但此操作並不是默認開啓的,需要在服務器中進行配置。

開啓配置後,纔會將rtmp的buffer生成一份鏡像的ts數據,這一部分的ts數據僅會供http-ts和hls兩個協議使用。服務器中還涉及到一個原生的hls服務,在這裏我們沒有做任何的改動,而是加入了hls+的服務來使用這個buffer。無論是ts還是hls+,它們都註冊了自己的fake session,這樣做的目的是爲了統一業務。例如在有播放請求進入時,我們需要讓業務服務器知道當前有請求產生。

類似這種網絡通知、事件通知的接口,在開發的過程中大家都希望只需要編寫一份業務數據,而不是說做hls協議要針對hls播放寫一個通知,做ts協議還要針對ts再寫一份通知,這樣業務代碼會越來越龐大,最後導致服務幾乎就很難維護。

因此fakesession的作用是非常大的,其會把網絡層與業務層完全隔離開。即使服務器本身的下發協議不是rtmp,創建一個rtmp-session並掛載到業務服務器中即可。總的來說,http-ts與http-flv唯一實現區別就是獲取buffer的位置不同。http-flv需要從rtmp buffer獲取,http-ts則是從ts buffer中獲取。如果能理解http-flv的協議流程,那麼也就不難理解http-ts的實現流程。

1.3 hls+在Nginx中的實現

圖中展示的是hls+在nginx中的實現。hls+與傳統hls不同,傳統hls在服務端沒有狀態,服務端包含大量碎數據,客戶端在不斷執行下載,而hls+則會記錄每一個客戶端的狀態。對於如何記錄每個客戶端的狀態,之前我曾嘗試通過對hls+的連接創建一個虛擬連接用來記錄狀態。但是發現業務會比較複雜,並且後期會存在很多問題,包括代碼量、bug以及維護成本等。於是更換另外一種思路,還是用fake session的方式來實現。利用fake session作爲消費者放入,根據每次進入的http,連接,通過session ID進行綁定。

由於第1次發送hls請求時客戶端是不知道sessionID的,如果服務器獲取到一個沒有session ID的連接,則認爲此客戶端爲第1次進入。客戶端會接收到一個302的回覆,302回覆中會告訴客戶端一個新的地址,其中包含一個session ID。客戶端得到session ID之後,再次請求m3u8時,會加入session ID,服務器就可獲取相應session ID並對客戶端進行身份區分。這樣就能夠通過session ID記錄每一個客戶端的播放狀態。爲什麼要記錄這個狀態?這主要是因爲服務器不是將數據直接寫入硬盤而是放進內存,它需要知道每一個用戶、每一個客戶端的下載進度,並根據不同的進度從內存中定位ts數據。

hls+和http-ts它們共用了一個 ts buffer,並且hls+是實時的從buffer中定位ts內容。所以對於hls+來說,並沒有真正的ts數據產生,只是記錄每一個文件在內存裏面的偏移量。因此hls+不存在讀寫的問題,在做hls服務時,以前可能會遇到過一個問題——讀寫硬盤的瓶頸。機械硬盤的讀寫速度比較慢,普遍的解決思路就是掛載一個虛擬硬盤,將內存映射到目錄中進行讀寫。

如果採用的是hls+的方案,就可以省去掛載的操作,對於內存也並沒有太多的消耗。而且如果同時有hls+以及 http-ts的需求,此時對於內存的利用率是非常高的。

2. 靜態推拉流

靜態推拉流主要是爲了滿足集羣化的需求。如果單臺服務器不足以支撐服務的高併發量,那麼我們就需要考慮服務器的擴展性。除此之外如果用戶分散在全國各地,還需要進行服務器之間的打通。但是如果業務沒有那麼複雜就可以選擇使用靜態推拉流。

靜態推拉流服務配置如上圖所示,首先看靜態拉流:首先存在一個目標源站,如果使用靜態回源,那麼目標地址會被配置在配置文件當中,目標源站能隨意更改。

圖中展示的是一個簡單的靜態拉流模型:如果來自主播的數據被推流到源站A,那麼我們需要保證服務器A的地址不會改變。

除此之外,如果想要構建一套完善的流媒體系統,則需要包含靜態拉流與靜態推流。假如有觀衆向服務器C請求播放,那麼服務器C就會向服務器A拉流,無論服務器A是否存在視頻流,服務器C都會拉取。因此該模型只適用於較爲簡單的業務場景。

3. 動態控制:動態回源、動態轉推、鑑權

相對於靜態推拉流的“無腦”推拉流,更適用於多數人需求的則是動態推拉流。

Nginx的RTMP服務針對每一項功能都做了不同的觸發階段。以oclp_play爲例,當有人啓動播放時會觸發play消息,play消息會攜帶一項start參數。

在播放過程中,play消息依舊會被觸發,只不過此時還會攜帶update參數。在play結束時也會觸發一個play消息,所攜帶的參數是down。藉助這些參數,我們可以實現向業務服務器通知請求播放以及播放的具體階段。

3.1 動態回源

推流過程也存在類似操作,推流中存在publish,同樣分爲三個階段,play和publish主要應用在鑑權操作中。如果在start階段,業務服務器返回了一個404或者非200的結果,服務器就會中斷當前的play請求,publish亦是如此。除此之外, pull與push主要應用於動態拉流階段。當服務器接收到play請求,並且發現當前服務器裏面沒有目標流,也就是說publish的流不存在,就會觸發pull的start階段。在發送start請求之後如果業務服務器返回結果爲302,並且在location中又寫了一個新的rtmp地址或http-flv地址,這臺服務器就會向標記的那一臺目標服務器拉取rtmp流或fIv流,這個過程就被稱爲動態拉流。

3.2 動態轉推

與動態拉流相對應的是動態推流,其理解方式與動態拉流大致相同。如果你向服務器推流,服務器會向配置好的目標地址發送start請求。如果在返回結果當中加入一個新的rtmp地址,這一臺媒體服務器就會向新的rtmp地址推流,這也就是動態推流的操作。這一切的前提是返回302的結果,如果不想將流推出,那麼反饋給服務器400或其他非200,該流就會被中斷。

Oclp_stream用的比較少,僅僅在這路流創建與消失時被觸發。不管是play還是publish,如果只有play或publish存在,都會認爲這路流的生命週期還沒有結束,只有當二者全部消失時纔會被認定該路流生命週期已結束。

同樣的,如果一路流沒有被髮布過而是僅僅第一次有人請求,此時也會觸發start並認爲是該路流被創建,只不過沒有生產者而已。這種場景的應用比較少,只有對業務要求比較高的系統可能會用到這一條消息。

上圖展示了一個配置事例,主要包括查詢服務器的IP、查詢服務器play操作希望支持哪些階段等。

集羣化部署依賴業務(調度)服務器,如果有回源需求則讓邊緣服務器B在oclp_hold階段向業務服務器查詢,此時業務服務器會告訴邊緣服務器B一個302地址,其中包含源地址。邊緣服務器B就會從標記出來的這一臺(媒體服務器A)拉流,從而實現動態回源。

動態轉推主要是爲了把本地的流推出去。在CDN的服務中,不同集羣負責不同的職能。例如有些集羣負責錄製,有些則僅負責轉碼,此時我們希望核心機器能夠把這些需要轉碼或需要錄製的流按照需求轉接到相應集羣。動態轉推非常重要,如果業務中包含這些不同的類型,就需要添加配置oclp_push去實現動態轉推。

3.3 鑑權

鑑權操作中,我們只會對publish或play進行鑑權。

如果play的時候反饋200就是允許播放,如果反饋403就是不允許播放,publish也是如此,通過業務服務器控制客戶某一次服務請求是否能被允許。

前端進行play或者是進行publish時,如何把鑑權的token帶過來?

主要通過變量:args=k=v&pargs=$pargs在向外發送play查詢時,如果加入args=k=v&pargs=$pargs ,發請求時會帶上這些參數,這樣就可以將rtmp的全部自定義參數傳遞過來。

4. 多進程:進程間回源

多進程問題在原生的nginx rtmp中有很多bug,現在的做法是通過共享內存記錄下每個進程上的stream列表。如果play的進程沒有流,則查詢stream列表,並通過unix socket向目標進程回源拉流。除此之外,進程間的回源不會觸發ocl_playoclp_publish oclp_pull消息。

5. 更多操作說明

PingOS:https://github.com/im-pingo/pingos

 

 

【線上分享預告】大規模互動直播與在線視頻用戶體驗優化實踐

伴隨疫情突發,在線教育、視頻會議、互動直播與在線視頻需求被推上風口,如何在大規模、高併發視頻需求下爲用戶提供更流暢、更高清、低延遲的直播與視頻觀看體驗?如何爲線上教育賦予電子白板功能優化用戶學習體驗與教學效果?如何激發用戶在線視頻觀看興趣、提升用戶視頻交互體驗?

3月29日19:00,LiveVideoStack攜手UCloud RTC首席架構師王立飛、學而思網校資深架構師趙文傑、嗶哩嗶哩資深研發工程師唐君行,共同探索大規模互動直播與在線視頻背後的底層技術架構,分享用戶體驗優化實踐經驗。

  • 《URTC萬人連麥直播的優化實踐》 王立飛 UCloud RTC首席架構師
  • 《線上/線下教育中白板技術優化》 趙文傑 學而思網校資深架構師
  • 《高能進度條:視頻交互體驗優化》 唐君行 嗶哩嗶哩資深研發工程師

 

公開課預約入口:

大規模互動直播與在線視頻用戶體驗優化實踐-LiveVideoStack&UCan公開課第21期​mudu.tv

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章