本文來自於騰訊bugly開發者社區,非經作者同意,請勿轉載,原文地址為:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1204&extra=page%3D1
2016年應該是直播元年,直播應用百團大戰,QQ 空間也在6.5版本上線了直播功能,從無到有、快速搭建了直播間。“先扛住再優化”,第一個版本和競品相比,我們進入直播間的速度比較慢。根據外網統計在6.5版本的用戶端看到畫面需要4.4s,因此在6.5發布之后,着手啟動了優化工作,目標:觀看直播需要達到秒進體驗(1s內看到畫面)。
先上一張直播間的截圖:
一、優化效果
1)實驗室數據(小米5 WIFI)30次平均進入時間475ms
2)外網運營數據 (6.5版本 對比 6.5.3版本)
從外網運營數據看,觀看成功率提升到99.41%;觀看延時提升到平均2.5s,加快43.5%。用戶進入直播間的時間區間在(0,1] (觀看端0-1秒內進入成功)的占比提升到19.52%,提升191%。
3)與競品“映客”對比 (左側空間 VS 右側映客 小米5 WIFI)
https://v.qq.com/iframe/preview.html?vid=x0306f83wfv&
4)總結
1、優化可以拔高速度上限,能使用戶進入直播間的耗時上限提高到 500ms 以內,從(0,1]的占比區間提升,對大量用戶的提升還是比較明顯的。
2、直播是強依賴網絡狀況的產品。如果主播的網絡條件很差,上行丟包嚴重時,觀眾卡在這個時間點進入,由於沒有上行是拉取不到首幀數據的,這種情況會導致統計數據被拉高。這也是整體平均時間未到 1s 以內的原因。
二、QQ 空間直播的架構
在前期技術選型上,綜合考慮開發周期,穩定性和質量監控體系,我們選用騰訊雲的現有視頻互動直播解決方案,以下是整體的架構圖。
1、 直播房間使用 roomid 做唯一 key。邏輯上分為兩層。音視頻房間,主要負責和騰訊雲的流媒體服務器通信音視頻數據和音視頻房間狀態的維護;消息房間,主要負責和空間的服務器進行交互,包括贊,評,打賞等業務邏輯和消息房間的狀態維護。消息房間通過注冊接口來響應音視頻房間的狀態。這樣設計好處就是,消息房間和音視頻房間是解耦的,各自單獨運行都是允許的;
2、 觀眾端可以通過雲 sdk、RTMP 或 HLS 協議三種方式收到主播的推流;觀看場景涵蓋 H5,native 多平台。
3、 直播浮層設計為獨立進程,主要是考慮到獨立進程 crash 不影響主進程的穩定性;缺點是和主進程的通信復雜,進程啟動有部分耗時;
三、耗時分析
我們將觀看直播耗時的各階段拆細分析:
1、 整個觀看直播的流程是串行的,導致整體耗時是每個步驟的耗時累加。
2、 拉取房間信息,拉取直播參數配置,拉取接口機 IP 是三個網絡請求,耗時存在不穩定性,一般是 300ms,網絡情況不好就會到 1000ms+;
3、 直播進程的生命周期是跟隨 avtivity 的生命周期,activity 銷毀后,進程也隨之銷毀,再打開需要耗時重新創建進程。
4、互動直播 SDK 的上下文是依賴直播進程,新進入也需要重新初始化。
5、 拉取首幀數據是單步驟耗時最久,急需解決。
四、確立方案,各個擊破
根據直播的具體業務來分析,我們確立了以下幾個解決的緯度。
速度優化一般有以下幾個方向來解決問題:
1、 預加載。
2、 緩存。
3、 串行變為並行,減少串行耗時。
4、 對單步驟中的耗時邏輯梳理優化。
根據這些方向,我們做的工作:
1、 預加載進程。
2、 互動直播 SDK 上下文全局單例,並且預先初始化。
3、 並行預拉取接口機 IP,房間信息,預進入互動直播 sdk 房間。
4、 接口機緩存首幀數據,減少 GOP 分片時間,修改播放器邏輯,解析到 I 幀就開始播放。
1)新方案的整體流程圖:
該方案在加速的基礎上,還有其他的優點:
1、對現有的代碼改動最小,保證版本的穩定性,除了新增的預拉取邏輯,在原有流程上只需要將之前的異步邏輯改為拉取緩存的邏輯。
2、原有邏輯成為備份邏輯,流程茁壯型得到增強,預拉取失敗還有原有邏輯作為備份“重試”,進房間成功率提高。
2)預拉取流程,詳細介紹
從“預拉取接口機 IP”這個點來詳細介紹如何做預拉取,緩存管理和時序處理:
1、 由於直播進程和主進程是內存隔離。Feeds 滾動停止(開始預拉取)是在主進程觸發。拉取的 wns 請求需要在直播進程。通過 AIDL 跨進程去調用。
2、 接口機 IP 的請求為異步,需要緩存請求的狀態。請求緩存接口機 IP 數據時,預拉取的狀態為成功,直接使用緩存數據。
預拉取的狀態為請求中,等待本次預拉取的結果。
預拉取的狀態為失敗,走之前流程,重新請求接口機 IP。
3、 接口機 IP 需要有時效性的,每次滑動停止都預加載 IP,會造成了請求浪費;並且騰訊雲的接口機 IP 有就近接入的特性。為保證負載穩定,如果一直使用緩存的接口機 IP 可能會導致某台機器負載過多。需要加入時效性的控制。
3)秒開關鍵
細心的同學肯定發現還有一個最大的耗時點沒有解決——拉取首幀數據過慢。這個步驟耗時降低才是秒開的關鍵。
首幀數據的展示過程,其實是一個下載,解碼,渲染的過程。
這里簡單插述一下視頻編解碼過程中的一種約定:GOP( Group of Pictures )
為了便於視頻內容的存儲和傳輸,通常需要減少視頻內容的體積,也就是需要將原始的內容元素(圖像和音頻)經過壓縮,壓縮算法也簡稱編碼格式。例如視頻里邊的原始圖像數據會采用 H.264 編碼格式進行壓縮,音頻采樣數據會采用 AAC 編碼格式進行壓縮。 視頻內容經過編碼壓縮后,確實有利於存儲和傳輸; 不過當要觀看播放時,相應地也需要解碼過程。因此編碼和解碼之間,顯然需要約定一種編碼器和解碼器都可以理解的約定。就視頻圖像編碼和解碼而言,這種約定很簡單: 編碼器將多張圖像進行編碼后生產成一段一段的 GOP ( Group of Pictures ) , 解碼器在播放時則是讀取一段一段的 GOP 進行解碼后讀取畫面再渲染顯示。 GOP ( Group of Pictures ) 是一組連續的畫面,由一張 I 幀和數張 B / P 幀組成,是視頻圖像編碼器和解碼器存取的基本單位,它的排列順序將會一直重復到影像結束。
在雲 SDK 中,將幀類型擴展到五種:
- I 幀不需要參考幀。
- P 幀只參考上一幀。
- P_WITHSP 幀可參考上一幀、I 幀、GF 幀、SP 幀,自己不可以被參考。
- SP 幀可參考 I 幀、GF 幀、SP 幀。
-
GF 幀可參考 I 幀、GF 幀 。
1) 標准的 H264 編碼的參照關系,每一個 GOP 的第一針是 I 幀,P 幀依次參考上一幀,抗丟包性不強,如果中間有 I 幀或 P 幀丟失,則該 GOP 內后續 P 幀就會解碼失敗。
(1.標准 GOP 組織圖)
2) 在實時直播的場景,為保證流暢性,重寫編碼器邏輯,首個 GOP 包開頭為I幀,后面 GOP 包開頭為 GF 幀,這是利用 GF 幀的傳遞參考關系是跨 GOP,每個 GF 幀參考上一個 GOP 的 I 幀或 GF 幀。GF 幀體積對比 I 幀要小,后續 GOP 的下載解碼更快速。
(2.SDK 的 GOP 組織圖)
3) 對 GOP 內部的幀組織,也使用 P_WITHSP 幀來代替 P 幀,主要是因為 P_WITHSP 幀(粉紅色表示)的解析可參考上一幀、I 幀、GF 幀、SP 幀,自己不可以被參考。就算上一幀 P_WITHSP 未解碼出來,后一幀 P_WITHSP 的解碼也不受影響,增強了抗丟包性
(3.SDK 的 GOP 幀內部參考關系)
那具體到業務上,通過 wireshark 抓包我們發現。過程主要耗時在首個 GOP 包下行比較慢,需要等待 I 幀(FT 是 0 代表 I 幀)下載完畢才開始解碼,如果 I 幀不完整無法解碼,則需要等待第二個 GOP 包,等待時間加長。
那通過這個現象,為了讓整個過程加快,和 SDK 的同事在1.8.1版本一起做了以下工作:
1、 減小首個 GOP 包的分片大小:將 GOP 的分片由 5s 改為 3s,並且首個 GOP 包只緩存必要的 I 幀,減少首個 GOP 包的體積;(PS:GOP 包的長度和主播端編碼性能也是強相關,GOP 分片太小,編碼性能不高,分片時長的確定需要綜合考慮)
2、 首個 GOP 包需要走網絡下載,同樣網絡條件下這部分路徑越短下載越快。GOP 包之前是存在流控服務器上,GOP 包要到達客戶端連接的接口機,還需要鏈接傳輸的耗時。新的版本直接在接口機上緩存當前直播中房間的 GOP 數據,保證在客戶端連上接口機之后,就可以直接從本機緩存中推流首幀數據,省掉之前的鏈接傳輸耗時。
3、 大部分播放器都是拿到一個完整的 GOP 后才能解碼播放, 改寫播放器邏輯讓播放器拿到第一個關鍵幀(I幀)后就給予顯示。不需要等待全部的 GOP 下載完畢才開始解碼。
以上三點做好了之后,效果明顯,整個的拉取首幀的時間由之前的 2140ms 降到平均 300ms,當然完成這些工作並不是上面敘述的三點那么簡單,中間過程我們也發現一些棘手問題,並推動解決:
如主播上行網絡丟包導致的 GOP 亂序、多台接口機之間緩存的管理、GOP 分片時長的確定。
4)持續優化
我們一直沒有放棄“更快更爽”的體驗追求,在后續的迭代中也持續優化直播的體驗:
1、接口機 IP 競速。
2、合並請求。
3、多碼率。五、遇到的問題
我們的優化手段是將串行的異步請求改為並行;但是將串行改為並行后,幾個異步請求同時開始,如何保證各個異步回調的時序運行正常,這是一定要解決的問題,也是大家在做優化過程中比較有代表性的問題。
處理這種異步回調時序問題類似於 Promise 模式。我這里在具體業務上使用 LiveVideoPreLoadManager 來統一處理,類圖如下: -
LiveVideoPreLoadManager:負責對外暴露啟動預加載方法和拉取結果數據對象的方法。其主要方法及職責如下。
Compute:注冊監聽器,獲取結果的數據對象,使用監聽器實例來響應對數據對象的處理。
preLoad:啟動異步任務的執行。 -
CacheManager:緩存異步任務處理結果和狀態,檢查是否過期。負責檢測異步任務是否處理完畢、返回和存儲異步任務處理結果。其主要方法及職責如下。
getResult:獲取緩存異步任務的執行結果。
setResult:設置緩存異步任務的執行結果及當前的執行狀態:開始,過程中,結束。
isDone:檢測異步任務是否執行完畢。 - Result:負責表示異步任務處理結果。具體類型由相應的業務決定。
- Task:負責真正執行異步任務。其主要方法及職責如下
run:執行異步任務所代表的過程。
獲取異步任務處理結果的序列圖如下。
采用這種模式,當異步任務同時開始,如拉取房間信息,接口機 IP,房間信息,它們都被封裝在 LiveVideoPreLoadManager 的 Task 請求實例中,而主流程則無須關心這些細節,只需要將之前的請求方式變為 LiveVideoPreLoadManage.compute,並注冊對應的異步回調接口。Compute 內部會通過 CacheManager 的 getResult 方法檢查異步任務處理結果狀態,如果異步任務已經執行完畢,則該調用會直接返回,類似與同步操作(步驟5,6,7),那么 LiveVideoPreLoadManager 對外暴露的 compute 方法是個同步方法;若異步任務還未執行完畢,則會阻塞一直等待異步任務執行完畢,再調用 compute 注冊的回調來響應結果,此時 compute 方法是個異步方法(步驟5,4)。也就是說,無論compute方法是一個同步方法還是異步方法,對客戶端的編寫方式都是一樣的。
采用這種 Promise 模式,即對原有流程改動最小,也增強了原有流程的茁壯型,在預拉取失敗的時候,那么原有流程的串行邏輯作為兜底保護。從統計數據也可以看到,在優化版本之后,版本的觀看端進入房間成功率也有提升。六、總結
整個的秒開優化版本時間非常緊張,中間肯定還有別的優化空間,統計數據上來看,整體用戶的進入時間還是在 2.5s+,新的迭代版本還在持續優化,大家如果對秒進有什么好的想法和建議,歡迎交流。也歡迎大家下載新版 QQ 空間獨立版體驗 Qzone 的直播功能,分享生活,留住感動! - 更多精彩內容歡迎關注bugly的微信公眾號:
-
騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合並功能幫助開發同學把每天上報的數千條 Crash 根據根因合並分類,每日日報會列出影響用戶數最多的崩潰,精准定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在發布后快速的了解應用的質量情況,適配最新的 iOS, Android 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!