美拍直播首屏耗時減少50%以上的優化實踐

隨著移動直播的火爆,大量的業務都有直播需求,這就使直播成了一種基本的配置。在觀看直播過程中,首屏時間是最重要的體驗之一,它的快慢直接影響了用戶對該直播APP的體驗。為了提高用戶體驗性,美拍對DNS解析優化、TCP連接耗時、HTTP響應耗時、音視頻流的探測耗時、buffer緩衝的耗時等方面進行了優化,使得首屏時間從2017年初還是秒級別以上的耗時,到現在是秒級別內,耗時減少50%以上,並且大部分請求落在0~500ms 和 500~1000ms 的區間範圍,從而使得大部分熱門視頻達到瞬開的效果。後面我們將基於 ijkplayer 和 ffpmeg 的源碼進行分析。

為什麼選擇ijkplayer播放器來剖析

ijkplayer 播放器是一款開源的基於 ffmpeg 的移動版的播放器,目前已經被很多互聯網公司直接採用。它的代碼結構比較清晰,很多做移動端視頻分析的都應該接觸過,所以基於它來分析應該跟容易理解。美拍直播的播放器並不是直接採用 ijkplayer 播放器,但也是基於 ffmpeg 來實現的,邏輯跟 ijkplayer 比較類似,原理上都是相通的,優化點也很類似,只是額外做了一些其他相關點的優化。所以基於 ijkplayer 展開,也方便大家從源碼級別可以直接看到相關的關鍵點。

一、首屏時間的影響因素

首屏時間是指從用戶從進入到直播間到直播畫面出來的這部分時間,這是觀眾最簡單,直觀的體驗。它主要受直播播放器和CDN加速策略,以及移動端手機網絡的影響。可以拆分為以下個方面:

  • 點擊直播後,進入到直播間後,加載一些比如用戶頭像,觀眾列表,禮物之類的會佔用網絡帶寬,影響到直播加載。

  • 移動端手機網絡帶寬的限制,目前一般直播的帶寬都在1Mbps左右,所以如果下行帶寬小於1Mbps,或者更小,對直播的體驗影響就會很大。

  • 直播播放器拉流的速度,以及緩衝策略的控制,對於直播類,實時性的需求更高,需要動態的緩衝控制策略,能儘快的渲染出視頻畫面,減少用戶等待時間。

  • CDN是否有緩存直播流,以及緩存的策略對首屏影響也很大。

  • 直播拉流協議的影響,以及CDN對不同的協議優化支持友好程度不一樣,當前流行的拉流協議主要有 rtmp 和 http-flv。經過大量的測試發現,移動端拉流時在相同的CDN策略以及播放器控制策略的條件下,http-flv 協議相比rtmp 協議,首屏時間要減少300~400ms 左右。主要是在 rtmp 協議建聯過程中,與服務端的交互耗時會更久,所以後面的分析會直接在 http-flv 協議的基礎上。

二、首屏耗時的“條分節解”

要想優化首屏時間,就必須清楚的知道所有的耗時分別耗在哪裡。下面我們以移動版的 ffplay(ijkplayer)播放器為基礎,逐漸剖析直播拉流細節。下面我們以 http-flv 協議為拉流協議分析,http-flv 協議就是專門拉去flv文件流的 http 協議,所以它的請求流程就是一個http 的下載流程,如下圖:

美拍直播首屏耗时减少50%以上的优化实践

從上圖中可以看出,首屏耗時的組成主要以下基本組成:

美拍直播首屏耗时减少50%以上的优化实践

1,DNS耗時

DNS解析,是所有網絡請求的第一步,在我們用基於ffmpeg實現的播放器ffplay中,所有的DNS解析請求都是 ffmpeg 調用`getaddrinfo`方法來獲取的。

  • 一般耗時多久?

