項目地址: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:admin@192.168.1.11:554/cam/realmonitor?channel=1&subtype=0"
}
在有多個客戶端需要播放相同的RTSP流地址時,需要保證返回對應的WebSocket地址相同,這里使用了UUID v3來將RTSP地址散列化保證返回的地址相同。
processCh := uuid.NewV3(uuid.NamespaceURL, splitList[1]).String()
playURL := fmt.Sprintf("/stream/live/%s", processCh)
FFMPEG轉換的視頻數據也會通過HTTP協議傳回服務端,每幀byte數據會以'\n'
結束,在go語言中可以通過bufio
模塊來讀出這樣的數據。
bodyReader := bufio.NewReader(c.Request.Body)
for {
data, err := bodyReader.ReadBytes('\n')
if err != nil {
break
}
}
FFMPEG 視頻轉換
視頻轉換模塊會在收到需要轉換的RTSP流地址后,啟動一個FFMPEG子進程來轉換RTSP視頻流,這里是使用exec.Command
來完成:
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可以很方便的實現這個功能。
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:password@192.168.3.10: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:password@192.168.3.10: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核心知識掌握好了便可很好的應對這些問題。
首發自個人博客 某中二的黑科技研究中心 ,歡迎訪問交流。