如何實現婚戀app原始碼中直播首屏載入優化?

雲豹科技程式設計師發表於2021-11-08
婚戀app原始碼開發中,直播首屏載入時間指的是進入直播間時從播放器載入到第一幀畫面渲染出來的時間,這個時間是直播體驗中的一項重要的指標。這篇文章就簡要介紹一下優化直播首屏載入時間的一些經驗。

客戶端業務側優化

說到優化,首先要看婚戀app原始碼客戶端上進入直播間的業務場景是什麼樣的?一般而言,都是從一個直播列表頁面,點選某一個直播卡片(Cell)即進入直播間。這個過程中,資料流是怎麼走的呢?最簡單的做法是,從婚戀app原始碼直播列表頁點選某個直播卡片到直播間後,從伺服器請求直播流地址以及各種直播間資訊(主播資訊、聊天資訊、點贊資訊、禮物資訊等等),拿到直播流地址後,交給播放器播放。
在這個過程中,我們可以看到播放器必須等到進入婚戀app原始碼直播間請求到直播流地址後才能開始播放,這個時間點其實是可以提前的:我們可以在婚戀app原始碼直播列表頁就拿到每個直播間對應的直播流地址,在進入直播間時直接傳過去,這樣一進入直播間播放器就可以拿著直播流地址開始播放了,省去了從伺服器請求直播流地址的時間(雖然這個時間可能沒多少)。
甚至,我們可以在婚戀app原始碼直播列表頁當滑到一個卡片就讓播放器拿著直播流地址預載入,進入直播間時則直接展示畫面。

如何實現婚戀app原始碼中直播首屏載入優化?

另外,婚戀app原始碼客戶端業務側還可以在進入直播間之前通過 HTTPDNS 來選擇網路情況最好的 CDN 節點,在進入直播間時從最好的節點拉取直播流播放從而優化網路載入的時間,加快首屏渲染。

流媒體伺服器側優化

協議的選擇
當前流行的直播拉流協議主要有 RTMP 和 HTTP-FLV。經過大量的測試發現,婚戀app原始碼移動端拉流時在相同的 CDN 策略以及播放器控制策略的條件下,HTTP-FLV 協議相比 RTMP 協議,首屏時間要減少 300~400ms 左右。主要是在 RTMP 協議建連過程中,與服務端的互動耗時會更久。所以我們應該優先使用 HTTP-FLV 協議。
服務端 GOP 快取
除了婚戀app原始碼客戶端業務側的優化外,我們還可以從流媒體伺服器側進行優化。我們都知道直播流中的影像幀分為:I 幀、P 幀、B 幀,其中只有 I 幀是能不依賴其他幀獨立完成解碼的,這就意味著當播放器接收到 I 幀它能馬上渲染出來,而接收到 P 幀、B 幀則需要等待依賴的幀而不能立即完成解碼和渲染,這個期間就是「黑屏」了。
所以,在婚戀app原始碼伺服器端可以通過快取 GOP(在 H.264 中,GOP 是封閉的,是以 I 幀開頭的一組影像幀序列),保證播放端在接入直播時能先獲取到 I 幀馬上渲染出畫面來,從而優化首屏載入的體驗。
這裡有一個 IDR 幀的概念需要講一下,所有的 IDR 幀都是 I 幀,但是並不是所有 I 幀都是 IDR 幀,IDR 幀是 I 幀的子集。I 幀嚴格定義是幀內編碼幀,由於是一個全幀壓縮編碼幀,通常用 I 幀表示「關鍵幀」。IDR 是基於 I 幀的一個擴充套件,帶了控制邏輯,IDR 影像都是 I 幀影像,當解碼器解碼到 IDR 影像時,會立即將參考幀佇列清空,將已解碼的資料全部輸出或拋棄。重新查詢引數集,開始一個新的序列。這樣如果前一個序列出現重大錯誤,在這裡可以獲得重新同步的機會。IDR 影像之後的影像永遠不會使用 IDR 之前的影像的資料來解碼。在 H.264 編碼中,GOP 是封閉式的,一個 GOP 的第一幀都是 IDR 幀。

