使用Gin+WebSocket在HTML中無插件播放RTSP

項目地址:gin-rtsphtml

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

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

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

  • RTSP 轉 RTMP:與上一方案相似,使用FFMPEG將RTSP轉爲RTMP推到流服務器分發播放,相比HLS延時很低,原本已經準備使用這個方案了,可是前端使用的video.js庫老是會偶現沒法加載視頻的問題,並且播放RTMP須要使用到Flash,在chrome等瀏覽器中已經默認禁止加載逐步淘汰,只能拋棄。github

  • WebSocket:最終在萬能的Github上翻到了一個JSMpeg項目,採用FFMPEG轉爲MPEG1 Video經過WebSocket代理推送到前端直接解碼播放的方案。測試了下,延遲低,無需插件,畫面質量也能夠根據須要調整,效果很不錯。golang

JSMpeg項目示例的WebSocket代理使用的是JS,簡單實現了單個視頻源的播放功能。咱們的後臺使用的是golang的Gin框架,會有多個網頁客戶端播放多個視頻流。好在看了下JS的代碼,這個WebSocket代理的原理並不難,在Gin中集成WebSocket也很方便。這裏記錄下個人集成方案。web

主要模塊

  • API 接口:接收FFMPEG的推流數據和客戶端的HTTP請求,將客戶端須要播放的RTSP地址轉換爲一個對應的WebSocket地址,客戶端經過這個WebSocket地址即可以直接播放視頻,爲了及時釋放再也不觀看的視頻流,這裏設計爲客戶端播放時須要在每隔60秒的時間裏循環請求這個接口,超過指定時間沒有收到請求的話後臺便會關閉這個視頻流。chrome

  • FFMPEG 視頻轉換:收到前端的請求後,啓動一個Goroutine調用系統的FFMPEG命令轉換指定的RTSP視頻流並推送到後臺對應的接口,自動結束已超時轉換任務。docker

  • WebSocket Manager:管理WebSocket客戶端,將請求同一WebSocket地址的客戶端添加到一個Group中,向各個Group廣播對應的RTSP視頻流,刪除Group中已斷開鏈接的客戶端,釋放空閒的Group。api

這裏大體介紹下這三個主要模塊的實現要點。

API 接口

API接收客戶端發送的包含了須要播放RTSP流地址的Json數據,格式如:

{
    "url":"rtsp://admin:admin@192.168.1.11: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: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核心知識掌握好了即可很好的應對這些問題。

首發自我的博客 某中二的黑科技研究中心 ,歡迎訪問交流。

相關文章
相關標籤/搜索