如果在沒有緩存的情況下,實測發現一次域名的解析會花費至少300ms 左右的時間,有時候更長,如果本地緩存命中,耗時很短,幾個ms左右,可以忽略不計。緩存的有效時間是在DNS 請求包的時候,每個域名會配置對應的緩存 TTL 時間,這個時間不確定,根據各域名的配置,有些長有些短,不確定性比較大。

  • 為什麼是這麼久?

為什麼DNS的請求這麼久呢,一般理解,DNS包的請求,會先到附近的運營商的DNS服務器上查找,如果沒有,會遞歸到根域名服務器,這個耗時就很久。一般如果請求過一次,這些服務器都會有緩存,而且其他人也在不停的請求,會持續更新,下次再請求的時候就會比較快。有時候通過抓包發現每次請求都會去請求`A`和`AAAA` 查詢,這是去請求IPv6的地址,但由於我們的域名沒有IPv6的地址,所以每次都要回根域名服務器去查詢。為什麼會請求IPV6的地址呢,因為 ffmpeg 在配置DNS請求的時候是按如下配置的:

hints.ai_family = AF_UNSPEC;

它是一個兼容IPv4和IPv6的配置,如果修改成`AF_INET`,那麼就不會有`AAAA`的查詢包了。通過實測發現,如果只有IPv4的請求,即使是第一次,也會在100ms內完成,後面會更短。這個地方的優化空間很大。

  • 如何統計?

以 ffmpeg 為例,可以在`libavformat/tcp.c`文件中,`tcp_open`方法中,按以下方法統計:

int64_t start = av_gettime;

if (!hostname[0])

ret = getaddrinfo(, portstr, &hints, &ai);

else

ret = getaddrinfo(hostname, portstr, &hints, &ai);

int64_t end = av_gettime;

2,TCP連接耗時

TCP 連接在這裡是只調用 Socket 的 connect 方法,並連接成功的耗時,它是一個阻塞方法,它會一直等待TCP 的三次握手完成。它直接反應了客戶端到CDN服務器節點,點對點的延時情況,實測在一般的 wifi 網絡環境下耗時在50ms以內。耗時較短,基本是沒有什麼優化空間的,不過它的時間反應了客戶端的網絡情況或者客戶端到節點的網絡情況。

  • 如何統計?

以ffmpeg為例,也是在`libavformat/tcp.c`文件中,`tcp_open`方法中,按以下方法統計:

int64_t start = av_gettime;

if ((ret = ff_listen_connect(fd, cur_ai->ai_addr, cur_ai->ai_addrlen,

s->open_timeout / 1000, h, !!cur_ai->ai_next)) < 0) {


if (ret == AVERROR_EXIT)

goto fail1;

else

goto fail;

}

int64_t end = av_gettime;

3,http響應耗時

  • 什麼是http響應耗時?

http 響應耗時是指客戶端發起一個http request 請求,然後等待http 響應的header 返回這部分耗時。直播拉流http-flv協議也是一個http 請求,客服端發起請求後,服務端會先將http的響應頭部返回,不帶音視頻流的數據,響應碼如果是200,表明視頻流存在,緊接著就開始下發音視頻數據。http 響應耗時非常重要,它直接反應了CDN服務節點處理請求的能力。它與CDN節點是否有緩存這條流有關,如果在請求之前有緩存這條流,節點就會直接響應客戶端,這個時間一般也在50ms左右,最多不會超過200ms,如果沒有緩存,節點則會回直播源站拉取直播流,耗時就會很久,至少都在200ms 以上,大部分時間都會更長,所以它反應了這條直播流是否是冷流和熱流,以及CDN節點的緩存命中情況。

  • 如何統計?

如果需要統計它的話,可以在`libavformat/http.c`文件中的,`http_open`方法

int64_t start = av_gettime;

ret = http_open_cnx(h, options);

int64_t end = av_gettime;

4,音視頻流探測耗時

  • 什麼是音視頻流探測耗時?

這個定義比較模糊,它在 ffplay 中對應的是`avformat_find_stream_info`的耗時,它是一個同步的方法。在播放器中它會阻塞整個流程,因為它的作用是找到初始化音視頻解碼器的必要的數據。它有一些參數會印象到它的耗時,不過如果參數設置合適的話,一般是100ms 內完成。

  • 如何統計?

