RTSP?不存在的 -> 前端實時流探索記

做爲一個從未接觸過實時流(直播流)的人,我以前對實時視頻一直沒有概念,而最近參與的項目恰好有視頻監控的需求,在參與技術選型以前,我對前端實時流的展現進行了一下摸底。

javascript

概覽

視頻有一個流的概念,因此稱流媒體。實時視頻的流很好理解,由於視頻是實時的,須要有一個地方不停地輸出視頻出來,因此整個視頻能夠用流來稱呼。那麼視頻能否直接輸出到前端頁面上呢?惋惜,若是能夠的話,就沒有我這篇文章了。如今攝像頭的實時視頻流廣泛採用的是 RTSP 協議,而前端並不能直接播放 RTSP 的視頻流。css

  • RTSP(Real-Time Stream Protocol),是 TCP/UDP 協議體系中的一個應用層協議,跟 HTTP 處在同一層。RTSP 在體系結構上位於 RTP 和RTCP 之上,它使用 TCP 或者 RTP 完成數據傳輸。RTSP 實時效果很是好,適合視頻聊天、視頻監控等方向。


那麼咱們就須要一層中間層,來將 RTSP 流轉成前端能夠支持的協議,這也引伸出了目前實時流技術的幾種方向:html

  • RTSP -> RTMP
  • RTSP -> HLS
  • RTSP -> RTMP -> HTTP-FLV

image.png

RTMP

RTMP(Real Time Messaging Protocol)是屬於 Adobe 的一套視頻協議,這套方案須要專門的 RTMP 流媒體,而且若是想要在瀏覽器上播放,沒法使用 HTML5 的 video 標籤,只能使用 Flash 播放器。(經過使用 video.js@5.x 如下的版本能夠作到用 video 標籤進行播放,但仍然須要加載 Flash)。它的實時性在幾種方案中是最好的,可是因爲只能使用 Flash 的方案,因此在移動端就直接 GG 了,在 PC 端也是明日黃花
因爲下面的兩種方法也須要用到 RTMP,因此這裏就展現一下 RTSP 流如何轉換成 RTMP ,咱們使用 ffmpeg+Nginx+nginx-rtmp-module 來作這件事:前端

# 在 http 同一層配置 rtmp 協議的相關字段
rtmp {
    server {
          # 端口
        listen 1935;
            # 路徑
        application test {
            # 開啓實時流模式
            live on;
            record off;
        }
    }
}
# bash 上執行 ffmpeg 把 rtsp 轉成 rtmp,並推到 1935 這個端口上
ffmpeg -i "rtsp://xxx.xxx.xxx:xxx/1" -vcodec copy -acodec copy -f flv "rtmp://127.0.0.1:1935/live/"

這樣咱們就獲得了一個 RTMP 的流,咱們能夠直接用 VLC 或者 IINA 來播放這個流。
java

HLS

HLS(HTTP Live Streaming)是蘋果公司提出的基於 HTTP 協議的的流媒體網絡傳輸協議,它的工做原理是把整個流分紅一個個小的基於 HTTP 的文件來下載,每次只下載一些。HLS 具備跨平臺性,支持 iOS/Android/瀏覽器,通用性強。可是它的實時性差:蘋果官方建議是請求到3個片以後纔開始播放。因此通常不多用 HLS 作爲互聯網直播的傳輸協議。假設列表裏面的包含5個 ts 文件,每一個 TS 文件包含5秒的視頻內容,那麼總體的延遲就是25秒。蘋果官方推薦的小文件時長是 10s,因此這樣就會有30s(n x 10)的延遲。
下面是 HLS 實時流的整個鏈路:
image.png
從圖中能夠看出來咱們須要一個服務端做爲編碼器和流分割器,接受流並不斷輸出成流片斷(stream),而後前端再經過一個索引文件,去訪問這些流片斷。那麼咱們一樣可使用 nginx+ffmpeg 來作這件事情。nginx

