平時(shí)看直播大家一般比較關(guān)心視頻質(zhì)量和直播延遲,可能在有些場景對直播延遲要求沒那么高,但是在有互動(dòng)的直播中直播延遲是非常重要的,不然每次互動(dòng)都有幾十秒延遲,這是不能接受的。

上圖中展示了不同直播方案的延遲,可以發(fā)現(xiàn)延遲最低的是 WebRTC,不過 WebRTC 一般不能支持大規(guī)模用戶同時(shí)觀看直播。這篇文章將介紹圖中的 Low-Latency HLS 直播方案,可以看到它是在 3 秒左右延遲的位置。
HLS
HLS 是蘋果公司在 2009 年提出的基于 HTTP 的流媒體傳輸協(xié)議。
在最開始蘋果推薦一個(gè) HLS 視頻片段時(shí)長是 10 秒(現(xiàn)在推薦 6 秒),在倒數(shù)第三個(gè)視頻片段開始播放,如果按照這個(gè)推薦配置,用 HLS 開直播的延遲將在 30 秒往上,也就是上方延遲圖中的最高延遲位置。
要想降低延遲,一個(gè)非常簡單的方法就是直接縮短一個(gè)視頻片段的時(shí)長,比如將一個(gè)視頻片段縮短成 3 秒,使用這中非常短的視頻片段,直播延遲將可以降低到 10 秒左右。
當(dāng)然 10 秒左右延遲還是挺高的,于是就有人想出了一個(gè)社區(qū)低延遲方案,它被稱為 LHLS。再后來蘋果推出了官方的低延遲解決方案,它被稱為 LLHLS。下面將詳細(xì)介紹這兩種方案。
LHLS
LHLS 也被稱為 CL-HLS,它并不是標(biāo)準(zhǔn)規(guī)范,而是社區(qū)驅(qū)動(dòng)的 HLS 低延遲方案,最早是由 Periscope 團(tuán)隊(duì)在 2017 年發(fā)布一篇博客 Introducing LHLS Media Streaming 提出這個(gè)概念。后面由 hls.js 與一些流媒體廠商一起合作,規(guī)范這一方案 Low-latency HLS Streaming 。
實(shí)現(xiàn)原理
LHLS 是怎么實(shí)現(xiàn)低延遲直播的呢?大家可以看下面這張圖,其中一個(gè)視頻片段是 8 秒。