可以在 ijkplayer 的工程中`ff_ffplay.c`文件中,`read_thread`方法

int64_t start = av_gettime;

avformat_find_stream_info(ic, opts);

int64_t end = av_gettime;

5,緩衝耗時

  • 什麼是緩衝耗時?

緩衝耗時是指播放器的緩衝的數據達到了預先設定的閾值,可以開始播放視頻了。這個值是可以動態設置的,所以不同的設置給首屏帶來的影響是不一樣的。我們在美拍直播播放器最開始的設置是視頻幀數和音頻幀數都達到10幀以上,才可以開始播放。所以這部分一般的耗時都比較大,同時它還跟播放器裡面的一個設置 `BUFFERING_CHECK_PER_MILLISECONDS` 值有關,因為播放器 check 緩衝區的數據是否達到目標值不是隨意檢測的,因為 check 本身會有一定的浮點數運算,所以 ijkplayer 最初給他設置了500ms 值,明顯比較大,所以會對緩衝耗時有比較大的影響。

  • 如何統計?

緩衝耗時的統計方法,不像前面幾個那麼簡單,因為它涉及到的代碼有多處,所以需要再多個地方計時。 開始計時可以直接從前面的find後面開始,結束計時可以在第一幀視頻渲染出來的時候結束計時。

avformat_find_stream_info(ic, opts);

start = av_gettime;

if (!ffp->first_video_frame_rendered) {

ffp->first_video_frame_rendered = 1;

ffp_notify_msg1(ffp, FFP_MSG_VIDEO_RENDERING_START);

end = av_gettime;

}

至此,首屏耗時的拆解就完成了,剩下的優化就從具體每個階段著手優化。

三、 首屏時間的具體優化

在前面的分解之後,再來優化首屏時間,思路就比較清晰了。因為流程是串行的,所以只需要做到局部最優,總體就會最優。

1,DNS的優化解析

  • 優化思路

DNS 的解析一直以來都是網絡優化的首要問題,不僅僅有時間解析過長的問題,還有小運營商 DNS 劫持的問題,一般的解決方案都是採用 HttpDNS,但 HttpDNS 在部分地區也可能存在準確性問題,綜合各方面我們採用了HTTPDNS 和 LocalDNS 結合的方案,來提升解析的速度和準確率。前面已經提到了,一般來說如果只是解析IPV4來說,LocalDNS 的耗時並不算長。但我們也不能直接修改 ffmpeg,因為也要考慮到將來的 IPV6 的擴展問題。好在我們內部有專門做 DNS 的 SDK,他們的大概思路是,APP 啟動的時候就會先預解析我們指定的域名,因為拉流域名是固定的幾個,所以完全可以先緩存起來。然後會根據各個域名解析的時候返回的有效時間,過期後再去解析更新。至於 DNS劫持的問題,內部會有一個評估策略,如果 loacldns 出來的IP無法正常使用,或者延時太高,就會切換到 HttpDns 重新解析。這樣就保證了每次真正去拉流的時候,DNS 的耗時幾乎為0,因為可以定時更新緩存池,使每次獲得的 DNS 都是來自緩存池的。

  • 具體實現方式

如何替換掉 ffmpeg 中`tcp.c`文件中的 `ret = getaddrinfo(hostname, portstr, &hints, &ai);` 方法,我們最開始想到了兩種方案:

方案A

比如我們的拉流url是這樣的 `http://a.meipai.com/m/c04.flv`,如果在傳遞url 給 ffmpeg 前將`a.meipai.com` 替換成DNS 預先解析出來的 ip 比如 `112.34.23.45` ,那替換後的url就是`http://112.34.23.45/m/c04.flv`。如果直接用這個url去發起http請求,在有些情況可以,很多情況是不行的。如果這個iP的機器只部署了 `a.meipai.com` 對應的服務,就能解析出來。如果有多個域名的服務,CDN 節點就無法正確的解析。所以這個時候一般是設置 http 請求的 header裡面的 Host 字段。一般可以通過以下代碼傳遞給 ffmpeg 內部,這個參數的作用就是填充 http 的Host 頭部,具體的實現,可以 ffmpeg 源碼,文件`http.c`中`http_connect` 方法中。

