移動直播技術秒開優化經驗

現今移動直播技術上的挑戰要遠遠難于傳統設備或電腦直播,其完整的處理環節包括但不限于:音視頻采集、美顏/濾鏡/特效處理、編碼、封包、推流、轉碼、分發、解碼/渲染/播放等。

直播常見的問題包括

主播在不穩定的網絡環境下如何穩定推流?

偏遠地區的觀眾如何高清流暢觀看直播?

直播卡頓時如何智能切換線路?

如何精確度量直播質量指標并實時調整?

移動設備上不同的芯片平臺如何高性能編碼和渲染視頻?

美顏等濾鏡特效處理怎么做?

如何實現播放秒開?

如何保障直播持續播放流暢不卡頓?

本次分享將為大家揭開移動直播核心技術的神秘面紗。

視頻、直播等基礎知識

什么是視頻?

首先我們需要理解一個最基本的概念:視頻。從感性的角度來看,視頻就是一部充滿趣味的影片,可以是電影,可以是短片,是一連貫的視覺沖擊力表現豐富的畫面和音頻。但從理性的角度來看,視頻是一種有結構的數據,用工程的語言解釋,我們可以把視頻剖析成如下結構:

內容元素 ( Content )

? ? ? ? 圖像 ( Image )

? ? ? ? 音頻 ( Audio )

? ? ? ? 元信息 ( Metadata )

編碼格式 ( Codec )

? ? ? ? Video : H.264,H.265, …

? ? ? ? Audio : AAC, HE-AAC, …

容器封裝 (Container)

? ? ? ?MP4,MOV,FLV,RM,RMVB,AVI,…

任何一個視頻 Video 文件,從結構上講,都是這樣一種組成方式:

? ? ? ?由圖像和音頻構成最基本的內容元素;

? ? ? ?圖像經過視頻編碼壓縮格式處理(通常是 H.264);

? ? ? ?音頻經過音頻編碼壓縮格式處理(例如 AAC);

? ? ? ? 注明相應的元信息(Metadata);

最后經過一遍容器(Container)封裝打包(例如 MP4),構成一個完整的視頻文件。

如果覺得難以理解,可以想象成一瓶番茄醬。最外層的瓶子好比這個容器封裝(Container),瓶子上注明的原材料和加工廠地等信息好比元信息(Metadata),瓶蓋打開(解封裝)后,番茄醬本身好比經過壓縮處理過后的編碼內容,番茄和調料加工成番茄醬的過程就好比編碼(Codec),而原材料番茄和調料則好比最原本的內容元素(Content)。

視頻的實時傳輸

簡而言之,理性的認知視頻的結構后,有助于我們理解視頻直播。如果視頻是一種“有結構的數據”,那么視頻直播無疑是實時傳輸這種“有結構的數據”(視頻)的方式。

那么一個顯而易見的問題是:如何實時(Real-Time)傳輸這種“有結構的數據”(視頻)呢?

這里邊一個悖論是:一個經過容器(Container)封裝后的視頻,一定是不可變的 ( Immutable ) 視頻文件,不可變的 ( Immutable ) 的視頻文件已經是一個生產結果,根據“相對論”,而這個生產結果顯然不可能精確到實時的程度,它已經是一段時空的記憶。

因此視頻直播,一定是一個 “邊生產,邊傳輸,邊消費”的過程。這意味著,我們需要更近一步了解視頻從原始的內容元素 ( 圖像和音頻 ) 到成品 ( 視頻文件 ) 之前的中間過程 ( 編碼 )。

視頻編碼壓縮

不妨讓我們來深入淺出理解視頻編碼壓縮技術。

為了便于視頻內容的存儲和傳輸,通常需要減少視頻內容的體積,也就是需要將原始的內容元素(圖像和音頻)經過壓縮,壓縮算法也簡稱編碼格式。例如視頻里邊的原始圖像數據會采用 H.264 編碼格式進行壓縮,音頻采樣數據會采用 AAC 編碼格式進行壓縮。