現(xiàn)在一共生成了 3 個(gè)視頻片段,第 4 個(gè)視頻片段已生成 3 秒,由于一個(gè)視頻片段只有完全生成才能被下載。所以我們有下面這幾種不同的方法來播放這個(gè)播放列表。
- 最簡單的方法當(dāng)然是從第 1 個(gè)分片開始播放,這樣延遲是 27 秒(3 * 8 + 3)。
- 第二種方案是我從最后一個(gè)分片開始播放,這樣延遲是 11 秒。
- 或者我們等待 5 秒,讓第 4 個(gè)分片生成再播放,這樣延遲是 8 秒。
可以發(fā)現(xiàn)上面這 3 個(gè)方案延遲都挺高,第三個(gè)方案延遲稍微低一點(diǎn)但是起播延遲卻太高了。
LHLS 方案是將一個(gè)視頻片段細(xì)分成一個(gè)個(gè)很小的 Chunk,無需等待一整個(gè)視頻片段生成,每生成一個(gè) Chunk 它就會(huì)被下載到播放器緩存起來。上圖中最后一種方法就是將一個(gè)分片分成一個(gè)個(gè) 1 秒小 Chunk,這樣我們就得到了 3 秒延遲的直播。
具體到實(shí)際實(shí)現(xiàn)中 LHLS 是使用 HTTP/1.1 的 Chunked transfer encoding 功能,播放器會(huì)保持與服務(wù)器的連接,每當(dāng)服務(wù)器生成一個(gè) Chunk 就會(huì)直接傳遞給播放器,直到一個(gè)視頻片段全部傳輸完畢才會(huì)斷開連接。另外 HTTP 的這個(gè)功能大部分 CDN 都支持。
社區(qū)方案的一個(gè)主要問題是它不好做 ABR 自適應(yīng)碼率切換,因?yàn)榕c服務(wù)器的連接是長連接,客戶端不好估算出當(dāng)前用戶的網(wǎng)絡(luò)帶寬,為了解決這個(gè)問題一般會(huì)用一個(gè)測試文件去測試當(dāng)前網(wǎng)速。
規(guī)范詳情
社區(qū)規(guī)范中一共引入兩個(gè)自定義標(biāo)簽 EXT-X-PREFETCH 和
EXT-X-PREFETCH-DISCONTINUITY。 EXT-X-PREFETCH-DISCONTINUITY 和 EXT-X-DISCONTINUITY 功能一樣,只不過 EXT-X-PREFETCH 上方不能放置 EXT-X-DISCONTINUITY,要把它變成 EXT-X-PREFETCH-DISCONTINUITY。
該規(guī)范完全兼容 HLS 標(biāo)準(zhǔn)規(guī)范,對于支持這一規(guī)范的播放器可以選擇使用它們來低延遲直播,對于不支持播放器會(huì)忽略這些標(biāo)簽,變成高延遲直播。
下面是一個(gè) LHLS M3U8 文件例子。
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z
#EXTINF:2.000
https://foo.com/bar/1.ts
#EXT-X-PROGRAM-DATE-TIME:2018-09-05T21:59:10.531Z
#EXTINF:2.000
https://foo.com/bar/5.ts
#EXT-X-PREFETCH:https://foo.com/bar/6.ts
#EXT-X-PREFETCH:https://foo.com/bar/7.ts
我們可以發(fā)現(xiàn)除了最后兩行,這和普通 M3U8 文件沒有任何區(qū)別。一個(gè) LHLS M3U8 最少包含一個(gè)并且不超過兩個(gè) EXT-X-PREFETCH 標(biāo)簽。EXT-X-PREFETCH 標(biāo)簽后面跟隨著一個(gè) URL,它是還沒有生成的分片。
支持 LHLS 的播放器會(huì)直接發(fā)送兩個(gè) HTTP 請求,去請求 6.ts 和 7.ts,服務(wù)器會(huì)維持這兩個(gè)請求,并不斷發(fā)送 Chunk。
可能有同學(xué)要問,如果 6.ts 連接還沒斷開,但是 7.ts 連接收到數(shù)據(jù)了怎么辦?這時(shí)候播放器就要內(nèi)部保持這些數(shù)據(jù),直到前一個(gè)請求完成。
Twitch 的直播延遲
那么誰在使用 LHLS 低延遲方案呢?上面提到的最早提出這個(gè)方案的 Periscope 團(tuán)隊(duì),它們后面被 Twitter 收購,再然后就被關(guān)停了。
不過國外非常出名的直播平臺(tái) Twitch 正在使用該方案。它并沒有按照社區(qū)規(guī)范來實(shí)現(xiàn),而是加入了一些自定義的東西,比如它把 EXT-X-PREFETCH 換成了 EXT-X-TWITCH-PREFETCH,而且 EXT-X-DISCONTINUITY 可以直接應(yīng)用在 EXT-X-TWITCH-PREFETCH 上。
那么 Twitch 的直播延遲是多少呢?我決定自己去開個(gè)直播間試試。
然而在第一步注冊賬號就卡住了。