AVDictionary **dict = ffplayer_get_opt_dict(ffplayer, opt_category);

av_dict_set(dict, "headers", "Host: hdl-test-meipai.com", 0);

但這種方案有個 bug 就是,如果在發出請求 `http://a.meipai.com/m/c04.flv` 的時候,服務端通過302調度方式返回了類似的結果 `http://112.34.23.45/a.meipei.com/m/c04.flv` ,指定了ip的url,這時客戶端並不知道跳轉的邏輯,因為http請求都是在 ffmpeg 內部進行的。這個時候再設置了Host,就會出現` http://112.34.23.45/a.meipai.com/a.meipai.com/m/c04.flv` 中間有兩個 host 的情況,導致服務端無法解析的 bug。這種情況也是在中途測試的時候偶爾發生的,目前沒有比較好的解決方案,除非讓服務端採用不下發302跳轉,但這樣就不通用了,會給將來留下隱患,所以這種簡單的方案不可行。

方案B

還有一種方案就是經常會用到的設置函數指針的方式,在 ffmpeg 中的 `tcp.c`中用函數指針替換掉 `getaddreinfo` 方法,因為這個方法就是實際解析 DNS的方法,比如下面代碼:

if(my_getaddreinfo) {

ret = my_getaddreinfo(hostname, portstr, &hints, &ai);

} else {

ret = getaddrinfo(hostname, portstr, &hints, &ai);

}

在` my_getaddreinfo` 方法中,可以調用 DNS SDK的解析方法,獲取到ip,然後填充到`ai`裡面,就實現了我們的需求。這種方案的優勢很明顯,就是靈活,容易擴展,而且沒有什麼風險。不過有個劣勢是需要修改ffmpeg源碼,這對於一個大的APP裡面,有多個功能共用一個 `ffmpeg` 庫的情況來講,需要增加很多測試成本。

總體來說,DNS優化後,根據線上的數據首屏時間能減少 100ms~300ms 左右,特別是針對很多首次打開,或者DNS本地緩存過期的情況下,能有很好的優化效果。

2,TCP連接耗時的優化解析

TCP 連接耗時,這個耗時可優化的空間主要是針對建連節點鏈路的優化,主要受限於三個因素影響:用戶自身網絡條件、用戶到 CDN 邊緣節點中間鏈路的影響、CDN 邊緣節點的穩定性。因為用戶網絡條件有比較大的不可控性,所以優化主要會在後面兩個點。我們這邊會結合著用戶所對應的城市、運營商的情況,同時結合著服務端的 CDN 多融合調度體系,可以給用戶下發更合適的 CDN 服務域名,然後通過 HTTPDNS SDK 來優化 DNS 解析的結果。同時對於一些用戶被解析到比較偏遠的節點,或者質量不穩定的節點,那麼我們會通過監控機制來發現,並推動做些優化。。

3,http響應耗時的優化解析

目前 HTTP 響應耗時分兩種情況:1. 如果 CDN 節點沒有緩存流,CDN收到HTTP請求後,就需要回源站去拉流,請求響應,並等待源站的響應結果。這個耗時就比較久了,一般是400ms左右,這塊和CDN內部的架構有關,有時更久,達到幾秒的情況都有,所以這種情況,一般需要推動CDN廠商做一些優化;2. 如果 CDN 節點有緩存流,CDN 收到 HTTP 請求後,會理解返回響應頭部,一般是在100ms 以內,響應很快。這塊比較受限於 CDN 邊緣節點分發策略,不同的 CDN 廠商的表現會有些差異,在端層面可做的東西較少,所以主要是推動多 CDN 的融合策略來提升更好的體驗。