視頻內容經過編碼壓縮后,確實有利于存儲和傳輸; 不過當要觀看播放時,相應地也需要解碼過程。因此編碼和解碼之間,顯然需要約定一種編碼器和解碼器都可以理解的約定。就視頻圖像編碼和解碼而言,這種約定很簡單:

編碼器將多張圖像進行編碼后生產成一段一段的 GOP ( Group of Pictures ) , 解碼器在播放時則是讀取一段一段的 GOP 進行解碼后讀取畫面再渲染顯示。

GOP ( Group of Pictures ) 是一組連續的畫面,由一張 I 幀和數張 B / P 幀組成,是視頻圖像編碼器和解碼器存取的基本單位,它的排列順序將會一直重復到影像結束。

I 幀是內部編碼幀(也稱為關鍵幀),P 幀是前向預測幀(前向參考幀),B 幀是雙向內插幀(雙向參考幀)。簡單地講,I 幀是一個完整的畫面,而 P 幀和 B 幀記錄的是相對于 I 幀的變化。

如果沒有 I 幀,P 幀和 B 幀就無法解碼。

小結一下,一個視頻 ( Video ) ,其圖像部分的數據是一組 GOP 的集合, 而單個 GOP 則是一組 I / P / B 幀圖像的集合。

在這樣的一種幾何關系中,Video 好比一個 “物體”,GOP 好比 “分子”,I / P / B 幀的圖像則好比 “原子”。

想象一下,如果我們把傳輸一個 “物體”,改成傳輸一個一個的 “原子”,將最小顆粒以光速傳送,那么以人的生物肉眼來感知,將是一種怎樣的體驗?

什么是視頻直播?

不難腦洞大開一下,直播就是這樣的一種體驗。視頻直播技術,就是將視頻內容的最小顆粒 ( I / P / B 幀,…),基于時間序列,以光速進行傳送的一種技術。

簡而言之,直播就是將每一幀數據 ( Video / Audio / Data Frame ),打上時序標簽 ( Timestamp ) 后進行流式傳輸的過程。發送端源源不斷的采集音視頻數據,經過編碼、封包、推流,再經過中繼分發網絡進行擴散傳播,播放端再源源不斷地下載數據并按時序進行解碼播放。如此就實現了 “邊生產、邊傳輸、邊消費” 的直播過程。

理解以上兩個關于?視頻和直播兩個基礎概念后,接下來我們就可以一窺直播的業務邏輯了。

直播的業務邏輯

如下是一個最精簡的一對多直播業務模型,以及各個層級之間的協議。

各協議差異對比如下

以上就是關于直播技術的一些基礎概念。下面我們進一步了解下影響人們視覺體驗的直播性能指標。

影響視覺體驗的直播性能指標

直播第一個性能指標是延遲,延遲是數據從信息源發送到目的地所需的時間。

根據愛因斯坦的狹義相對論,光速是所有能量、物質和信息運動所能達到的最高速度,這個結論給傳播速度設定了上限。因此,即便我們肉眼感覺到的實時,實際上也是有一定的延遲。

由于 RTMP/HLS 是基于 TCP 之上的應用層協議,TCP 三次握手,四次揮手,慢啟動過程中的每一次往返來回,都會加上一次往返耗時 ( RTT ),這些交互過程都會增加延遲。

其次根據 TCP 丟包重傳特性,網絡抖動可能導致丟包重傳,也會間接導致延遲加大。

一個完整的直播過程,包括但不限于以下環節:采集、處理、編碼、封包、推流、傳輸、轉碼、分發、拉流、解碼、播放。從推流到播放,再經過中間轉發環節,延遲越低,則用戶體驗越好。

第二個直播性能指標卡頓,是指視頻播放過程中出現畫面滯幀,讓人們明顯感覺到“卡”。單位時間內的播放卡頓次數統計稱之為卡頓率。

造成卡頓的因素有可能是推流端發送數據中斷,也有可能是公網傳輸擁塞或網絡抖動異常,也有可能是終端設備的解碼性能太差。卡頓頻次越少或沒有,則說明用戶體驗越好。

