Windows遠程桌面實現之十一:桌面屏幕通過各種直播服務端直播(RTSP, RTMP, HTTP-FLV, HLS)

                                                    by fanxiushu 2020-01-23 轉載或引用請註明原始作者。
此文還是基於xdisp_virt遠程項目中的一個子功能。在把xdisp_virt移植到各種平臺之後,就想着再做點什麼新功能,
於是乾脆再次增強原先實現的直播推流功能,在xdisp_virt程序中集成直播服務端,
這樣可以不用推流到第三方平臺,直接使用播放器就能播放。
(xdisp_virt越玩功能越多,現在基本江郎才盡都不知道還能再添加些什麼功能)

本文闡述的就是基於直播服務端比較常用的直播協議:
1,RTSP  經常用於監控行業 ,可以說是監控行業裏的老大。
2,RTMP 基於Adobe Flash的協議,雖然Flash面臨淘汰,但此協議還在大量使用
3,HTTP-FLV,與2類同,不過使用的是HTTP傳輸直播流,這樣更加適合目前互聯網的使用
4,HLS/DASH,HLS和DASH是同樣的東西,都是把直播流切成小片組成很多的小音視頻文件,
      然後客戶端通過HTTP下載這些小文件然後播放。嚴格得說這算不上直播協議,頂多就是個點播,而且延時非常的高。

以上是查詢到的目前經常使用的直播協議,於是打算把這四種協議全部集成到xdisp_virt程序中,
同樣的,本文包含的內容較多,也不能面面俱到,如果對xdisp_virt沒興趣,可只關注協議內容即可。
本文是下面鏈接的擴展篇,並且本文中RTMP和HTTP-FLV闡述的格式,多少會牽涉到其中一些內容。
https://blog.csdn.net/fanxiushu/article/details/80996391 (Windows遠程桌面實現之五(FFMPEG實現桌面屏幕RTSP,RTMP推流及本地保存)

