1. 程式人生 > >使用Gin+WebSocket在HTML中無外掛播放RTSP

使用Gin+WebSocket在HTML中無外掛播放RTSP

專案地址:gin-rtsp

在後臺的開發中遇到了對接顯示攝像頭視訊流的需求。目前獲取海康及大華等主流的攝像頭的視訊流使用的基本都是RTSP協議。不過HTML頁面並不能直接播放RTSP協議的視訊流,查詢了一番各種網頁播放RTSP的資料,有如下的一些方案:

  • 外掛開發播放:使用ActiveX等瀏覽器外掛的方式來播放,海康和大華的瀏覽器管理頁面便是通過安裝瀏覽器外掛來播放視訊的。視訊播放穩定,延時短,但是對技術要求較高,對於chrome等現代瀏覽器也存在相容性問題,並不想考慮。

  • RTSP 轉 HLS:使用FFMPEG將RTSP轉為HLS,推流到流伺服器,如安裝了nginx-rtmp-module模組的nginx,用這個方案測試了下,HLS協議在PC端和移動端的瀏覽器的播放都很穩,但是用HLS協議的直播流延時很大,至少有15秒左右,對於低延時視訊的需求只能PASS。

  • RTSP 轉 RTMP:與上一方案類似,使用FFMPEG將RTSP轉為RTMP推到流伺服器分發播放,相比HLS延時很低,本來已經準備使用這個方案了,但是前端使用的video.js庫總是會偶現無法載入視訊的問題,而且播放RTMP需要使用到Flash,在chrome等瀏覽器中已經預設禁止載入逐步淘汰,只能拋棄。

  • WebSocket:最終在萬能的Github上翻到了一個JSMpeg專案,採用FFMPEG轉為MPEG1 Video通過WebSocket代理推送到前端直接解碼播放的方案。測試了下,延遲低,無需外掛,畫面質量也可以根據需要調整,效果很不錯。

JSMpeg專案示例的WebSocket代理使用的是JS,簡單實現了單個視訊源的播放功能。我們的後臺使用的是golang的Gin框架,會有多個網頁客戶端播放多個視訊流。好在看了下JS的程式碼,這個WebSocket代理的原理並不難,在Gin中整合WebSocket也很方便。這裡記錄下我的整合方案。

主要模組

  • API 介面:接收FFMPEG的推流資料和客戶端的HTTP請求,將客戶端需要播放的RTSP地址轉換為一個對應的WebSocket地址,客戶端通過這個WebSocket地址便可以直接播放視訊,為了及時釋放不再觀看的視訊流,這裡設計為客戶端播放時需要在每隔60秒的時間裡迴圈請求這個介面,超過指定時間沒有收到請求的話後臺便會關閉這個視訊流。

  • FFMPEG 視訊轉換:收到前端的請求後,啟動一個Goroutine呼叫系統的FFMPEG命令轉換指定的RTSP視訊流並推送到後臺對應的介面,自動結束已超時轉換任務。

  • WebSocket Manager:管理WebSocket客戶端,將請求同一WebSocket地址的客戶端新增到一個Group中,向各個Group廣播對應的RTSP視訊流,刪除Group中已斷開連線的客戶端,釋放空閒的Group。

這裡大致介紹下這三個主要模組的實現要點。

API 介面

API接收客戶端傳送的包含了需要播放RTSP流地址的Json資料,格式如:

{
    "url":"rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=0"
}

在有多個客戶端需要播放相同的RTSP流地址時,需要保證返回對應的WebSocket地址相同,這裡使用了UUID v3來將RTSP地址雜湊化保證返回的地址相同。

service/rtsptrans.go

processCh := uuid.NewV3(uuid.NamespaceURL, splitList[1]).String()
playURL := fmt.Sprintf("/stream/live/%s", processCh)

FFMPEG轉換的視訊資料也會通過HTTP協議傳回服務端,每幀byte資料會以'\n'結束,在go語言中可以通過bufio模組來讀出這樣的資料。

api/rtsp.go

bodyReader := bufio.NewReader(c.Request.Body)

for {
    data, err := bodyReader.ReadBytes('\n')
    if err != nil {
        break
    }
}

FFMPEG 視訊轉換

視訊轉換模組會在收到需要轉換的RTSP流地址後,啟動一個FFMPEG子程序來轉換RTSP視訊流,這裡是使用exec.Command來完成:

service/rtsptrans.go