通過 OBS 推流后,進(jìn)入自己的直播間,可以看到我推的 1080P 直播,延遲是 5 秒左右。
LL-HLS
LL-HLS 表示是蘋果官方版的低延遲方案,它也被稱為 ALHLS。在 2019 年 WWDC 上蘋果介紹了他們官方的 HLS 低延遲解決方案,蘋果發(fā)布的低延遲方案并沒有借鑒社區(qū)低延遲方案的成果,而是重新設(shè)計(jì)了一套低延遲方案。蘋果的目標(biāo)是 1 到 2 秒低延遲,支持大規(guī)模用戶的直播,并且可以完全向下兼容。
看了蘋果的方案后,大家情緒都不穩(wěn)定了,因?yàn)樘O果的方案中提到需要 HTTP2 的 push 功能,但是這個(gè)功能大部分的 CDN 都沒有實(shí)現(xiàn),并且這個(gè)功能和傳統(tǒng)方案有很大的差別,實(shí)現(xiàn)起來也非常頭疼。到最后蘋果終于決定將 HTTP2 push 功能移出了規(guī)范,加入 EXT-X-PRELOAD-HINT 標(biāo)簽代替該功能。
LLHLS 方案相比 LHLS 復(fù)雜度大大的提高,LLHLS 中一共加入了 5 大修改,分別是 Partial Segment、請求長連接、增量更新、預(yù)加載和快速碼率切換,下面將詳細(xì)介紹這些功能。
另外由于蘋果推出了官方 HLS 低延規(guī)規(guī)范,于是社區(qū)立馬拋棄了社區(qū)規(guī)范,hls.js 也刪除了相關(guān)代碼,去實(shí)現(xiàn) LLHLS 規(guī)范。
Partial Media Segment
LLHLS 將一個(gè)視頻片段再細(xì)分稱為小分段,一個(gè)視頻片段由多個(gè)小分段組成。原先需要等待一個(gè)視頻片段完全被生成才能下載,比如一個(gè)片段是 6 秒種,客戶端就需要等待 6 秒這個(gè)分片被生成才能下載它。
現(xiàn)在服務(wù)端將一個(gè)片段分成多個(gè)小分段,比如一個(gè)小分段是 200 毫秒,那么一個(gè)視頻片段包含 30 個(gè)小分段,客戶端只需等待 200 毫秒就可以一個(gè)個(gè)下載這些小分段。