第三個直播性能指標首屏耗時,指第一次點擊播放后,肉眼看到畫面所等待的時間。技術上指播放器解碼第一幀渲染顯示畫面所花的耗時。通常說的 “秒開”,指點擊播放后,一秒內即可看到播放畫面。首屏打開越快,說明用戶體驗越好。

如上三個直播性能指標,分別對應一個低延遲、高清流暢、極速秒開 的用戶體驗訴求。了解這三個性能指標,對優化移動直播 APP 的用戶體驗至關重要。

那么移動直播場景下具體而言有哪些常見的坑呢?

根據實踐總結下來的經驗,移動平臺上視頻直播的坑主要可以總結為兩方面:設備差異,以及網絡環境這些場景下帶來的技術考驗。

移動直播場景的坑與規避措施

不同芯片平臺上的編碼差異

iOS 平臺上無論硬編還是軟編,由于是 Apple 一家公司出廠,幾乎不存在因為芯片平臺不同而導致的編碼差異。

然而,在 Android 平臺上,Android Framework SDK 提供的 MediaCodec 編碼器,在不同的芯片平臺上,差異表現很大, 不同的廠家使用不同的芯片,而不同的芯片平臺上 Android MediaCodec 表現略有差異,通常實現全平臺兼容的成本不低。

另外就是 Android MediaCodec 硬編層面的 H.264 編碼畫質參數是固定的 baseline,所以畫質通常也一般。因此,在 Android 平臺下,推薦是用軟編,好處是畫質可調控,兼容性也更好。

低端設備如何上高性能地采集和編碼?

例如 Camera 采集輸出的可能是圖片,一張圖的體積并不會小,如果采集的頻次很高,編碼的幀率很高,每張圖都經過編碼器,那么編碼器又可能會出現過載。

這個時候,可以考慮在編碼前,不影響畫質的前提下(前面我們講過幀率的微觀意義),進行選擇性丟幀,以此降低編碼環節的功耗開銷。

弱網下如何保障高清流暢推流

移動網絡下,通常容易遇到網絡不穩定,連接被重置,斷線重連,一方面頻繁重連,建立連接需要開銷。另一方面尤其是發生 GPRS / 2G / 3G / 4G 切換時,帶寬可能出現瓶頸。當帶寬不夠,幀率較高/碼率較高的內容較難發送出去,這個時候就需要可變碼率支持。

即在推流端,可檢測網絡狀態和簡單測速,動態來切換碼率,以保障網絡切換時的推流流暢。

其次編碼、封包、推流 這一部分的邏輯也可以做微調,可以嘗試選擇性丟幀,比如優先丟視頻參考幀(不丟 I 幀和音頻幀 ),這樣也可以減少要傳輸的數據內容,但同時又達到了不影響畫質和版視聽流暢的目的。

需要區分直播流的狀態和業務狀態

直播是媒體流、APP 的交互是 API 信令流,兩者的狀態不能混為一談。尤其是不能基于 APP 的交互的 API 狀態來判斷直播流的狀態。

以上是移動直播場景下常見的幾個坑和規避措施。

移動直播場景其他優化措施

一、怎么優化打開速度,達到傳說中的 “秒開”?

大家可能會看到,市面上某些手機直播 APP 的打開速度非常快,一點就開。而某些手機直播 APP,點擊播放后要等好幾秒以后才能播放。是什么原因導致如此的天壤之別呢?

大部分播放器都是拿到一個完成的 GOP 后才能解碼播放,基于 FFmpeg 移植的播放器甚至需要等待音畫時間戳同步后才能播放(如果一個直播里邊沒有音頻只有視頻相當于要等待音頻超時后才能播放畫面)。

“秒開”可以從以下幾個方面考慮:

1. 改寫播放器邏輯讓播放器拿到第一個關鍵幀后就給予顯示。

GOP 的第一幀通常都是關鍵幀,由于加載的數據較少,可以達到 “首幀秒開”。

如果直播服務器支持 GOP 緩存,意味著播放器在和服務器建立連接后可立即拿到數據,從而省卻跨地域和跨運營商的回源傳輸時間。

