Yuanchieh's Blog

Yuanchieh's Blog

生命是長期而持續的累積

24 Sep 2018

HLS 教學 (上) — 從閱讀Spec 開始

在數位內容的時代,影音的閱讀比例也越來越高,相關的直播與影音串流服務越來越多,而其中最常使用的串流技術規格便是HLS;
近日工作上開始使用不少HLS服務,決定自己翻Spec搞懂整個技術架構,並整理成筆記分享。

目前預計會寫個兩篇:
上篇(此篇)介紹 HLS Spec內容
下篇介紹影音處理相關的規範與工具使用
全文內容基於文件 https://www.rfc-editor.org/rfc/rfc8216.txt ,但內文為自己閱讀後並重組,如果有不清楚或是筆誤之處,再麻煩留言囉

簡介與概論

RFC 8216 主要是介紹 HTTP Live Streaming (HLS),主要定義資料的檔案格式Server/Client應該有的相對應行為 ,目前的最新版本為 7。

HLS 提供穩定、省花費的方式推送連續性或長時段的影片,有幾個特性

  1. 接收方可以根據網路狀況調整不同解析度
  2. 支援加密
  3. 同個影音內容不同的播放條件,例如字幕切換
  4. 基於HTTP,可以有良好的Cache機制(省花費的原因)

HLS 大致上是將影片切成小塊狀,並透過播放清單推送,Client 收到切成塊狀的影片片段後就可以直接播放,而不需等到整個影片載完才可以播;
而每個片段都可以被Cache在CDN,方便推送到全世界。

小塊狀的影片稱為 Media Segment,常用的格式為 ts / fmp4
播放清單則是 Playlist,格式為 m3u8

HLS 有兩種 Playlist 格式: Master PlaylistMedia Playlist
Master Playlist 主要是為了提供多種解析度、翻譯字幕的播放清單 (Media Playlist),當 Client (如播放器)依據網路條件與用戶選擇等,則選擇支援的播放清單下載影片檔

Master Playlist

主要針對相同內容提供不同的 Streaming,例如不同的解析度、不同的國家有不同的字幕等

#EXTM3U  
#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000  
http://example.com/low.m3u8  
#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000  
http://example.com/mid.m3u8  
#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000  
http://example.com/hi.m3u8  
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"  
http://example.com/audio-only.m3u8

大意是在平均流量6000000時播放 hi.m3u8,在比較低點的1000000播放 low.m3u8,如果是針對音檔而已則播 audio-only.m3u8。

Media Playlist

就是 Master Playlist中 Streaming指定的串流播放清單,主要說明要播放的片段URI、播放時長、如何解碼影片或是解密內容等等

#EXTM3U  
#EXT-X-TARGETDURATION:10  
  
#EXTINF:9.009,  
http://media.example.com/first.ts  
#EXTINF:9.009,  
http://media.example.com/second.ts  
#EXTINF:3.003,  
http://media.example.com/third.ts

這個 Media Playlist 說明了大約最大個Media Segments播放時長為10秒;
第一個片段 URI 是 http:…/first.ts 長度為 9秒;
第一個片段播完換第二個片段,以此類推。

Media Segments

Media Playlist 是由多個 Media Segments 而組成,每個Media Segments 都是必須註明檔案來源(URI),且基本上需要是與前一個Media Segments 相連續的內容,也就是 1 播完換 2 ,2 播完換 3 的概念,除非有顯示宣告兩個Media Segments 是不連續的。

任一個Media Segments 都必須帶有足夠的資訊讓播放器可以解碼,在不同格式下,有些格式的初始化片段(Media Initialization Segments) 是相同的,這時候可以宣告 EXT-X-MAP 指定,後續的Media Segments 就可以共用。

以下常見的支援格式有

  1. MPEG-2 Transport Stream
  2. Fragmented MPEG-4
  3. Packed Audio(音訊)
  4. WebVTT(字幕)

前兩者是不同的影音編碼格式,MPEG-2被廣泛應用於有線電視、DVD等,而MPEG-4 是基於 MPEG-2 改良,各自有不同的格式與 Media Initialization Segments 的格式。

WebVTT通常需要額外的 EXT-TIMESTAMP-MAP 去與影音播放校時。

Playlist

Playlist 的檔名必須是 .m3u8.m3u ,並且回傳的 HTTP Response Content-Type必須為 application/vnd.apple.mpegur 或是 audio/mpegurl
編碼必須採用 utf-8 ,除了 CR / LF 外不能有其他 utf-8定義的控制碼。

在Playlist中,每一行可以是 URI / 空白或是由 # 開頭的字串;
特別注意 # 開頭的字串可以是標籤或是註解,標籤像是 #EXT-X 開頭,且區分大小寫(標籤全大寫)

URI 部分可以是相對路徑,相對於 Playlist 的URI。

接著介紹Playlist的組成標籤,這部分主要是節錄 Spec內容,因為是名詞定義有點枯燥,有興趣可以深入查詢,後續會有實際範例。

Basic Tags