(一)首先,我們來看看RTSP。
RTSP和HTTP協議和類似,但是也有用不同。與HTTP協議格式相同的是請求命令,
比如 OPTIONS,DESCRIBE,SETUP,PLAY,PAUSE,TEARDOWN這些命令與HTTP協議的GET,POST命令的協議格式幾乎是一樣的。
RTSP與HTTP不同的是流傳輸部分, RTSP傳輸音視頻流使用的是RTP協議,控制流採用的是RTCP協議,
這兩個協議底層即可以使用UDP傳輸,也可以使用TCP傳輸,這篇文章不介紹UDP傳輸流的RTP和RTCP,只介紹TCP方式傳輸流。
同時xdisp_virt也只實現TCP傳輸流。
UDP傳輸音視頻流看起來比較快,實際上面臨許多問題,最嚴重的就是丟包問題,也許有線網絡丟包情況稍微好些,
但是WIFI無線丟包情況就挺糟糕了,因此傳輸高質量的視頻幀的時候如果丟包多,顯示的畫面經常就會亂七八糟的。
RTSP在TCP中傳輸音視頻幀,使用的是傳輸OPTIONS,DESCRIBE,SETUP,PLAY這些命令的同一個TCP鏈接,
不會另外建立鏈接來傳輸音視頻流。這樣的設計其實挺好,節省了建立另外的TCP鏈接避免浪費資源。
但是會在同一個TCP鏈接中,同時傳輸 SETUP,PLAY這些類似HTTP格式的命令,還得傳輸RTP和RTCP攜帶音視頻流的數據。
同一個鏈接中傳輸這麼多不同類的數據,處理起來會麻煩一些,但是其實理解了RTSP協議格式種類,也並不難。
就比如xdisp_virt項目中,在同一個TCP鏈接中,傳輸的數據種類更多。
總結起來,一個TCP鏈接中傳輸三種數據,一是RTSP命令比如PLAY,SETUP等,一個是RTP數據,一個是RTCP數據。
RTP(RTCP)數據包: RTP頭(RTCP頭)+負載數據。
RTP頭和RTCP頭定義如下:
(網路序)
struct rtp_header_t
{
    uint32_t ver : 2;        // protocol version
    uint32_t pad : 1;        // padding flag
    uint32_t x : 1;        // header extension flag
    uint32_t cc : 4;        // CSRC count
    uint32_t m : 1;        // marker bit
    uint32_t pt : 7;        // payload type  
    uint16_t seq ;    // sequence number
    uint32_t timestamp; // timestamp
    uint32_t ssrc;        // synchronization source
} ;
struct rtcp_header_t
{
    uint32_t ver : 2;        // version
    uint32_t pad : 1;        // padding
    uint32_t rc : 5;        // reception report count
    uint8_t  pkt_type;  ///packet type
    uint16_t length ; // pkt len in words, w/o this word
} ;
在UDP傳輸RTP和RTCP的時候,都是分開兩個UDP通道(不同的UDP端口)分別傳輸RTP和RTCP數據包,RTSP命令則使用TCP傳輸。
因此也非常容易區分,當然浪費的網絡資源也很多。
從以上RTP和RTCP頭的定義看得出來,如果把三種數據在同一個TCP中傳輸,是無法區分的,因此必須在RTP頭和RTCP頭前面再加一個頭
這個頭叫interleaved,定義如下:
struct rtsp_interleaved_t
{
    unsigned char  magic; // 固定爲0x24,'$' 金錢符
    unsigned char  channel; //通道類型,就是指示是傳輸的RTP包,還是RTCP包
    unsigned short length;  //後面的RTP包或RTCP包的大小,不超過64K
};
這樣三種包在同一個TCP中的格式就是:
A), 類似HTTP格式命令
B), rtsp_interleaved_t + rtp_header_t + 音視頻負載數據
C), rtsp_interleaved_t + rtcp_header_t + RTCP負載數據
其中 RTCP和RTP可以通過 rtsp_interleaved_t中的channel區分,但是rtsp_interleaved_t頭和普通的RTSP請求命令如何區分呢?
其實也很好區分,我們編程的時候,先接收一個字節,
如果這個字節是 ‘$'(0x24)可以確定是rtsp_interleaved_t頭,否則就是普通的OPTIONS,DESCRIBE,PLAY這些命令。

我們來看看一個RTSP播放器是如何請求播放流的,
首先播放器會發送OPTIONS命令,服務端返回 OPTIONS,PLAY,PAUSE,TEARDOWN這些命令表示服務端支持哪些RTSP命令。
接着播放器發送DESCRIBE命令請求音視頻流的SDP描述信息,播放器獲取到SDP之後,就知道了服務端提供了哪些流。
然後播放器發送SETUP給服務端,表示準備開始傳流,其中包括採用哪種傳輸方式,傳輸通道等信息,服務端回覆SETUP之後。
然後播放器發送PLAY命令開始請求播放, 這個時候,服務端就開始推送包含音視頻數據的RTP包給播放器。
播放器停止播放的時候,會發送TEARDOWN命令,這個時候,服務端停止推送流。
這就是一個RTSP播放的基本流程。
其實SETUP和PLAY命令是播放器必須發送的,其他的可以可選。

下面是一個請求例子(C是播放器, S是服務端):
C:
       OPTIONS rtsp://192.168.88.1/test RTSP/1.0\r\n
       CSeq: 1\r\n
       \r\n
S:   RTSP/1.0 200 OK\r\n
       CSeq: 1\r\n
       Server: xdisp_virt\r\n
       Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE\r\n
      \r\n
C:
      DESCRIBE rtsp://192.168.88.1/test RTSP/1.0\r\n
     CSeq: 2\r\n
      \r\n
S:  RTSP/1.0 200 OK\r\n
      CSeq: 2\r\n
      Server:  xdisp_virt\r\n
      Session: 1234\r\n
      Context-Length: 345\r\n
      \r\n
      SDP 內容