可以發(fā)現(xiàn)這種方式和社區(qū)方案非常相似,社區(qū)方案是將一個(gè)視頻分段分成一個(gè)個(gè)小 Chunk,通過 HTTP/1.1 的 Chunked transfer encoding 功能下載到客戶端。而 LLHLS 是將一個(gè)視頻片段分成一個(gè)個(gè)小分段,通過普通 HTTP 請求去下載這些小分段。
與小分段相關(guān)的標(biāo)簽有 EXT-X-PART-INF 和 EXT-X-PART 兩個(gè)標(biāo)簽。
EXT-X-PART-INF
EXT-X-PART-INF 提供了播放列表中小分段的信息,如果播放列表中存在 EXT-X-PART 標(biāo)簽,那么必須提供這個(gè)標(biāo)簽。
這個(gè)標(biāo)簽只有一個(gè)必傳屬性 PART-TARGET,它的值是浮點(diǎn)數(shù),單位是秒。和 EXT-X-TARGETDURATION 標(biāo)簽類型,這個(gè)屬性表示的是小分段的目標(biāo)時(shí)長。
EXT-X-PART
EXT-X-PART 標(biāo)簽與 EXTINF 相似,它是用來聲明一個(gè)小分段,它一共有 5 個(gè)屬性。
- URI 小分段的資源鏈接。
- DURATION 小分段時(shí)長。
- INDEPENDENT 如果小分段中包含關(guān)鍵幀,可以將這個(gè)字段設(shè)置為 YES。
- BYTERANGE 如果要使用 HTTP Range 請求,可以使用該屬性,它的值與 EXT-X-BYTERANGE 標(biāo)簽一樣。
- GAP 如果這個(gè)小分段不可使用,可以將這個(gè)屬性設(shè)置為 YES。
需要注意,如果該標(biāo)簽包含了 GAP=YES 屬性,那么客戶端就不應(yīng)該去請求這個(gè)資源,客戶端需要自己解決如何跳過這個(gè) gap,蘋果播放器的做法是延長上一幀的播放時(shí)長。
下面是一個(gè)完整 LLHLS 播放列表的例子。
#EXTM3U
#EXT-X-TARGETDURATION:4
#EXT-X-VERSION:6
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=1.0,CAN-SKIP-UNTIL=12.0
#EXT-X-PART-INF:PART-TARGET=0.33334
#EXT-X-MEDIA-SEQUENCE:266
#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:13:36.106Z
#EXT-X-MAP:URI="init.mp4"
#EXTINF:4.00008,
fileSequence266.mp4
#EXTINF:4.00008,
fileSequence267.mp4
#EXTINF:4.00008,
fileSequence268.mp4
#EXTINF:4.00008,
fileSequence269.mp4
#EXTINF:4.00008,
fileSequence270.mp4
#EXT-X-PART:DURATION=0.33334,URI="filePart271.0.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.1.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.2.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.3.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.4.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart271.5.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.6.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.7.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.8.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart271.9.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.10.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.11.mp4"
#EXTINF:4.00008,
fileSequence271.mp4
#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z
#EXT-X-PART:DURATION=0.33334,URI="filePart272.a.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.b.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.c.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.d.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.e.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.f.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart272.g.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.h.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.i.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.j.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.k.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.l.mp4"
#EXTINF:4.00008,
fileSequence272.mp4
#EXT-X-PART:DURATION=0.33334,URI="filePart273.0.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart273.1.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart273.2.mp4"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.3.mp4"
#EXT-X-RENDITION-REPORT:URI="../1M/waitForMSN.php",LAST-MSN=273,LAST-PART=2
#EXT-X-RENDITION-REPORT:URI="../4M/waitForMSN.php",LAST-MSN=273,LAST-PART=1
可以發(fā)現(xiàn) LLHLS 播放列表中有非常多的 Part 小分段,為了防止生成太多的小分段,服務(wù)端將會(huì)定期清理老的小分段。
條件請求
之前請求 HLS 播放列表都是客戶端發(fā)起一個(gè)普通的 HTTP GET 請求,然后服務(wù)器返回一個(gè) m3u8 文件。
現(xiàn)在 LLHLS 允許在請求播放列表時(shí)添加查詢條件。服務(wù)器是否支持這些功能,是通過 EXT-X-SERVER-CONTROL 標(biāo)簽設(shè)置,該標(biāo)簽后面跟著一個(gè)屬性列表,來指明服務(wù)器支持哪些條件查詢。
目前 LLHLS 一共支持了 3 個(gè)查詢參數(shù),分別是 _HLS_msn 、 _HLS_part 和 _HLS_skip。通過它們可以實(shí)現(xiàn)不同的功能,具體參數(shù)含義將在下方詳細(xì)介紹。
請求長連接
在 HLS 直播中,我們需要頻繁的去請求播放列表文件去查看是否有新的視頻片段被添加,這樣非常的浪費(fèi)時(shí)間和資源。在 LLHLS 中服務(wù)器可以保持這個(gè)連接不斷開,直到客戶端需要的片段被生成才完成請求。