Playlist 可分成 Media Playlist 與 Master Playlist,以下為兩者共用的標籤。

  1. EXTM3U
    指定檔案為 Extented M3U,必須是檔案的第一行(雷同於html的 DOCTYPE)
  2. EXT-X-VERSION
    指定版號,目前最新版為 7,整份 Playlist只應該出現一次。

Media Segments Tags

Media Segments Tag 會應用於下一個 Media Segment 或是後續所有的Media Segment 上。

  1. EXTINF
    註明該 Media Segment的時間長度,這個標籤是必須的。
    格式為 #EXTIF:<duration>,[<title>] 
    duration 單位為秒,建議用浮點數表示,如果版號小於三才使用整數
  2. EXT-X-BYTERANGE
    註記該 Media Segment只截取某範圍而非整個完整檔案
    格式為 #EXT-X-BYTERANGE:<n>[@<o>] 
    if( o ) {
     o 代表 byte offset,n表示從o開始取幾個 byte
    } else {
     o 取上一個 Media Segment的結尾
    }
  3. EXT-X-DISCONTINUITY
    先前說每個 Media Segment 都必須是連續的,但如果檔案格式、影音軌(Track)的數字/型別等、timestamp 有改變,就必須要顯示宣告
  4. EXT-X-KEY
    Media Segment 可以被加密,而#EXT-X-KEY則是宣告解密的方式
  5. EXT-X-MAP
    定應 Media Initialization Segments 來源,效力應用於每個Media Segment 上;
    格式為 #EXT-X-MAP:<Attribute-List>
  6. EXT-X-PROGRAM-DATE-TIME
    宣告第一個Media Segment 的絕對時間,格式為 ISO-8601,須包含時區,例如 #EXT-X-PROGRAM-DATE-TIME:2010–02–19T14:54:23.031+08:00
  7. EXT-X-DATERANGE
    指定該時間範圍內的屬性定義,這部分我看不懂在幹嘛OTZ

Media Playlist Tags

  1. EXT-X-TARGETDURATION
    單個 Media Segments 的 EXTINT最大值,單位為秒,格式是整數,此標籤為必填。
  2. EXT-X-MEDIA-SEQUENCE
    註記第一個Media Segment的序號,如果不存在則預設為0;
    此標籤需出現於所有Media Segments 之前
  3. EXT-X-DISCONTINUITY-SEQUENCE
    用於多個串流同步的序號。
  4. EXT-X-ENDLIST
    不會有更多的 Media Segment被加入 Playlist中,可以出現在清單的各處
  5. EXT-X-PLATLIST-TYPE
    可以是 EVENT 或是 VOD
    EVENT 表示 Media Segment 會持續加到 Playlist之後
    VOD表示 Playlist 不會改變了
  6. EXT-X-I-FRAMES-ONLY
    每個 Media Segment都是獨立的I-Frame,影片的編碼與前後沒有相依性,可用於快轉、快速倒帶、關鍵影格等場景*

Master Playlist Tags

  1. EXT-X-MEDIA
    註明在不同的場景(例如純音檔/英文字幕/法文字幕等)下播放不同的 Media Playlist,也就是不同的Rendition,但這些Playlist都必須是相同的內容;
    其中有許多屬性可以設定
    TYPE:可以是AUDIO/VIDEO/SUBTITLE/CLOSE-CAPTIONS(隱藏字幕)
    URI:對應的Media Playlist 資源位置
    LANGUAGE
    ASSOC-LANGUAGE:用於其他地方的語言指定,如語音 vs 文字
    DEFAULT:預設播放此Playlist
    GROUP-ID:自定義的名稱
    等等
  2. EXT-X-STREAM-INF
    宣告 Variant Stream,屬性有
    BANDWITDH:加總Media Playlist的最大 Segment Bit rate
    AVERAGE-BANDWITDH:平均每秒幾個bits
    CODES:編解碼格式
    VIDEO:其值需對應#EXT-X-MEDIAGROUP-ID ,且該#EXT-X-MEDIA 的TYPE必須是 VIDEO
    AUDIO:狀況雷同
    #EXT-X-STREAM-INF#EXT-X-MEDIA 相輔相成
  3. EXT-X-I-FRAME-STREAM-INF
  4. EXT-X-SESSION-DATA
    任意的 Session值
  5. EXT-X-SESSION-KEY
    預先加載在 Media Playlist宣告的 EXT-X-KEY的值

Media or Master Playlist Tags

  1. EXT-X-INDEPENDENT_SEGMENTS
    註記每個Media Segments 可以單獨decode而不需要其他 segment的資訊
    如果出現在Master Playlist對所有 Media Playlist都有效
  2. EXT-X-START
    從何處開始播放清單
    需指名 TIME-OFFSET屬性,正值代表從影片開頭開始算,負值則從影片尾端開始算
    TIME-OFFSET值不可超過影片的長度,如果超過則正值從尾部算而負值從頭算(一個循環的概念)

以上是所有的標籤,也不是每個意思都很好理解,後續盡量以範例說明

Key files

EXT-X-KEY的URI屬性標記Key的檔案位置,用於解密Media Segments;
目前的加密方式可支援 AES-128,如果採用此加密方式,則需要 IV值,用於增強AES-128加密的初始值。