GOP 體現了關鍵幀的周期,也就是兩個關鍵幀之間的距離,即一個幀組的最大幀數。假設一個視頻的恒定幀率是 24fps(即1秒24幀圖像),關鍵幀周期為 2s,那么一個 GOP 就是 48 張圖像。一般而言,每一秒視頻至少需要使用一個關鍵幀。

增加關鍵幀個數可改善畫質(GOP 通常為 FPS 的倍數),但是同時增加了帶寬和網絡負載。這意味著,客戶端播放器下載一個 GOP,畢竟該 GOP 存在一定的數據體積,如果播放端網絡不佳,有可能不是能夠快速在秒級以內下載完該 GOP,進而影響觀感體驗。

如果不能更改播放器行為邏輯為首幀秒開,直播服務器也可以做一些取巧處理,比如從緩存 GOP 改成緩存雙關鍵幀(減少圖像數量),這樣可以極大程度地減少播放器加載 GOP 要傳輸的內容體積。

2. 在 APP 業務邏輯層面方面優化。

比如提前做好 DNS 解析(省卻幾十毫秒),和提前做好測速選線(擇取最優線路)。經過這樣的預處理后,在點擊播放按鈕時,將極大提高下載性能。

一方面,可以圍繞傳輸層面做性能優化;另一方面,可以圍繞客戶播放行為做業務邏輯優化。兩者可以有效的互為補充,作為秒開的優化空間。

二、美顏等濾鏡如何處理?

在手機直播場景下,這就是一個剛需。沒有美顏功能的手機直播 APP,主播基本不愛用。可以在采集畫面后,將數據送給編碼器之前,將數據源回調給濾鏡處理程序,原始數據經過濾鏡處理完后,再送回給編碼器進行編碼即可。

除了移動端可以做體驗優化之外,直播流媒體服務端架構也可以降低延遲。例如收流服務器主動推送 GOP 至邊緣節點,邊緣節點緩存 GOP,播放端則可以快速加載,減少回源延遲。

其次,可以貼近終端就近處理和分發

三、如何保障直播持續播放流暢不卡頓?

“秒開”解決的是直播首次加載的播放體驗,如何保障直播持續播放過程中的畫面和聲音視聽流暢呢?因為,一個直播畢竟不是一個 HTTP 一樣的一次性請求,而是一個 Socket 層面的長連接維持,直到直到主播主動終止推流。

上述我們講過卡頓的定義:即播放時畫面滯幀,觸發了人們的視覺感受。在不考慮終端設備性能差異的情況下,針對網絡傳輸層面的原因,我們看看如何保障一個持續的直播不卡頓。

這其實是一個直播過程中傳輸網絡不可靠時的容錯問題。例如,播放端臨時斷網了,但又快速恢復了,針對這種場景,播放端如果不做容錯處理,很難不出現黑屏或是重新加載播放的現象。

為了容忍這種網絡錯誤,并達到讓終端用戶無感知,客戶端播放器可以考慮構建一個FIFO(先進先出)的緩沖隊列,解碼器從播放緩存隊列讀取數據,緩存隊列從直播服務器源源不斷的下載數據。通常,緩存隊列的容量是以時間為單位(比如3s),在播放端網絡不可靠時,客戶端緩存區可以起到“斷網無感”的過渡作用。

顯然,這只是一個“緩兵之計”,如果直播服務器邊緣節點出現故障,而此時客戶端播放器又是長連接,在無法收到對端的連接斷開信號,客戶端的緩沖區容量再大也不管用了,這個時候就需要結合客戶端業務邏輯來做調度。

重要的是客戶端結合服務端,可以做精準調度。在初始化直播推流之前,例如基于 IP 地理位置和運營商的精確調度,分配線路質量最優的邊緣接入節點。在直播推流的過程中,可以實時監測幀率反饋等質量數據,基于直播流的質量動態調整線路。

Q & A

1. 關鍵幀設置頻率一般是多少?有沒有根據接入動態設置?過長首屏秒會很難做到。

