歡迎光臨
每天分享高質量文章

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

導讀:直播行業的競爭越來越激烈,各廠商對用戶體驗的追求也越來越高,這其中首屏時間的體驗尤為重要。本文中美圖的包紅來同學從DNS解析優化、TCP連接耗時、HTTP響應耗時、音視頻流的探測耗時、buffer緩衝的耗時等五個方面非常詳細的解說了美拍直播首屏時間減少50%,達到500ms左右的一個具體優化實踐,對做直播的同學非常有借鑒意義。

隨著移動直播的火爆,大量的業務都有直播需求,這就使直播成了一種基本的配置。在觀看直播過程中,首屏時間是最重要的體驗之一,它的快慢直接影響了用戶對該直播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 的下載流程,如下圖:

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

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(NULL, 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 響應的essay-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 請求的 essay-header裡面的 Host 欄位。一般可以通過以下代碼傳遞給 ffmpeg 內部,這個引數的作用就是填充 http 的Host 頭部,具體的實現,可以 ffmpeg 原始碼,檔案`http.c`中`http_connect` 方法中。

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

    av_dict_set(dict, “essay-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]

相關閱讀

高可用架構

改變互聯網的構建方式

赞(0)

分享創造快樂