國內應用比較多的開源流媒體服務器nginx-rtmp-module一直存在功能少、集羣化難度大等問題。在LiveVideoStack線上分享中,PingOS 開源項目組開發工程師、UCloud RTC研發工程師朱建平詳細介紹了基於nginx-rtmp-module的PingOS流媒體服務器在http-flv、http-ts、hls+、多進程、轉推、回源以及集羣化部署方面的技術實現細節。前端
文 / 朱建平nginx
整理 / LiveVideoStackgit
直播回放github
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的播放與拉流。session
一樣,咱們能夠根據上述思路繼續擴展協議。假如咱們在收到一個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