使用UDP完成720p以上高清視頻傳輸
1. 項目背景
視頻傳輸: 在一臺電腦上播放視頻(捕捉攝像頭畫面),同局域網內另一臺電腦上實時播放,儘量不卡頓。
先放最後的照片,和用gif展示一下視頻效果。
- 傳輸視頻可以採取
圖片
或者流
的形式,本文采取傳輸圖片的形式,在1s之內顯示多張圖片從而形成連續的視頻畫面。 - 經費有限,所有實驗均基於筆記本電腦。
- 使用的視頻源是本機攝像頭,以及進擊的巨人720p資源。
2. 解決方案
- 使用
Python
的Socket
,使用opencv
捕捉攝像頭/視頻的畫面。 - 原始的圖片很大(720p的大小是
1920*1080*3
),整圖就算壓縮成jpg
格式其大小也非常大。而UDP
最大隻能傳輸65535
字節大小的數據區,故對圖片進行分塊,分塊過後的數據壓縮成jpg
格式,並對圖片分塊數據進行編號。 - 實驗檢測表明,本文實驗環境發送端不需要使用發送隊列,基本上新生成的幀很快就能被socket傳輸掉。
- 接收端使用多線程接收,每個線程是一個socket,接收過後的數據存儲於
數據片池
。 - 接收端另開一個線程,用於反覆從
數據片池
讀取數據片,根據數據片的編號更新幕布
,這裏幕布
是專門用於圖像顯示的一個數組,其維度是720p(1920*1080*3
)。更新過後的結果暫存於圖片池
- 主線程反覆從
圖片池
讀取圖片,並顯示。
3. 實現細節
3.1 TCP/UDP的選擇
爲了實現低延遲,毫無疑問選取無連接的UDP
傳輸。
3.2 圖片分片算法
這裏其實也談不上什麼算法,就是將圖片水平分割。這種做法的好處在於,分割後圖片的編號可以和區域一一對應。
本文沒有探索更爲複雜的圖片分片算法。
經過處理,圖片變爲一個個分片,如下:
對上述圖片進行編號,很顯然可以編號0,1,2,3
,對於任意分塊(例如2)在圖像數組中對應的區域是frame[2*piece_size:(2+1)*piece_size]
,其中piece_size
表示一片數據的大小。
這種對應關係方便解壓後的圖像還原操作。
3.3 JPG壓縮
這其實是個很小的技術點,因爲使用的壓縮算法都是現成的。但是值得一提的是,JPG的壓縮率是真的高,在實驗數據上實現了10-20倍的壓縮率。
使用了多線程壓縮,壓縮完過後,更新對應的桶
,這裏的桶
實際上就是數據片。
由主線程Main Thread
反覆從桶裏取數據片(t1),每取1片發送一次,然後再取下一片(t2),直到所有桶
都被取了一次(例子中有10片)。至此,一張圖片的分片數據被全部取完,於是開始統計一些FPS相關信息。
3.4 接收隊列
接收端開了10個線程用於異步socket接收數據片。
爲了保證接收端產生絲滑的視頻效果,使用接收隊列是個不錯的選擇。本文使用了2個隊列的設計。實現數據接收的二級緩衝。
示意圖如下:
這樣一來,視頻效果明顯絲滑了很多。
4. 遇到的坑及解決辦法
4.1. Windows防火牆
巨坑,最好都關了。
4.2. 路由器網絡頻段
同一臺路由器的5G
和2.4G
頻段有時候不能互相ping通,要確保兩個電腦連接在同一頻段上。
4.3. Wifi配置
如果上述設置都對了,但是還是ping不通。將wifi連接設置成專用網絡
,也許就能解決問題。
4.4. 硬件瓶頸
個人PC的性能是較大瓶頸,尤其是單機測驗的時候(本地兩個終端,一個發送、一個接收),CPU使用率分分鐘到100%。聽某個技術大哥說要使用GPU壓縮。
用兩臺電腦,一臺接收一臺發送之後,效果要好很多。
4.5. OpenCV讀取攝像頭大坑
由於攝像頭驅動的關係,在我的電腦上需要設置以下兩個變量,才能成功啓用外置的720p攝像頭。
os.environ["OPENCV_VIDEOIO_DEBUG"] = "1"
os.environ["OPENCV_VIDEOIO_PRIORITY_MSMF"] = "0"
即使如此,如果不做額外的設置,讀出來的圖片將是480p的(看起來很像是720p被壓縮過後的)。所以如果要傳輸真·720p,還需要設置讀出的圖像大小,如下:
self.stream = cv2.VideoCapture(1) # 讀取第一個外置攝像頭
self.stream.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) # float
self.stream.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) # float
4.6. Socket卡頓
不知道是不是我寫的有問題,感覺多線程的socket
會爭搶資源(發送和接收的線程間,對應5.1節
功能),造成接收端的畫面顯示將變得卡頓。
5. 尚未Bug Free的功能
5.1 使用TCP回傳幀率信息
爲了計算網絡時延,採取類似伽利略測光速的方法。從數據包打包之前,到對方收到數據包之後,再將這個數據回傳到發送方。這樣就不存在兩臺機器時間差校準的問題。
該算法的大致流程如下圖所示。
如上圖,這樣回傳之後,設數據包時間戳是(單位,下同),當前時間是,則網絡時延計算方式爲:
這種計算方式應該是自己的實驗環境下比較準確的方法了。
時延信息的反饋不需要特別快(比如200-500ms發送一次),所以使用TCP技術
其實TCP和UDP在使用Python編程的時候代碼差距可以說極小…
但是!!!
- 自己目前在實現信息回傳的時候,會莫名卡頓起來。
- 接收端建立回傳的
socket
之後,甚至還沒傳輸數據,整個程序運行起來就變得非常卡頓,這個讓我比較苦惱,目前正在找bug.
5.2 擁塞控制的算法
這個本來是想着和5.1
綜合起來用的,已經寫好了,但是還沒能真正展現價值,設計是否合理也值得商榷。
控制的是發送端的發送頻率,從而實現接收端的流暢播放
思想和TCP的擁塞控制一樣慢增長,快下降
。如果接收端的隊列一直處於較空的狀態,則表明還有一定的性能剩餘,此時可以緩慢加快發送的頻率;如果檢測到接收端隊列中數據較多,表明發送速度太快來不及顯示,這時候就大幅下降發送的頻率。
這個擁塞控制的算法基於幾個假設:
- 網絡情況良好,丟包率比較低;
- 接收端電腦的性能足夠高,來得及處理解包、顯示圖像。
如果5.1能夠正確實現,則應該根據網絡時延的大小來控制發送的頻率。
6. 總結
這個項目是一週的時間內完成的,目前還有點bug。小組內的成員分別在不同技術方向上進行了探索,收穫都還挺大的。這篇博客就當一個項目總結吧,寫的難免有紕漏之處,有想法和問題可以在評論區交流。