4,音視頻流探測耗時的優化解析

音視頻流的探測耗時,在 ffmpeg 中可以對應函數 `avformat_find_stream_info`函數。在 ijkplayer 的實現中,這個方法的耗時一般會比較久。在 ffmpeg 中的`utils.c` 文件中的函數實現中有一行代碼是 `int fps_analyze_framecount = 20;`,這行代碼的大概用處是,如果外部沒有額外設置這個值,那麼 `avformat_find_stream_info ` 需要獲取至少20幀視頻數據,這對於首屏來說耗時就比較長了,一般都要1s左右。而且直播還有實時性的需求,所以沒必要至少取20幀。這裡就有優化空間,可以去掉這個條件。設置方式:

av_dict_set_int(&ffp->format_opts, "fpsprobesize", 0, 0);

這樣,`avformat_find_stream_info ` 的耗時就可以縮減到 100ms 以內。

5,buffer緩衝耗時的優化解析

這部分是純粹看播放器內部邏輯的實現,因為我們是基於ijkplayer來修改的,就以 ijkplayer 來講。先點出需要優化的兩個地方:1. BUFFERING_CHECK_PER_MILLISECONDS 值需要降低,2.MIN_MIN_FRAMES 值需要降低,3. CDN配置快啟優化。下面具體分析:

  • BUFFERING_CHECK_PER_MILLISECONDS

這部分邏輯主要是在ijkplayer工程中`ff_ffplay.c`文件中的`read_thread`方法中。用到的地方只有一處:

#define BUFFERING_CHECK_PER_MILLISECONDS (300)


if (ffp->packet_buffering) {

io_tick_counter = SDL_GetTickHR;

if (abs((int)(io_tick_counter - prev_io_tick_counter)) > BUFFERING_CHECK_PER_MILLISECONDS){

prev_io_tick_counter = io_tick_counter;

ffp_check_buffering_l(ffp);

}

}

從這個代碼邏輯中可以看出,每次調用 `ffp_check_buffering_l` 去檢查 buffer是否滿足條件的時間間隔是 500ms 左右,如果剛好這次只差一幀數據就滿足條件了,那麼還需要再等 500ms 才能再次檢查了。這個時間,對於直播來說太長了。我們當前的做法是降低到 50ms,理論上來說可以降低 150ms 左右,根據我們線上灰度的數據來看,平均可以減少 200ms 左右,符合預期值。

  • MIN_MIN_FRAMES

這部分代碼實現是在`ffp_check_buffering_l(ffp)`函數中。

#define MIN_MIN_FRAMES 10


if (is->buffer_indicator_queue && is->buffer_indicator_queue->nb_packets > 0) {

if ( (is->audioq.nb_packets > MIN_MIN_FRAMES || is->audio_stream < 0 || is->audioq.abort_request)

&& (is->videoq.nb_packets > MIN_MIN_FRAMES || is->video_stream < 0 || is->videoq.abort_request)) {

printf("ffp_check_buffering_l buffering end \n");

ffp_toggle_buffering(ffp, 0);

}

}

這裡大概的意思需要緩衝的數據至少要有 11 幀視頻,和 11 個音頻數據包,才能離開緩衝區,開始播放。我們知道音頻數據很容易滿足條件,因為如果採樣率是 44.1k 的採集音頻話,那麼1s,平均有44個音頻包。11 個音頻包,相當於0.25s 數據。但對於視頻,如果是24幀的幀率,至少需要0.4s左右的數據,對於大部分 android 直播來說,因為美顏、AR 方面的處理消耗,所以他們的採集編碼幀率只有10~15s,那麼就需要接近1s的數據,這個耗時太長。緩衝區裡需要怎麼多數據,但實際上播放器已經下載了多少數據呢?我們深入 ff_ffplay.c 源碼可以看到視頻解碼後會放到一個 frame_queue 裡面,用於渲染數據。可以看到視頻數據的流程是這樣的:下載到緩衝區->解碼->渲染。其中渲染的緩衝區就是 frame_queue。下載的數據會先經過解碼線程將數據輸出到 frame_queue 中,然後等 frame_queue 隊列滿了,才留在緩衝隊列中。在 ff_ffplay.c 中,可以找到如下代碼:

