注:本文已收錄專利,版權所有。另外,部分段落縮減了部分內容。
1. 背景
當下移動互聯網視頻直播正處于如火如荼的井噴式發展當中,不同的行業(比如教育、醫療、旅游等)都涉足參與進來,企圖在這個市場上占有一席之地,爭當獨角獸式的公司。傳統的直播,大多數是單向型,比如電視臺或者運營商直播,用戶只需要打開終端收看即可,對于實時性并沒有太大的要求。而移動直播往往在功能上需要主播端和播放端有交互,這種交互不限于文字的互動,視頻與語音的互動也日趨成為基本的交互場景需求,傳統的做法是主播端采集視頻數據并編碼成x264、采集音頻數據并編碼成aac,再合并打包后通過一定的Qos算法將音視頻流數據通過rtmp協議(基于TCP協議之上)推流到CDN服務器進行分發,用戶端從CDN服務器拉流解碼來播放,這種方式的延時表現在rtmp推流到CDN和播放器從CDN通過rtmp拉流緩存播放,整個網絡鏈路的延遲通常在1-3秒或者更差。在網絡不穩定的情況下,通常提高用戶觀看體驗的方式是通過在主播端和播放端設置Gop緩存,讓碼率均勻;另外可以在主播端通過Qos算法檢測變化的網絡來動態改變碼率和幀率;還可以接入多個視頻云服務CDN提供商,這樣可以做推拉流線路互備,對推流后視頻服務集群再優化并根據端點網絡狀況做實時線路切換。另外一種方式是通過http HLS的方式進行傳輸,這種切片式的直播方式延遲更大,不適合高互動式的場景直播。
所以本文提出了一種基于UDP方式的視頻傳輸方案(webrtc層外),旨在服務于低延遲高實時互動式的直播場景,這種方式能將延遲控制在800ms人眼可接收范圍內,在網絡帶寬比較差或者強丟包、亂序的情況下,通過緩存機制、帶寬自適應檢測機制實時匯報給編碼器降低碼率、分辨率、幀率,能可靠有效的將實時畫面完整的播放出來,提高了用戶體驗。
2 視頻傳輸流程
2.1 視頻傳輸時序圖
本方案采用基于udp協議的報文傳輸,設計了一套私有的報文格式,以使得整個報文在網絡鏈路中傳輸簡單可靠并可控,相對于TCP傳輸的方式,大大的降低了報文的大小與復雜度,保證了整個傳輸過程的實時性。如下圖1為視頻傳輸的時序圖。
2.2 關鍵技術
本方案視頻編碼采用h264的形式(也可以是h265,負載數據是不受傳輸模塊限制的,可以在兩端進行適配和協調),因為B幀是雙向預測幀,它需要根據后向視頻幀來預測編碼,一定程度上會增大編解碼延遲,所以為了保證傳輸和播放的實時性,本方案視頻壓縮丟棄B幀編碼。
2.2.1 發送端處理
2.2.1.1 視頻切片算法
當編碼器編碼出一幀完整的h264視頻幀數據時送入發送端,對于高分辨率的視頻編碼幀,幀大小往往高于UDP網絡MTU,所以此時需要發送端對其進行分片處理再發送,每次按照分片單位來發送幀塊數據。
視頻編碼幀的最大分片數SMAX為500,規定單個分片字節大小SEGS為800bytes (根據探測MTU可動態計算調整)。當幀字節大小FS小于SEGS+50時只分為一個分片;否則,整數倍的分片數S為FS/SEGS,超出的最后字節數FS1為FS%SEGS,如果FS1大于50,則再單獨分一個分片,此時總分片數為S+1,否則如果FS1小于50并且大于0時,將FS1字節累加放入最后一個分片中,此時總分片數為S。如下圖2為視頻分片結構與流程。
2.2.1.2 發送端滑動窗口
發送緩存區保存著所有正在發送且沒有收到接收方連續seq確認acked的報文。當收到peer端發來的分片的ack信息(攜帶一系列丟包的分片seq和已經ack并連續處理到的seq)時,發送端從發送緩存中獲取對應的那些丟包seq并重發分片(如下圖的seq 10/12分片),同時從緩存中刪除區間[s1+1, s2]中的分片,同時移動滑動窗口。如下圖3為發送端滑動窗口滑動過程圖。
2.2.1.3 發送端帶寬自適應調整算法
對于網絡的不可預測性,可能出現抖動、擁塞或者很多的丟包,如果按照固定的碼率和參數來發送視頻幀,這會導致發送端與接收端的線路間更擁塞,從而使觀看端出現更多的播放延遲或者馬賽克。所以在發送端需要做帶寬的實時估算來探測網絡情況,從而便于實時根據網絡帶寬來調整上層視頻編碼器的幀率或者碼率。
在發送端設置了一個定時器,每10秒鐘(可配置)做一次帶寬統計。rtt修正值(網絡抖動的時間差值)為rtt_var,期望目標帶寬為dst_bw,當前發送的幀分片時間為cur_ts,最后acked的幀分片時間為acked_ts,則
delay_ts_delta = cur_ts–acked_ts
當前單位時間內的acked帶寬為bw,則帶寬抖動修正值acked_bw為:
acked_bw = (acked_bw * 3 + bw) / 4
如果當前有包在重發,且 delay _ts_delta 大于 8 * MAX(rtt + rtt_var, 100),則dst_bw = acked_bw,向下降低調整視頻編碼器的分辨率、幀率或碼率來保證播放的實時性和流暢性;否則如果acked_bw 大于0,則dst_bw = acked_bw * 9 / 8,向上提高調整視頻編碼器的分辨率、幀率或碼率來恢復清晰度和提高播放體驗。
2.2.1.4 過期幀丟棄策略
在網絡擁塞時可能發送窗口緩沖區中有很多正在發送中的分片報文,為了緩解擁塞和減少延遲會對整個緩沖區做檢查,如果有超過一定閾值時間的GOP 幀存在,則會將這個 GOP 內的所有幀的分片從窗口緩沖區移除,并將它的下一個 GOP 的 I 幀 fid和分片seq 通過 syn 協議同步到各個接收端上,接收端接收到此協議,會將最新連續 seq 設置成同步過來的 seq。如果頻繁出現過期幀丟棄處理則會造成一定程度上的播放卡頓,此時說明當前網絡不適合傳輸高分辨率或高幀率的視頻,可以通知上層視頻編碼器設置為更小的分辨率或幀率。
2.2.2 接收端處理
2.2.2.1 接收端收包處理
接收端收到服務器中轉過來的syn消息(攜帶用戶uid、開始分片序列號start_seq、幀率)后,根據uid查找(或分配)對應的發送者并激活,同時根據start_seq更新已經連續接收到的分片序列號base_seq和當前接收到的分片最大序列號max_seq。
接收端第一幀必須是關鍵幀(即一個完整GOP的開始),如果是其他幀則丟棄,直到出現關鍵幀為止,因為如果第一幀是P幀將出現花屏現象。
每收到一個分片,如果此分片的seq小于base_seq或者幀fid小于已經接收到的最小幀min_fid(已經接收過了)或者分片的seq大于max_seq+2000(太大的跳變導致丟包緩存太大),則丟棄;否則,當收到第一個關鍵幀的第一個片段時,記下此時max_seq和base_seq,將此分片放入對應幀fid的分片緩存區,同時計算單位時間內的幀間隔時長,并更新丟包緩存表,如果此分片的seq和前一個已接收分片的seq連續,則更新base_seq為此分片的seq,再更新max_seq為MAX(max_seq, seq),最后發送ack給peer端。
如下圖5為接收端收到幀分片并存儲到緩存區的一個實例,對于一個完整的幀fid1的分片序列為區間seq [1, 8],幀fid2的分片序列為區間seq [9, 20]。幀的分片seq總是單調遞增的。
2.2.2.2 更新丟包緩存區策略
如下圖所示,接收端已經連續收到了seq [1, 5] 的分片包,此時base_seq 和 max_seq 都是5,當接收到下一個分片包seq 10時,將seq 10從丟包緩存中刪除,此時認為 seq [6, 9] 是暫時丟失的(可能亂序不一定真丟失,需要后續的包來確認),如果丟包緩存中沒有此序號的丟包,則將它們放入丟包緩存中,同時更新它們的丟包時間戳(當前時間減去rtt值),等待下一次的接收確認。
2.2.2.3 接收端發送回應ack策略
當接收端每次收到peer端發來的分片,需要判斷是否發送回應ack給peer端,發送周期是10ms(毫秒),小于10ms則不發送ack,發送太頻繁會導致網絡擁塞;否則,獲取接收緩存中最老的一幀中最小的分片包序號min_seq,檢查接收端丟包緩存區,并刪除區間[base_seq+1, min_seq]中的丟包,表明這些丟包已經處理過了,同時設置滑動窗口的base_seq為min_seq,然后循環掃描丟包緩存區檢查當前時間是否超過一個發送分片的rtt時間,如果超過則累加此分片的丟包計數器,并更新丟包分片的時間為當前時間,最后將所有超過rtt時長的丟包發送ack回peer端,最后計算播放緩存延遲時長。
在幀緩存中會選擇性刪除一些播放過的幀分片,播放過的幀分片是不需要進行重發的。
在接收端設置了一個定時器,每隔5ms(毫秒)也會檢測一次是否發送回應ack給peer端,并掃描檢查和更新丟包緩存區。
2.2.3 獲取播放視頻幀處理
2.2.3.1 播放緩存區策略
在當前播放端設置了一個幀緩存區,如果緩存區過大時播放延遲就大,過小時又會出現播放卡頓情況。所以設置播放緩存區策略就至關重要,緩沖時間大小wait_ts應該大于rtt + 2 * rtt_val,根據重發報文的次數來決定,在接收端的計時器中定期檢查丟包數和rtt時長來動態的確定wait_ts的大小。
每次上層從播放緩存區獲取一幀時,內部都會檢查當前播放緩存區是處于可播放(max_fid>min_fid并且緩存區中最新幀(max_fid)的時間戳max_ts>wait_ts * 5 / 4,則更新可播放的絕對時間戳play_ts為當前系統時間且cached_ts = max_ts - wait_ts * 5 / 4)中還是緩沖(max_fid=min_fid)中,如果在緩沖中則返回空數據;否則同步更新播放緩沖幀時間戳,如果緩存中最久的一幀(min_fid)的所有分片已經接收完整并且此幀時間F_Oldest_ts在播放緩存時間cached_ts內,則合并當前幀分片返回給上層解碼播放,同時從緩存中刪除此幀,并且更新min_fid為此幀的fid和當前已經緩存到的幀的時間戳cached_ts。
此cached_ts的計算方式為:當 F_Oldest_ts + wait_ts * 5 / 4 >= max_ts(說明緩存區幀很少)或者 min_fid + 1 = max_fid (說明只剩一幀)時,cached_ts = F_Oldest_ts;否則cached_ts = max_ts - wait_ts * 5 / 4,說明緩存區的幀足夠。