C: SETUP rtsp://192.168.88.1/test/track1 RTSP/1.0\r\n   請求視頻幀
     CSeq: 3\r\n
     Transport: Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n
     Session: 1234\r\n
     \r\n
S: RTSP/1.0 200 OK\r\n
     CSeq: 3\r\n
     Session: 1234\r\n
     Transport: Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n
     \r\n
C:
    PLAY rtsp://192.168.88.1/test RTSP/1.0\r\n
    CSeq: 4\r\n
    Session: 1234\r\n
    \r\n
S: RTSP/1.0 200 OK\r\n
    CSeq: 4\r\n
    Session: 1234\r\n
    \r\n
服務端開始推送攜帶視頻數據的RTP包(rtsp_interleaved_t+rtp_header_t+視頻數據)
服務端還會接收到播放器發送來的RTCP報告數據包。服務端根據情況也會發生狀態報告RTCP包給播放器。
。。。。
C:
    TEARDOWN rtsp://192.168.88.1/test RTSP/1.0\r\n
    CSeq: 5\r\n
    Session: 1234\r\n
    \r\n
S: RTSP/1.0 200 OK\r\n
     \r\n
服務端停止推送視頻流。

接下來就是如何把H264視頻幀封包到RTP包中,
一幀H264數據包含許多NALU單元,通常就是把每個NALU單元封包成一個RTP包即可,
但是這個RTP包是有大小限制的,因爲如上面rtsp_interleaved_t數據結構裏邊的length不超過64K,RTP包大小不能超過64K,
有可能一個NALU單元很大,會超過64K大小,因此需要把這個NALU單元拆分成多個部分,每個部分再封裝成RTP包。
至於如何拆分和如何再封包成RTP包,這裏也就不再贅述,有興趣可查閱RFC文檔的相關介紹。

(二),HTTP-FLV 和 RTMP 直播協議。
之所以要把這兩合併到一起來講,其實是音視頻包封包格式都差不多,基本都是基於FLV文件格式。不同的是 採用的通訊協議不同。
RTMP不但支持拉流,更主要的是支持推流。在互聯網應用中,採集到的直播流數據可以通過RTMP協議推送到服務器端,
服務器端再拆分成多個拉流協議,比如 HTTP-FLV, HLS,DASH等各種拉流協議。
在互聯網應用中,HTTP協議是主流,因此我們對拉流的使用,使用的HTTP-FLV較多,RTMP相對會少一些,尤其在Flash面臨淘汰的現在。
HTTP-FLV和RTMP實時性較高,延時一般都在1-2秒內,因此適合用於對應實時性要求高的互動直播環境中。
RTMP在瀏覽器中需要藉助Flash才能播放,PC系統瀏覽器一般都支持Flash插件,但是基本也是日落西山。
除非是自己在客戶端開發播放器專門使用RTMP來播放, 比如VLC播放器可運行在移動系統和PC系統中,且支持多個串流播放協議。
至於HTTP-FLV,可以在瀏覽器端利用js解碼,轉成MSE餵給video標籤來播放,但是不是所有系統都支持MSE,這給兼容性帶來麻煩。
至少目前iOS系統都不支持MSE,也許以後的iOS版本能支持MSE。
目前來看,兼容性最好的還是HLS,能支持所有平臺的絕大部分瀏覽器,但是就是延遲實在太高。

HTTP-FLV是利用HTTP鏈接,發送一個無限大的flv文件來達到直播的效果。
FLV文件是一個 flv頭 + 多個tag組成的序列流, tag包括音頻tag, 視頻tag,script的tag(比如媒體信息tag)。
FLV不像MP4文件格式,需要文件尾和頭才能組成一個完整的MP4文件,
FLV文件只要 flv_header + meta_header(描述音視頻信息)+ 無數個音視頻數據的tag,
只要前面的flv_header+meta_header信息完整,後面的音視頻tag可以無限個,也就是不需要文件尾部,這是實現HTTP-FLV直播的基本條件。
同時在 HTTP協議定義中,在回覆中如果沒有包含 Content-Length 這個字段,
客戶端就會一直接收數據,直到服務端主動關閉這個HTTP鏈接,這樣服務端就會連續不斷的推送音視頻tag 給客戶端。