服務(wù)器支持這一功能,需要 EXT-X-SERVER-CONTROL 標(biāo)簽中的 CAN-BLOCK-RELOAD 屬性為 YES。
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES
要告訴服務(wù)器何時(shí)才完成請求,需要用到 _HLS_msn 和 _HLS_part 兩個(gè)查詢條件。如果只需要服務(wù)器在生成下一個(gè)視頻片段時(shí)才完成請求可以發(fā)送下面這個(gè)請求。
https://llhls.com/playlist.m3u8?_HLS_msn={下一個(gè)片段的 Media Sequence Number}
_HLS_msn 用來控制服務(wù)器播放列表包含了指定片段或指定片段之后的片段才返回請求,_HLS_part 控制服務(wù)器播放列表包含了指定片段的哪個(gè)小分段才返回請求,小分段的下標(biāo)是從 0 開始,比如一個(gè)視頻片段是 6 秒,一個(gè)小分段是 1 秒,那么這個(gè)視頻片段一共由下標(biāo) 0 到 5 的小分段組成。
https://llhls.com/playlist.m3u8?_HLS_msn={下一個(gè)片段的 Media Sequence Number}&_HLS_part={小分段下標(biāo)}
需要注意 _HLS_msn 可以單獨(dú)使用, _HLS_part 必須和 _HLS_msn 一起使用,否則服務(wù)器將會(huì)返回 400 錯(cuò)誤。當(dāng) _HLS_msn 超過最新生成片段太多服務(wù)器也會(huì)返回 400 錯(cuò)誤。
如果播放列表包含 EXT-X-ENDLIST,服務(wù)器將會(huì)忽略 _HLS_part 和 _HLS_msn 兩個(gè)參數(shù)。
播放列表增量更新
在 HLS 直播中,我們每次刷新播放列表都會(huì)包含一些我們已經(jīng)知道的老片段信息。比如第一次請求返回 0、1 和 2 這三個(gè)片段信息,第二次刷新返回 1、2 和 3 這新的片段信息,可以發(fā)現(xiàn) 1 和 2 我們是知道的,其實(shí)無需再包含在播放列表中。
LLHLS 提供了播放列表增量更新功能,我們可以告訴服務(wù)器可以跳過哪些片段,不用將它包含在播放列表中,從而減少傳輸損耗。
要支持增量更新功能,需要 EXT-X-SERVER-CONTROL 標(biāo)簽中包含 CAN-SKIP-UNTIL 屬性。還可以包含必須與 CAN-SKIP-UNTIL 一起使用的 CAN-SKIP-DATERANGES 屬性,它表示是否可以跳過老的 EXT-X-DATERANGE 標(biāo)簽。
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=12.0,CAN-SKIP-DATERANGES=YES
CAN-SKIP-UNTIL 屬性的值是十進(jìn)制浮點(diǎn)數(shù),單位是秒,這個(gè)值至少是目標(biāo)時(shí)長的 6 倍。它表示跳過分段的邊界。
要發(fā)起一個(gè)播放列表增量更新請求,需要包含 _HLS_skip 查詢參數(shù)。
https://llhls.com/playlist.m3u8?_HLS_skip={YES或v2}
_HLS_skip 的值是 YES 或 v2。YES 表示跳過老的片段。v2 表示跳過老的片段和老的 EXT-X-DATERANGE 標(biāo)簽(需要服務(wù)器返回 CAN-SKIP-DATERANGES=YES)。
需要注意當(dāng)客戶端沒有一個(gè)完整的播放列表或當(dāng)前播放列表太久沒更新超過一半的可跳過邊界時(shí)應(yīng)該使用全量查詢而不是增量查詢。
當(dāng)一個(gè)播放列表是增量更新時(shí),播放列表中會(huì)包含一個(gè) EXT-X-SKIP 標(biāo)簽,這個(gè)標(biāo)簽只有兩個(gè)屬性, SKIPPED-SEGMENTS 表示跳過視頻片段數(shù)量和
RECENTLY-REMOVED-DATERANGES 表示跳過了哪些 DATERANGE id。
下面是一個(gè)增量更新的播放列表例子。
#EXTM3U
#EXT-X-TARGETDURATION:4
#EXT-X-VERSION:9
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=1.0,CAN-SKIP-UNTIL=12.0
#EXT-X-PART-INF:PART-TARGET=0.33334
#EXT-X-MEDIA-SEQUENCE:266
#EXT-X-SKIP:SKIPPED-SEGMENTS=3
#EXTINF:4.00008,
fileSequence269.mp4
#EXTINF:4.00008,
fileSequence270.mp4
#EXTINF:4.00008,
fileSequence271.mp4
#EXTINF:4.00008,
fileSequence272.mp4
#EXT-X-PART:DURATION=0.33334,URI="filePart273.0.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart273.1.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart273.2.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart273.3.mp4"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.4.mp4"
#EXT-X-RENDITION-REPORT:URI="../1M/waitForMSN.php",LAST-MSN=273,LAST-PART=3
#EXT-X-RENDITION-REPORT:URI="../4M/waitForMSN.php",LAST-MSN=273,LAST-PART=3
可以發(fā)現(xiàn)上面這個(gè)例子中跳過了 3 個(gè)視頻片段,跳過的視頻片段的 msn 分別是 266、267 和 268。
片段預(yù)加載
LLHLS 中還有視頻片段預(yù)加載功能,它表示一個(gè)視頻片段還沒被創(chuàng)建,但是客戶端去請求它。這個(gè)功能與社區(qū)方案的 EXT-X-PREFETCH 非常相似。
與該功能相關(guān)的標(biāo)簽是 EXT-X-PRELOAD-HINT,它后面跟一個(gè)屬性列表,一共有 4 個(gè)屬性。
- TYPE 屬性有兩個(gè)值,PART 表示是小分段,MAP 表示是媒體初始部分(與 EXT-X-MAP 相似)。
- URI 資源的 url。
- BYTERANGE-START 如果是一個(gè)資源的一部分,這個(gè)屬性用來指定開始部分。
- BYTERANGE-LENGTH 這個(gè)表示資源的字節(jié)長度,與 BYTERANGE-START 配合使用。
當(dāng)客戶端碰到這個(gè)標(biāo)簽時(shí),可以選擇是否直接請求這個(gè)資源,服務(wù)器會(huì)和上面請求長連接中一樣維持這個(gè)請求,直到整個(gè)資源數(shù)據(jù)可用時(shí)才返回資源。當(dāng)然也有可能直接返回 404。
快速碼率切換
使用 HLS 的一個(gè)優(yōu)勢是可以自適應(yīng)碼率切換,根據(jù)當(dāng)前網(wǎng)速、屏幕大小等信息選擇最適合用戶的當(dāng)前環(huán)境碼率的流。在 LLHLS 中蘋果提供了一種可以快速切換碼率的功能。
服務(wù)器通過 EXT-X-RENDITION-REPORT 標(biāo)簽,將主播放列表中與當(dāng)前流相關(guān)的其他碼率的流條件到當(dāng)前 Media 類型的播放列表中,這個(gè)標(biāo)簽一共有 3 個(gè)屬性。
- URI 與當(dāng)前流相關(guān)的其他碼率流的鏈接。
- LAST-MSN 這個(gè)流中最后一個(gè)視頻片段的視頻編號。
- LAST-PART 這個(gè)流中最后一個(gè)小分段的下標(biāo)。
每個(gè)視頻流的 LAST-MSN 和 LAST-PART 可能不一樣,EXT-X-RENDITION-REPORT 標(biāo)簽提供了這些信息,我們就不用去請求那些比較落后的流,這樣可以減少很多不必要的請求。
直播從哪兒開始播放
客戶端面對一個(gè)播放列表,應(yīng)該從哪兒開始播放呢?離主播位置越遠(yuǎn)延遲就越高,離主播當(dāng)前位置越近 Buffer 有太少,容易引起播放卡頓。
上面介紹的 EXT-X-SERVER-CONTROL 標(biāo)簽可以解決這個(gè)問題,這個(gè)標(biāo)簽還有兩個(gè)屬性 HOLD-BACK 和 PART-HOLD-BACK,這兩個(gè)屬性是服務(wù)器推薦的直播開始位置。
- HOLD-BACK 的值是一個(gè)浮點(diǎn)數(shù)秒數(shù),代表服務(wù)器推薦的離播放列表末尾最小距離,它應(yīng)該最小是 3 個(gè)視頻片段目標(biāo)時(shí)長。
- PART-HOLD-BACK 的值是一個(gè)浮點(diǎn)數(shù)秒數(shù),代表服務(wù)器推薦的離播放列表末尾最小距離,它最小是 2 倍的 Part 小分段的目標(biāo)時(shí)長。推薦是 Part 目標(biāo)時(shí)長的 3 倍。
當(dāng)存在 PART-HOLD-BACK 屬性時(shí),客戶端應(yīng)該忽略 HOLD-BACK 屬性。如果播放列表包含 EXT-X-PART-INF 標(biāo)簽,則必須要有 PART-HOLD-BACK 屬性。
如何獲取最新播放列表 CDN Tune-in
CDN 一般會(huì)有緩存,那么如何獲取最新版本的播放列表呢?蘋果給出了一個(gè)解決方案。
- 首先發(fā)送一個(gè)不包含 _HLS_msn 和 _HLS_part 查詢參數(shù)的請求。
- 記錄這次請求的接收時(shí)間和 Age 響應(yīng)頭。如果沒有 Age 響應(yīng)頭那么這次請求應(yīng)該就是最新的版本。
- 設(shè)置變量 goalDuration 去匹配 Age 響應(yīng)頭,如果 Part 目標(biāo)時(shí)長小于 1 秒則 goalDuration 加 1 秒。
- 如果 Age 響應(yīng)頭大于或等于 Part 目標(biāo)時(shí)長,則設(shè)置 currentGoal 等于 goalDuration 加上現(xiàn)在到第一次響應(yīng)的時(shí)間。
- 利用片段目標(biāo)時(shí)長和 Part 目標(biāo)時(shí)長,去估算服務(wù)器應(yīng)該加了多少片段和小分段到播放列表了。
- 利用估算出來的值去發(fā)送帶有 _HLS_msn 和 _HLS_part 查詢條件的請求,就可以獲得最新版本的播放列表了。
當(dāng)然也可以實(shí)現(xiàn)自己的算法來獲取最新版本的播放列表,比如 hls.js 中是這樣計(jì)算 currentGoal 的。
currentGoal = Math.min(cdnAge - partTarget, targetDuration * 1.5)
segments = Math.floor(currentGoal / targetDuration)
parts = Math.round((currentGoal % targetDuration) / partTarget)
它在目標(biāo)時(shí)長的 1.5 倍和 Age 響應(yīng)頭與 Part 目標(biāo)延遲之間差值取最小值,計(jì)算出 currentGoal。然后通過 currentGoal 計(jì)算出 _HLS_msn 和 _HLS_part 兩個(gè)查詢條件的參數(shù)。
推薦時(shí)長設(shè)置
蘋果推薦的一個(gè)視頻片段的時(shí)長是 6 秒鐘,一個(gè) Part 小分段時(shí)長推薦設(shè)置為 1 秒鐘,GOP 推薦設(shè)置為 1 到 2 秒。推薦最少在有 3 個(gè) Part 目標(biāo)時(shí)長位置開始播放。
1.7 秒的直播延遲
由于 LLHLS 相對還比較新,我還不知道哪個(gè)直播平臺(tái)有使用,不過那些流媒體服務(wù)廠商都實(shí)現(xiàn)了 LLHLS。
在 Wowza 的低延遲解決方案中就包括蘋果低延遲 HLS 解決方案,剛好他們官網(wǎng)演示視頻中就有展示直播延遲。

演示視頻中將 Part 小分段時(shí)長設(shè)置為 0.4 秒,PART-HOLD-BACK 設(shè)置為 0.8 秒。然后使用支持 LLHLS 的 THEOplayer 來播放直播流??梢园l(fā)現(xiàn)是只有 1.7 秒的延遲。
總結(jié)
本文介紹了兩種 HLS 直播方案,LHLS 社區(qū)方案和 LLHLS 官方方案,它們都可以提供不錯(cuò)的低延遲直播。要推薦的話當(dāng)然是官方的 LLHLS 方案,因?yàn)樗墓δ鼙容^多,而且蘋果的設(shè)備都會(huì)去支持它,官方也會(huì)不斷維護(hù)擴(kuò)展這個(gè)方案。另外在設(shè)置直播延遲時(shí)也要考慮到具體的使用場景,越低的延遲當(dāng)然越好,但是它也會(huì)導(dǎo)致越低的緩存,容易造成直播卡頓。