徐立:關鍵幀間隔越長,也就是 GOP 越長,理論上畫面越高清。但是生成 HLS 直播時,最小切割粒度也是一個 GOP,所以針對交互直播,通常不建議 GOP 設置太長。直播一般 2 個關鍵幀間隔即可。比如幀率是 24fps, 那么 2 個關鍵幀的間隔就是 48fps ,這個 GOP 就是2s。

2. 七牛這個直播是用的網宿加速?有遇到什么坑沒?

徐立:七牛在直播方面主要是自建節點,也支持融合眾多第三方 CDN 服務商,多樣化的線路組合為客戶提供更優質的服務。在和第三方 CDN 合作的過程中遇到的問題等有機會再做更細粒度的交流和分享。

3. RTMP 直播流除了優化線路外,還有什么加速手段嗎?

徐立:物理上優化線路,邏輯上優化策略,比如選擇性丟幀,不影響編碼畫質的前提下減輕傳輸體積。

4. OBS 推流,播放端 HLS 出現視/音頻不同步是哪個環節的問題?怎么優化?

徐立:有可能是采集端的問題,如果是采集端編碼環節就出現音畫不同步,可以在收流服務器上做音畫時間戳同步,這樣是全局的校對。如果是播放端解碼性能問題,那么需要調節播放邏輯,比如保證音畫時間戳強一致性的前提下,選擇性丟一部幀。

5. PPT 前幾頁中一個概念好像錯了,I 幀不是關鍵幀,IDR 幀才是。IDR 幀是 I 幀,但是 I 幀不一定是 IDR 幀。只有 IDR 幀才是可重入的。

徐立:中文都把 I 幀翻譯成關鍵幀了,不過既然提到了 IDR 幀,可以展開說明一下。所有的 IDR 幀都是 I 幀,但是并不是所有 I 幀都是 IDR 幀,IDR 幀是 I 幀的子集。I 幀嚴格定義是幀內編碼幀,由于是一個全幀壓縮編碼幀,通常用 I 幀表示 “關鍵幀”。IDR 是基于 I 幀的一個 “擴展”,帶了控制邏輯,IDR 圖像都是 I 幀圖像,當解碼器解碼到 IDR 圖像時,會立即將參考幀隊列清空,將已解碼的數據全部輸出或拋棄。重新查找參數集,開始一個新的序列。這樣如果前一個序列出現重大錯誤,在這里可以獲得重新同步的機會。IDR 圖像之后的圖像永遠不會使用 IDR 之前的圖像的數據來解碼。

6. 有沒有調研過 nginx rtmp module,為什么沒有用,對它有什么評價?

徐立:有調研過,nginx_rtmp_module 是單進程多線程,非 go 這種輕量級線程/協程用并發自然語義的方式編寫流業務。nginx 原本的代碼量較大(約 16 萬行,但和直播業務相關的功能并不是很多)。且主要靠寫 nginx.conf 做配置租戶,通常單租戶可以,但業務可擴展性方面不是很靈活,可滿足基本需求,不滿足高級功能。

7. 用到了那些開源軟件?編碼用的是 x264 嗎?直播服務器你們自己開發還是開源的?

徐立:直播服務器用 go 開發的,移動端編碼優先硬編,軟編用 x264

8. 請教一下用 OBS 推流到 nginx_rtmp_module 的時候是已經做了視頻壓縮了還是需要基于 OBS 再開發?

徐立:OBS 把編碼壓縮都做了,不需要再開發。

9. 視頻直播想在 HLS 流中無縫插入一段廣告的 ts 文件,有問題想請教一下:1、這段 ts 的分辨率是否一定要和之前的視頻流一致?2、pts 時間戳是否要和上一個 ts 遞增?

徐立:1、可以不一致。這種情況兩段視頻完全是獨立狀態,可以沒有任何關系,只需要插入 discontinue 標記,播放器在識別到這個標記之后重置解碼器參數就可以無縫播放,畫面會很平滑的切換。2、不需要遞增。舉個例子,視頻 A 正在直播,播放到 pts 在 5s 的時候,插入一個視頻 B,需要先插入一個 discontinue,再插入 B,等 B 播放完之后,再插入一個 discontinue,再插入 A,這個時候 A 的 pts 可以和之前遞增,也可以按照中間插入的 B 的時長做偏移,一般做點播和時移的時候 pts 會連續遞增,直播的話會算上 B 的時長。