比如一個HTTP-FLV請求和回覆流程:
C:
    GET /flv_stream/test.flv HTTP/1.1\r\n
    Host: ddd.com\r\n
    \r\n
S:
    HTTP/1.1 200 OK\r\n
    Server: xdisp_virt\r\n
    Connection: keep-alive\r\n
    \r\n
    FLV頭 + 媒體信息tag
    持續不斷的 音頻tag 和 視頻tag
    。。。。
    直到C或者S關閉這個鏈接爲止。

FLV頭一共9個字節,前3個是’FLV‘,第4個字節是版本號,第5個是支持audio+video類型,接下來4個字節是頭長度,這裏爲9,
再接下來4個字節全是0, 這個意思是PreviousTagSize0,就是第一個前Tag的大小。
接下來就是以TAG爲單位的組合:
11個字節的TAG頭 + 負載數據內容 + 4個字節的這段數據大小(11 + 負載數據大小)

TAG頭包括TAG類型(比如 audio,video, script等),負載數據大小,timestamp,流ID。
第一個TAG是 meta類型的TAG, 用來描述接下來的音視頻流的相關信息,比如SPS,PPS信息,視頻寬高,音頻採集率等。
這個是必須有的,否則客戶端無法獲取到音視頻相關信息。
接下來視頻幀,除了11個字節的TAG頭之外,還包含5個字節描述video相關信息,比如是否關鍵幀,
接下來就是一幀的H264數據,採用AVCC編碼,最後就是4個字節用來指示當前tag的大小。
音頻幀,除了11個字節的TAG,還包含2個字節的描述Audio信息,接下來就是一幀Audio數據,最後4個字節就是當前tag的大小。

(三),HLS,DASH切片直播。
嚴格得說,這不是個直播協議,它是把連續不斷的直播流,切片成很多的小文件。
客戶端先從服務端獲取到一個列表,然後根據列表分別下載這些小文件,然後再連續不斷的播放這些小文件。
直播流是連續不斷的,小文件會越生成越多,服務端因此會刪除先前的小文件,比如同時只保留20個小文件,
文件按照序號不斷的生成和不斷的刪除不用的小文件,總數保持20個不變。
同時這個列表上的小文件名字也會不斷更新,客戶端會定時重新獲取這個列表,從而按照順序不斷的下載新的小文件,然後再播放。
這樣的效果就造成好像在不斷的播放一樣,其實就是在連續不斷的下載和播放小視頻文件而已。

爲何會設計出這麼一個不倫不類的直播協議,(不倫不類這麼說可能有點過分)
其實我想跟目前瀏覽器video標籤播放視頻的方式有關係,
目前瀏覽器的內嵌的 video標籤強項是播放單個視頻文件,而不是播放直播流。
這也是直播協議在瀏覽器中面臨的尷尬局面。

以前在研究如何直接在瀏覽器中實現遠程控制的時候,也是面臨同樣的問題。
在不需要給瀏覽器安裝插件的情況下,而且要兼容各個系統的瀏覽器,
當時也是折騰來折騰去,最終選擇了使用WebSocket傳輸音視頻數據,然後js解碼,然後使用canvas畫圖來解決。
目前來看,這也是目前實現瀏覽器方式遠程控制的最好方式,實時性也是非常高,符合遠程控制對實時性的要求。
就是這個時候瀏覽器佔用的系統資源比較高些,但是基本也能給接受。

下圖是最新版本的xdisp_virt實現的四種直播服務端:


下圖是在iPhone手機瀏覽器中直接打開上面第4個切片直播的URL地址的播放效果圖:

發佈了76 篇原創文章 · 獲贊 101 · 訪問量 35萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章