如何實現婚戀app原始碼中直播首屏載入優化?

通常我們可以在婚戀app原始碼的 CDN 邊緣節點做 GOP 快取。
服務端快速下發策略
快速啟動優化則是會在 GOP 快取基本上根據婚戀app原始碼播放器緩衝區大小設定一定的 GOP 數量用於填充播放器緩衝區。
這個優化項並不是客戶端播放器來控制的,而是在婚戀app原始碼的 CDN 服務端來控制下發視訊資料的頻寬和速度。因為緩衝區耗時不僅跟緩衝需要的幀數有關,還跟下載資料的速度優化,以網宿 CDN 為例,他們可以配置快速啟動後,在拉取直播流時,服務端將以 5 倍於平時頻寬的速度下發前面快取的 1s 的資料,這樣的效果除了首屏速度更快以外,首屏秒開也會更穩定,因為有固定 1s 的快取快速下發。這個優化的效果可以使首屏秒開速度提升 100ms 左右。

直播播放器側優化

耗時分析
HTTP-FLV 協議就是專門拉去 FLV 檔案流的 HTTP 協議,所以它的請求流程就是一個 HTTP 的下載流程,如下圖:

如何實現婚戀app原始碼中直播首屏載入優化?

從上圖中可以看出,婚戀app原始碼中的直播首屏耗時的組成主要以下基本組成:
  • DNS 解析耗時
  • TCP 建連耗時
  • HTTP 響應耗時
  • 音視訊流探測耗時
  • Buffer 填充耗時