# 在 rtmp 的 server 下開啓 hls
# 做爲上圖中的 Server,負責流的處理
application hls{
        live on;
        hls on;
         hls_path xxx/; #保存 hls 文件的文件夾
        hls_fragment 10s;
}
# 在 http 的 server 中添加 HLS 的配置:
# 做爲上圖中的 Distribution,負責分片文件和索引文件的輸出
location /hls {
      # 提供 HLS 片斷,聲明類型
      types {
        application/vnd.apple.mpegurl m3u8;
        video/mp2t ts;
      }
      root /Users/mark/Desktop/hls; #訪問切片文件保存的文件夾
      # Cache-Controll no-cache;
      expires -1;
}

而後一樣使用 ffmpeg 推流到 hls 路徑上:git

ffmpeg -i "rtsp://xxx.xxx.xxx:xxx/1" -vcodec copy -acodec copy -f flv rtmp://127.0.0.1:1935/hls

這個時候能夠看到文件夾裏已經有許多流文件存在,且不停地更新:
image.png
而後咱們可使用 video.js+video.js-contrib-hls 來播放這個視頻:github

<html>
<head>
<title>video</title>
<!-- 引入css -->
<link href="https://unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet">


</head>
<body>
<div class="videoBox">
    <video id="video" class="video-js vjs-default-skin" controls>
        <source src="http://localhost:8080/hls/test.m3u8" type="application/x-mpegURL"> 
    </video>
</div>

</body>
</html>
<script src="https://unpkg.com/video.js/dist/video.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/videojs-contrib-hls/5.15.0/videojs-contrib-hls.min.js"></script>
<script>
videojs.options.flash.swf = "./videojs/video-js.swf"
        videojs('video', {"autoplay":true}).play();
</script>

在個人測試下,HLS 的延遲在10-20秒左右,咱們能夠經過調整切片的大小來減小延遲,可是因爲架構的限制,延遲是一個不可忽視的問題。
ajax

HTTP-FLV

接下來就是重頭戲 HTTP-FLV 了,它集合了 HLS 的通用性和 RTMP 的實時性,能夠作到在瀏覽器上用 HTML5 的 video 標籤,以較低的延時播放實時流。HTTP-FLV 依靠 MIME 的特性,根據協議中的 Content-Type 來選擇相應的程序去處理相應的內容,使得流媒體能夠經過 HTTP 傳輸。除此以外,它能夠經過 HTTP 302 跳轉靈活調度/負載均衡,支持使用 HTTPS 加密傳輸,也可以兼容支持 Android,iOS 等移動端。HTTP-FLV 本質上是將流轉成 HTTP 協議下的 flv 文件,在 Nginx 上咱們可使用 nginx-http-flv-module 來將 RTMP 流轉成 HTTP 流。瀏覽器

其實 flv 格式依然是 Adobe 家的格式,原生 Video 標籤沒法直接播放,可是好在咱們有 bilibili 家的 flv.js,它能夠將 FLV 文件流轉碼複用成 ISO BMFF(MP4 碎片)片斷,而後經過 Media Source Extensions 將 MP4 片斷喂進瀏覽器。


在支持瀏覽器的協議裏,延遲排序是這樣的:RTMP = HTTP-FLV = WebSocket-FLV < HLS
而性能排序是這樣的:RTMP > HTTP-FLV = WebSocket-FLV > HLS


說了這麼多,不如直接上手看看吧:

  1. 首先咱們須要一個新的 nginx 插件:nginx-http-flv-module
  2. 在 nginx.conf 中進行一些新的配置:
# rtmp server
application myvideo {
      live on;
      gop_cache: on; #減小首屏等待時間
}

# http server
location /live {
      flv_live on;
}
  1. 依然用 ffmpeg 來推流,使用上面 RTMP 的命令
  2. 前端 import flv.js,而後使用它來播放
// 前端使用 flv.js,開啓實時模式,而後訪問這個 nginx 地址下的路徑便可
import flvJs from 'flv.js';

export function playVideo(elementId, src) {
  const videoElement = document.getElementById(elementId);
  const flvPlayer = flvJs.createPlayer({
    isLive: true,
    type: 'flv',
    url: src,
  });
  flvPlayer.attachMediaElement(videoElement);
  flvPlayer.load();
}