params := []string{
    "-rtsp_transport",
    "tcp",
    "-re",
    "-i",
    rtsp,
    "-q",
    "5",
    "-f",
    "mpegts",
    "-fflags",
    "nobuffer",
    "-c:v",
    "mpeg1video",
    "-an",
    "-s",
    "960x540",
    fmt.Sprintf("http://127.0.0.1:3000/stream/upload/%s", playCh),
}

cmd := exec.Command("ffmpeg", params...)
cmd.Stdout = nil
cmd.Stderr = nil
stdin, err := cmd.StdinPipe()

通過FFMPEG的 -q 和 -s 引數可以除錯視訊的質量和解析度。為了簡便,命令的stdout和stderr都賦值為了nil,實際專案中可以儲存到日誌中方便排查問題。為了及時釋放不再播放的資源,客戶端停止請求超過一定時間後,FFMPEG子程序會自動關閉,通過golang的select可以很方便的實現這個功能。

service/rtsptrans.go

for {
    select {
    case <-*ch:
        util.Log().Info("reflush channel %s rtsp %v", playCh, rtsp)

    case <-time.After(60 * time.Second):
        stdin.Write([]byte("q"))
        err = cmd.Wait()
        if err != nil {
            util.Log().Error("Run ffmpeg err:%v", err.Error)
        }
        return
    }
}

這裡的*ch channel通過一個map和每個子程序關聯,子程序關閉時需要從map中清除,需要考慮併發的問題,可以使用sync.Map來保證執行緒安全。

WebSocket Manager

WebSocket Manager 負責對頁面上請求視訊資料的 ws 客戶端進行管理,在Gin中,主要是使用github.com/gorilla/websocket這個庫來開發相關功能。JSMpeg庫連線WebSocket時使用到了Sec-WebSocket-Protocol這個header,需要對其處理:

upgrader := websocket.Upgrader{
    // cross origin domain
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
    // 處理 Sec-WebSocket-Protocol Header
    Subprotocols: []string{ctx.GetHeader("Sec-WebSocket-Protocol")},
}
conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil)

ws 客戶端連線後,會分配一個唯一的UUID,放入到URL對應的Group中,相同Group下的客戶端會收到同一視訊流的資料。客戶端斷開連線後,需要從Group中刪除,同時釋放掉已經為空的Group。這個過程同樣需要考慮到併發的問題,WebSocket Manager通過單獨啟動一個Goroutine監聽註冊,斷開連線,廣播的三個對應的golang的channel,來統一管理各個Group,可以很好的解決這個問題。具體實現在 service/wsservice.go#L75,程式碼比較長就不貼了。

測試

專案需要執行在安裝有FFMPEG程式的環境中。通過編寫了一份Dockerfile已經封裝好了需要的環境,可以使用Docker build後,以Docker的方式執行。

$ docker build -t ginrtsp .
$ docker run -td -p 3000:3000 ginrtsp

使用內建的FFMPEG轉換

將需要播放的RTSP流地址提交到 /stream/play 介面,例如:

POST /stream/play
{
   "url": "rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=0"
}

後臺可以正常轉換此RTSP地址時便會返回一個對應的地址,例如:

{
    "code": 0,
    "data": {
        "path": "/stream/live/5b96bff4-bdb2-3edb-9d6e-f96eda03da56"
    },
    "msg": "success"
}

編輯html資料夾下view-stream.html檔案,將script部分的url修改為此地址,在瀏覽器中開啟,便可以看到視訊了。

手動執行FFMPEG

由於後臺轉換RTSP的程序在超過60秒沒有請求後便會停止,也可以通過手動執行ffmpeg命令,來更方便地在測試狀態下檢視視訊。

ffmpeg -rtsp_transport tcp -re -i 'rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=0' -q 0 -f mpegts -c:v mpeg1video -an -s 960x540 http://127.0.0.1:3000/stream/upload/test

通過如上命令,執行之後在view-stream.html檔案的url中填入對應的地址為/stream/upload/test,在瀏覽器中開啟檢視視訊。

顯示效果

總結

得益於JSMpeg專案的強大,實現一個WebSocket的在網頁上播放RTSP視訊流還是很簡單的了。隨著golang語言日漸成熟,基於現成的庫也可以方便的在Gin中新增WebSocket功能。需要注意主要是併發時,對FFMPEG子程序,WebSocket客戶端的增刪問題,好在golang天生對併發有良好的支援,gouroutine,channel,sync庫這些golang核心知識掌握好了便可很好的應對這些問題。

首發自個人部落格 某中二的黑科技研究中心 ,歡迎訪問交流。