下面我們來分別從這幾個方面討論如何優化。
優化 DNS 解析耗時
DNS 解析是婚戀app原始碼網路請求的第一步,在我們用基於 FFmpeg 實現的播放器 ffplay 中,所有的 DNS 解析請求都是 FFmpeg 呼叫 getaddrinfo 方法來獲取的。
我們如何在 FFmpeg 中統計 DNS 耗時呢?
可以在 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();
如果在沒有快取的情況下,實測發現一次域名的解析會花費至少 300ms 左右的時間,有時候更長,如果本地快取命中,耗時很短,幾個 ms 左右,可以忽略不計。快取的有效時間是在DNS 請求包的時候,每個域名會配置對應的快取 TTL 時間,這個時間不確定,根據各域名的配置,有些長有些短,不確定性比較大。
為什麼 DNS 的請求這麼久呢?一般理解,DNS 包的請求,會先到附近的運營商的 DNS 伺服器上查詢,如果沒有,會遞迴到根域名伺服器,這個耗時就很久。一般如果請求過一次,這些伺服器都會有快取,而且其他人也在不停的請求,會持續更新,下次再請求的時候就會比較快。
在測試婚戀app原始碼的DNS 請求的過程中,有時候通過抓包發現每次請求都會去請求 A 和 AAAA 查詢,這是去請求 IPv6 的地址,但由於我們的域名沒有 IPv6 的地址,所以每次都要回根域名伺服器去查詢。為什麼會請求 IPV6 的地址呢,因為 FFmpeg 在配置 DNS 請求的時候是按如下配置的:
hints.ai_family = AF_UNSPEC;
它是一個相容 IPv4 和 IPv6 的配置,如果修改成 AF_INET,那麼就不會有 AAAA 的查詢包了。通過實測發現,如果只有 IPv4 的請求,即使是第一次,也會在 100ms 內完成,後面會更短。這裡是一個優化點,但是要考慮將來相容 IPv6 的問題。
DNS 的解析一直以來都是婚戀app原始碼網路優化的首要問題,不僅僅有時間解析過長的問題,還有小運營商 DNS 劫持的問題。採用 HTTPDNS 是優化 DNS 解析的常用方案,不過 HTTPDNS 在部分地區也可能存在準確性問題,綜合各方面可以採用 HTTPDNS 和 LocalDNS 結合的方案,來提升解析的速度和準確率。大概思路是,婚戀app原始碼啟動的時候就預先解析我們指定的域名,因為拉流域名是固定的幾個,所以完全可以先快取在婚戀app原始碼本地。然後會根據各個域名解析的時候返回的有效時間,過期後再去解析更新快取。至於 DNS 劫持的問題,如果 LocalDNS 解析出來的 IP 無法正常使用,或者延時太高,就切換到 HTTPDNS 重新解析。這樣就保證了每次真正去拉流的時候,DNS 解析的耗時幾乎為 0,因為可以定時更新快取池,使每次獲得的 DNS 都是來自快取池。
那麼怎麼去實現 HTTPDNS 呢?
方案一:IP 直連。
假設原直播流的 URL 是:。假設從 HTTPDNS 服務獲取的 這個 Host 對應的 IP 是:192.168.1.1。那麼處理後的 URL 是:http://192.168.1.1/abc.mp4。如果直接用這個 URL 去發起 HTTP 請求,有些情況可以成功,但很多情況是不行的。如果這個 IP 的機器只部署了 對應的服務,就能解析出來,如果有多個域名的服務,CDN 節點就無法正確的解析。這個時候一般需要設定 HTTP 請求的 header 裡面的 Host 欄位。
AVDictionary **dict = ffplayer_get_opt_dict(ffplayer, opt_category);
av_dict_set(dict, "headers", "Host: ", 0);
但是這個方案有兩個問題:
1)婚戀app原始碼服務端採用 302/307 跳轉的方式排程資源,則 IP 直連會有問題。
如果在客戶端發出請求的時候,服務端是通過 302/307 排程方式返回直播資源的真實地址,這時 IP 直連會有問題。因為婚戀app原始碼客戶端並不知道跳轉邏輯,而客戶端做了 IP 直連,用的是  獲取到的直連 IP 並替換成了,這個請求到達伺服器,伺服器又沒有對應的資源,則會導致錯誤。這種情況可以讓服務端採用不下發 302 跳轉的方式,但這樣就不通用了,會給將來留下隱患。所以常見的做法是做一層播控服務,客戶端請求播控服務獲取到實際的播放地址以及各種其他的資訊,然後再走 IP 直連就沒問題。
2)使用 HTTPS 時,IP 直連會有問題。
這種方案在使用 HTTPS 時,是會失敗的。因為 HTTPS 在證照驗證的過程,會出現 domain 不匹配導致 SSL/TLS 握手不成功。這時候的方案參考 HTTPS(含SNI)業務場景“IP直連”方案說明 和 iOS HTTPS SNI 業務場景“IP直連”方案說明。
方案二:替換 FFmpeg 的 DNS 實現。
另一種方案是替換原來的 DNS 解析的實現。在 FFmpeg 中即替換掉 tcp.c 中 getaddreinfo 方法,這個方法就是實際解析 DNS 的方法,比如下面程式碼:
if (my_getaddreinfo) {
    ret = my_getaddreinfo(hostname, portstr, &hints, &ai);
} else {
    ret = getaddrinfo(hostname, portstr, &hints, &ai);
}
在 my_getaddreinfo 中可以自己實現 HTTPDNS 的解析邏輯從而優化原來的 DNS 解析速度。
總體來說,DNS 優化後,婚戀app原始碼的直播首屏時間能減少 100ms~300ms 左右,特別是針對很多首次開啟,或者 DNS 本地快取過期的情況下,能有很好的優化效果。
優化 TCP 建連耗時
TCP 建連耗時在這裡即呼叫 Socket 的 connect 方法建立連線的耗時,它是一個阻塞方法,它會一直等待 TCP 的三次握手完成。它直接反應了婚戀app原始碼客戶端到 CDN 伺服器節點的點對點延時情況,實測在一般的 Wifi 網路環境下耗時在 50ms 以內,基本是沒有太大的優化空間,不過它的時間反應了客戶端的網路情況或者客戶端到節點的網路情況。
要統計這段耗時,可以在 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();
TCP 連線耗時可優化的空間主要是針對建連節點鏈路的優化,主要受限於三個因素影響:使用者自身網路條件、使用者到 CDN 邊緣節點中間鏈路的影響、婚戀app原始碼CDN 邊緣節點的穩定性。因為使用者網路條件有比較大的不可控性,所以優化主要會在後面兩個點。可以結合著使用者所對應的城市、運營商的情況,同時結合優化服務端的 CDN 排程體系,給使用者下發更合適的 CDN 服務域名,然後通過 HTTPDNS SDK 來優化 DNS 解析的結果。
優化 HTTP 響應耗時
HTTP 響應耗時是指客戶端發起一個 HTTP Request 請求,然後等待 HTTP 響應的 Header 返回這部分耗時。直播拉流 HTTP-FLV 協議也是一個 HTTP 請求,婚戀app原始碼客服端發起請求後,服務端會先將 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();
通常 婚戀app原始碼CDN 的快取命中策略是與訪問資源的 URL 有關。如果命中策略是 URL 全匹配,那麼就要儘量保證 URL 的變化性較低。比如:儘量不要在 URL 的引數中帶上隨機性的值,這樣會造成 CDN 快取命中下降,從而導致不斷回源,這樣訪問資源耗時也就增加了。當然這樣就失去了一些靈活性。
CDN 方面其實可以提供一些配置策略,比如:根據域名可配置對其快取命中策略忽略掉某些引數。這樣就能保證一定的靈活性了。
優化音視訊流探測耗時
當婚戀app原始碼做直播業務時,播放端需要一個播放器來播放視訊流,當一個播放器支援的視訊格式有很多種時,問題就來了。一個視訊流來了,播放器是不清楚這個視訊流是什麼格式的,所以它需要去探測到一定量的視訊流資訊,去檢測它的格式並決定如何去處理它。這就意味著在播放視訊前有一個資料預讀過程和一個分析過程。但是對於婚戀app原始碼的直播業務來說,我們的提供的直播方案通常是固定的,這就意味著視訊流的格式通常是固定的,所以一些資料預讀和分析過程是不必要的。在直播流協議格式固定的情況下,只需要讀取固定的資訊即可開始播放。這樣就縮短了資料預讀和分析的時間,使得播放器能夠更快地渲染出首屏畫面。