Client / Server Responsibilities

第六章主要定義Server / Client 遇到不同標籤的處理行為,算是上面的大統整,以及實做上的引導說明。

Server

負責產生Playlist與提供Media Segments

實際如何產生來源媒體檔不再 Spec範疇中,而Server必須負責把來源媒體檔分割成連續且時長小於總目標時長的Media Segments,需注意當Server分割檔案時需要切在 packet / key frame 上;
接著針對每個Media Segment分配特定的URI,如果Server支援 Byte-Range GET則可以在Playlist中使用 EXT-X-BYTERANGE

當Media Segment 被運用在 Playlist中,Server必須確保資源可以被取得且不會出錯,且下載必須是立即下載而非 Streaming

如果TYPE是 VOD,Server 產生出 Playlist就不應該修改;
如果TYPE 是 EVENT,已經產生的Playlist也不能修改,只能在Playlist之後繼續增加行數。

如果Media Playlist沒有包含 EXT-X-ENDLIST,則Server 必須提供包含新的Media Segments 來更新Playlist,而提供的時間必須等 0.5 ~ 1.5 目標時間內,因為這樣才能最大化利用網路效能。

Client

當Client 收到Master Playlist ,可以選擇一個 Variant Stream 播放,Client 必須檢查版號與標籤的語法,如果不合則停止播放,但為了更好的向前相容,遇到無法識別的標籤或屬性值就自動忽略。

如果 Media Playlist TYPE 是 Event 且沒有 EXT-X-ENDLIST 則必須定期重載 Playlist 以便獲取最新的 Media Segments,但為了不帶給Server過多負擔,重新獲取的時間差必須在至少一倍的 target duration,如果第一次重載後沒有更新則至少等半個target duration 再重試。

這部分內容很多,有興趣可以慢慢翻,主要就是講如何載入資源與遇到標籤要怎麼處理等等

Examples

Example Playlists for HTTP Live Streaming | Apple Developer Documentation

Spec 中也有範例,但這邊我擷取Apple Developer 的範例

安插廣告

#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,ad0.ts
#EXTINF:8.0,ad1.ts
#EXT-X-DISCONTINUITY
#EXTINF:10.0,movieA.ts
#EXTINF:10.0,movieB.ts

注意在 ad1.ts 與 movieA.ts 中有個 #EXT-X-DISCONTINUITY ,因為 Ad 跟 Movie 勢必是兩個不同的片段,所以必須加入才不會因為 timestamp 銜接不上而導致播放錯誤!

Master Playlist with Alternative Audio

#EXTM3U  
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English", \\  
    DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en", \\  
    URI="main/english-audio.m3u8"  
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Deutsch", \\  
    DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="de", \\  
    URI="main/german-audio.m3u8"  
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Commentary", \\  
    DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="en", \\  
    URI="commentary/audio-only.m3u8"  
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="...",AUDIO="aac"  
low/video-only.m3u8  
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="...",AUDIO="aac"  
mid/video-only.m3u8  
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="...",AUDIO="aac"  
hi/video-only.m3u8  
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5",AUDIO="aac"  
main/english-audio.m3u8

如果希望可以讓Video 依據不同的頻寬切換,而音檔根據不同的語言選擇切換,就可以參考這個範例

#EXT-X-MEDIA 定義了Rendition,透過LANGUAGE指定語言,接著TYPE表明為AUDIO聲音檔,GROUP-ID自取為 aac;

#EXT-X-STREAM-INF 定義的是 Variant Stream,AUDIO指定為 aac,也就是對應 #EXT-X-MEDIA 的GROUP-ID,其URI則指定播放 video。

可以看出常用的標籤就那幾個,有些標籤我來回看了幾次還是看不懂,像是 EXT-X-DATERANGE就很難懂,範例也都沒用上,如果有人知道實際上的應用與原理再麻煩留言了OTZ

結語

整份Spec 死嗑完很多部份也還是懵懵懂懂,但至少有個全局的認知與介紹,下一章從影片處理到Playlist修改,來看實際應用的處理。

註記

影片編碼

參考維基百科

視訊壓縮圖像類型 - 维基百科,自由的百科全书

影像是一幀一幀的圖片連續快速播放,但如果儲存時把每一幀都用圖片方式儲存資料量會非常大,但是影片往往一段時間的場景雷同,如果我可以先記第一張圖,其餘去計算第二張與第一張差多少的概念去儲存,就可以避免儲存過多相同冗余的數據
但因為要避免掉了一點資料就全毀,保留一些彈性所以會在不同時刻安插取全圖的時機。

影片壓縮透過建立 I-Frame / P-Frame / B-Frame方式
I-Frame 關鍵影格,基本上就儲存並壓縮整張圖,作為其他Frame解碼的依據
P-Frame則僅儲存與上一個 I Frame差異的像素
B-Frame則為前後預測圖像,會參考前一個 I-Frame 與後一個 P-Frame
所以大致會長成 IBB..PIBB..P,取 I-Frame / B-Frame 的頻率可以停整

影音檔與 FFMpeg 教學:ffmpeg-libav-tutorial