將bilibili自帶的Demo運行起來

一般我們用到的任何一個播放器都是基于FFmpeg播放的,但蘋果提供的AiPlayer不支持直播文件,而bilibili已經將FFmpeg封裝好,用它可以面向對象去開發。

1.在GitHub搜索ijkplayer,可以看到,Android,iOS都是基于此

2.找到Build iOS,打開終端

3.輸入:cd Desktop/

git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-ios

下載ijkplayer-ios到桌面

cd Desktop/ijkplayer-ios

./init-ios.sh? ? (下載FFmpeg)

cd ios? (跳到ios文件夾下編譯FFmpeg)

./compile-ffmpeg.sh clean

./complie-ffmpeg.sh all

4.編譯完成后,運行Demo,出現如下界面表示完成

此時需要將bilibili的dome集成到我們自己的工程中,方法有兩個:

1、官方給出的方法是工程里集成工程,比較復雜;

2、將ijk所有的源碼打包成framework,只需要繼承這個framework就可以了。

生成framework步驟如下:

1.運行IJKMediaPlayer下的工程,點擊Edit Scheme…

2.彈出如下窗口,將Debug改成Release,然后點擊Close

3.我們需要Build一個模擬器版本和一個真機版本,然后合并

Build模擬器版本:隨意選一個模擬器

4.command+B,紅色變為黑色,文件已存在,show in Finder可查看

5.然后是真機版本,當沒有連接手機時,我們選Generic iOS Device

再次command+B,生成真機版本

我們真正要合并的是這個文件

6.合并方法

7.完成后,將IJKMediaFramework文件替換到之前真機里面,然后復制上一級IJKMediaFramework.framework文件到桌面,以備后用。

(播直播就是播一個URL,我們已經拿到了URL,已經寫到我們的數據源里了,把數據源拿過來。我們需要導入一個播放的框架,來到target->General->Linked Frameworks and Libraries,點擊加號,添加AVFoundation.framework,導入#import,

MPMoviePlayerViewController *movieVC? = [[MPMoviePlayerViewController alloc] initWithContentURL:[NSURL URLWithString:live.streamAddr]];

[self presentViewController:movieVC animated:YES completion:nil];

至此,當點擊cell時,可以彈出播放界面,但并不能播放,因為無法解碼,SO,我們只能放棄這個MP方法)

這時就需要用到我們事先準備的IJKMediaFramework.framework。

8.將我們合并的IJKMediaFramework.framework拖到我們的工程,一定要點擊這個Copy

9.同時它還會依賴一些框架,在GitHub上,搜索ijkPlayer,找到需要導入的框架,如下

開始導入:來到target->General->Linked Frameworks and Libraries,點擊加號,開始添加,導完編譯一下,查看是否成功

10.接著我們就需要自己創建一個播放的界面了,新建

11.將IJKMediaDemo里的東西復制到YKPlayerViewController來

導入頭文件#import <IJKMediaFramework/IJKMediaFramework.h>

添加協議@property(atomic, retain) idplayer

然后注冊通知,remove通知(將相應代碼復制過來),但還是有黃色警告,說明有方法沒實現,需要把方法在復制過來,剩下的就是創建真正的直播了(配置直播)

來到viewDidLoad,[self initPlayer];

然后完成這個初始化 -(void)initPlayer ,

IJKFFOptions *options = [IJKFFOptions optionsByDefault];

IJKFFMoviePlayerController *player = [[IJKFFMoviePlayerController alloc] initWithContentURLString:self.live.streamAddr withOptions:options];

self.player = player;

self.player.view.frame = self.view.bounds;

self.player.shouldAutoplay = YES;

[self.view addSubview:self.player.view];

然后傳值過去

YKPlayerViewController *playerVC = [[YKPlayerViewController alloc] init];

playerVC.live = live;

[self.navigationController pushViewController:playerVC animated:YES];

至此就可以播放了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容