playVideo('#video', 'http://localhost:8080/live?port=1985&app=myvideo&stream=streamname')

能夠看到 flv.js 使用了 video/x-flv 這個 MIME 返回數據。
image.png
若是對延遲有更高的要求,能夠嘗試下面的操做:

  1. 能夠配置 flv.js 的 enableStashBuffer 字段,它是 flv.js 用於控制緩存 buffer 的開關,關閉了以後能夠作到最小延遲,但因爲沒有緩存,可能會看到網絡抖動帶來的視頻卡頓。
  2. 能夠嘗試關閉 nginx 的 http 配置裏的 gop_cache 。gop_cache 又稱關鍵幀緩存,其意義是控制視頻的關鍵幀之間的緩存是否開啓。

這裏引入了一個關鍵幀的概念:咱們使用最普遍的 H.264 視頻壓縮格式,它採用了諸如幀內預測壓縮/幀間預測壓縮等壓縮方案,最後獲得了 BPI 三種幀:

  • I 幀:關鍵幀,採用幀內壓縮技術。
  • P 幀:向前參考幀,在壓縮時,只參考前面已經處理的幀,表示的是當前幀畫面與前一幀(前一幀多是 I 幀也多是 P 幀)的差異。採用幀間壓縮技術。
  • B 幀:雙向參考幀,在壓縮時,它即參考前面的幀,又參考它後面的幀。B 幀記錄的是本幀與先後幀的差異。採用幀間壓縮技術。

帶有 I 幀、B 幀和 P 幀的典型視頻序列。P 幀只須要參考前面的 I 幀或 P 幀,而 B 幀則須要同時參考前面和後面的 I 幀或 P 幀。因爲 P/B 幀對於 I 幀都有直接或者間接的依賴關係,因此播放器要解碼一個視頻幀序列,並進行播放,必須首先解碼出 I 幀。假設 GOP(就是視頻流中兩個I幀的時間距離) 是 10 秒,也就是每隔 10 秒纔有關鍵幀,若是用戶在第 5 秒時開始播放,就沒法拿到當前的關鍵幀了。這個時候 gop_cache 就起做用了:gop_cache 能夠控制是否緩存最近的一個關鍵幀。開啓 gop_cache 可讓客戶端開始播放時,當即收到一個關鍵幀,顯示出畫面,固然,因爲增長了對上一個幀的緩存,因此延時天然就變大了。若是對延時有更高的要求,而對於首屏時間/播放流暢度的要求沒那麼高的話,那麼能夠嘗試關閉 gop_cache,來達到低延時的效果。

思考

延遲與卡頓

實時視頻的延時與卡頓是視頻質量中最重要的兩項指標。 然而,這兩項指標從理論上來講,是一對矛盾的關係——須要更低的延時,則代表服務器端和播放端的緩衝區都必須更短,來自網絡的異常抖動容易引發卡頓;業務能夠接受較高的延時時,服務端和播放端均可以有較長的緩衝區,以應對來自網絡的抖動,提供更流暢的體驗。

直播廠商是怎麼作的?

如今各個直播平臺基本上都放棄了以上這些比較傳統的方式,使用了雲服務商提供的 CDN,但仍是離不開前文所說的幾種協議與方式。以下圖是阿里雲的直播服務圖。能夠看到其流程大概分爲這幾步:

  1. 採集視頻流(主播端使用 RTMP 進行推流)
  2. 推流到 CDN 節點(上傳流)
  3. CDN 節點轉到直播中心,直播中心相似於強大的具備計算能力的中間源,能夠提供額外服務諸如落存(錄製/錄製到雲存儲/點播),轉碼,審覈,多種協議的輸出等。
  4. 直播中間分發到 CDN 節點
  5. 播放(阿里雲支持 RTMP、FLV 及 HLS 三種播流協議)

image.png

PS:若是你已經看到這兒了,以爲我寫得還行的話,麻煩給個贊,謝謝!🙏

相關文章
相關標籤/搜索