如何實現婚戀app原始碼中直播首屏載入優化?

基於 FFmpeg 實現的播放器,在播放視訊時都會呼叫到一個 avformat_find_stream_info (libavformat/utils.c) 函式,該函式的作用是讀取一定長度的碼流資料,來分析碼流的基本資訊,為視訊中各個媒體流的 AVStream 結構體填充好相應的資料。這個函式中做了查詢合適的解碼器、開啟解碼器、讀取一定的音視訊幀資料、嘗試解碼音視訊幀等工作,基本上完成了解碼的整個流程。這時一個同步呼叫,在不清楚視訊資料的格式又要做到較好的相容性時,這個過程是比較耗時的,從而會影響到婚戀app原始碼播放器首屏秒開。
可以在 ijkplayer 的工程中 ff_ffplay.c 檔案中的 read_thread 方法統計其耗時:
int64_t start = av_gettime();
avformat_find_stream_info(ic, opts);
int64_t end = av_gettime();
在外部可以通過設定 probesize 和 analyzeduration 兩個引數來控制該函式讀取的資料量大小和分析時長為比較小的值來降低 avformat_find_stream_info 的耗時,從而優化婚戀app原始碼播放器首屏秒開。但是,需要注意的是這兩個引數設定過小時,可能會造成預讀資料不足,無法解析出碼流資訊,從而導致播放失敗、無音訊或無視訊的情況。所以,在婚戀app原始碼服務端對視訊格式進行標準化轉碼,從而確定視訊格式,進而再去推算 avformat_find_stream_info 分析碼流資訊所相容的最小的 probesize 和 analyzeduration,就能在保證播放成功率的情況下最大限度地區優化首屏秒開。
在我們能控制視訊格式達到標準化後,我們可以直接修改 avformat_find_stream_info 的實現邏輯,針對該視訊格式做優化,進而優化首屏秒開。
在 FFmpeg 中的 utils.c 檔案中的函式實現中有一行程式碼是 int fps_analyze_framecount = 20;,這行程式碼的大概用處是,如果外部沒有額外設定這個值,那麼 avformat_find_stream_info 需要獲取至少 20 幀視訊資料,這對於首屏來說耗時就比較長了,一般都要 1s 左右。而且直播還有實時性的需求,所以沒必要至少取 20 幀。你可以試試將這個值初始化為 0 看看效果。在開發中,我們可以去掉這個條件來實現優化:
av_dict_set_int(&ffp->format_opts, "fpsprobesize", 0, 0);
這樣,avformat_find_stream_info 的耗時就可以縮減到 100ms 以內。
甚至,我們可以進一步直接去掉 avformat_find_stream_info 這個過程,自定義完成解碼環境初始化。
優化 Buffer 填充耗時
緩衝耗時是指播放器的緩衝的資料達到了預先設定的閾值,可以開始播放視訊了。這個值是可以動態設定的,所以不同的設定給首屏帶來的影響是不一樣的。
緩衝耗時的統計方法,不像前面幾個那麼簡單,因為它涉及到的程式碼有多處,所以需要在多個地方計時。開始計時可以直接從 avformat_find_stream_info 後面開始,結束計時可以在第一幀視訊渲染出來的時候結束。
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();
}
優化一:調整 BUFFERING_CHECK_PER_MILLISECONDS 設定。
婚戀app原始碼緩衝區填充耗時跟播放器裡面的一個設定 BUFFERING_CHECK_PER_MILLISECONDS 值有關,因為播放器 check 緩衝區的資料是否達到目標值不是隨意檢測的,因為 check 本身會有一定的浮點數運算,所以 ijkplayer 最初給他設定了 500ms 時間間隔去定時檢查,這個時間明顯比較大,所以會對緩衝耗時有比較大的影響。可以把這個值改小一些。
#define BUFFERING_CHECK_PER_MILLISECONDS        (500)
這個值會在 ijkplayer 工程中 ff_ffplay.c 檔案中的 read_thread 方法中用到:
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 才能再次檢查了。這個時間,對於婚戀app原始碼的直播來說太長了。我們當前的做法是降低到 50ms,從實測效果來看平均可以減少 200ms 左右。
優化二:調整 MIN_MIN_FRAMES 設定。
另外一個跟緩衝區相關的設定是 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 個音訊資料包,才能離開緩衝區開始播放。婚戀app原始碼音訊資料很容易滿足條件,因為如果取樣率是 44.1k 的音訊,那麼 1s 的資料平均有 44 個音訊包,0.25s 的資料就能達到 11 個音訊包。但對於視訊,如果是 24 幀的幀率,至少需要 0.4s 左右的資料才能達到 11 幀。如果視訊採集的編碼幀率較低(美顏、AR 情況下由於處理消耗較大可能採集的幀率較低),只有 10-15,那就需要接近 1s 的資料才能達到 11 幀,緩衝區需要這麼多資料才能開始播放,這個時長太大。
緩衝區裡達到這麼多資料時,實際上播放器已經下載了多少資料呢?我們深入 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;
所以目前來看,如果設定 MIN_MIN_FRAMES 為 10,婚戀app原始碼播放器開始播放時至少有 14 幀視訊。對於低幀率的視訊來說,也相當大了。在實踐中我們把它調整到 5,首屏時間減少了 300ms 左右,並且卡頓率只上升了 2 個百分點左右。
本文轉載自網路,轉載僅為分享乾貨知識,如有侵權歡迎聯絡雲豹科技進行刪除處理


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69996194/viewspace-2841148/,如需轉載,請註明出處,否則將追究法律責任。

相關文章