#define VIDEO_PICTURE_QUEUE_SIZE_MIN (3)

#define VIDEO_PICTURE_QUEUE_SIZE_MAX (16)

#define VIDEO_PICTURE_QUEUE_SIZE_DEFAULT (VIDEO_PICTURE_QUEUE_SIZE_MIN)



ffp->pictq_size = VIDEO_PICTURE_QUEUE_SIZE_DEFAULT; // option


/* start video display */

if (frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1) < 0)

goto fail;

所以目前來看,如果設置10,播放器開始播放時至少有14幀視頻。對於低幀率的視頻來說,也相當大了。在實踐中我們把它調整到5,首屏時間減少了300ms左右,並且卡頓率只上升了2個百分點左右。

  • CDN邊沿優化

CDN 邊沿的優化主要包括 GOP 緩存技術及快啟優化技術。這項兩項技術基本原理是通過快速下發足夠的視頻幀以填充滿播放器的緩衝區從而讓播放器在最短的時間內達到播放條件以優化首屏時間。視頻緩存會以完整 GOP 為單位,這個主要是為了防止視頻出現花屏,快啟優化則是會在 GOP 緩存基本上根據播放器緩衝區大小設定一定的 GOP 數量用於填充播放器緩衝區。

這個優化項並不是客戶端播放器來控制的,而是 CDN 下發視頻數據的帶寬和速度。因為緩衝區耗時不僅跟緩衝需要的幀數有關,還跟下載數據的速度優化,以網宿 CDN 為例,他們可以配置快啟後,在拉流時,前面緩存1s 的數據,服務端將以 5 倍於平時帶寬的速度下發。這樣的效果除了首屏速度跟快以外,首屏也會更穩定,因為有固定 1s 的緩存快速下發。這個優化的效果是平均可以更快 100ms 左右。

四、小結

至此,美拍直播的首屏效果,已經基本跟業界主流直播效果相當,後面我們將在穩定性、卡頓率和卡頓時間上面做進一步優化。

需要注意的是:基礎數據的統計是一切優化的基礎。比如首屏時間優化的一個最基本的大前提就是需要有直播播放情況的各個階段的統計數據,這在我們工作開展的前期是不完善的,比如,DNS 的耗時和 http 響應的耗時。這個因為種種原因導致一直都沒有上報上來,所以最初是無法精準定位,只有一個大概的時間。還有一些更致命的問題是統計數據的不準確,因為歷史原因導致數據的準確性不夠,所以往往會因為錯誤的數據導致錯誤的分析。因此,我們需要重視基礎數據統計的準確性和完善程度。

美圖招人

2017年發展速度最快的行業是什麼?區塊鏈和人工智能!

年終獎已經落袋了,是不是要考慮下給自己更好的一個機會呢?嘗試在未來的人工智能或區塊鏈方面取得一定成就呢?

作為人工智能見長的美圖公司,開始踏足區塊鏈,同時在人工智能領域依然期望將更多的算法落實到應用中去。在此需要更多的技術同學一起取得更多的成就。重要的崗位需求如下:

  • GO 系統研發工程師:主要做區塊鏈、存儲、即時通訊、流媒體等方向。

  • 視頻算法工程師:視頻分類算法;視頻編解碼算法等領域的研究以及落地

  • JAVA 系統研發工程師:主要做的方向核心業務方向研發,區塊鏈相關係統研發等

  • C/C++ 系統研發工程師:主要方向為存儲、緩存、nginx、區塊鏈等方向

相關崗位不限高級、中級、初級,工作地點不限北京、廈門、深圳。勾搭郵箱:[email protected]


分享到:


相關文章: