按下播放之後,你的手機其實在狂抓上千個 6 秒小檔案:從我在 CatchPlay 做轉檔的經驗,聊聊串流、HLS 跟 DASH
先講結論:串流就是一張菜單,加上一大堆 6 秒的小檔案
你在手機上按下播放,感覺好像有一條水管,把整部電影咕嚕咕嚕灌過來。
其實不是這樣。
播放器會先下載一張純文字的「菜單」,上面寫著這部片有哪幾種畫質、每種畫質被切成哪些小檔案。接下來它就一段一段抓,每段大概 6 秒。每抓下一段之前,它都會先看一下自己現在網速多快,再決定這一段要抓高畫質還是低畫質。
算一下就知道有多碎:一部兩小時的電影,光一種畫質就會切出 1,200 個小檔案。
2016 到 2017 年我在 CatchPlay(台灣的影音串流平台)寫轉檔系統,每天在做的事,就是把一部部電影剁成這個樣子。這篇就把那套東西拆開來聊,裡面的檔案都是我在自己筆電上實際跑出來的。
先把整篇重點濃縮成一張表:
| 你可能以為 | 其實是 |
|---|---|
| 影片用一條連線從頭傳到尾 | 播放器一直發 HTTP 請求,一次抓一個 6 秒左右的小檔 |
| 畫質是網站幫你決定的 | 是你手機上的播放器,每抓一段就自己決定一次 |
| 串流要用很特別的串流伺服器 | 丟在最普通的網頁伺服器或 CDN 上就能播 |
| HLS 跟 DASH 選一個就好 | 2016 年的 CatchPlay 兩個都做,因為 Apple 的 DRM 走 HLS、Google 的 DRM 走 DASH |
| 兩種格式就得存兩份影片 | 現在可以用 CMAF,一份片段讓兩張菜單共用 |
丟一個 MP4 給你看不就好了?問題是你的網速一直在變
最直覺的做法,就是把電影轉成一個 MP4 放上網站,讓你邊下載邊看。
這招叫「漸進式下載」(progressive download),早年很多網路影片真的就是這樣播的。
但它有個大問題:畫質在你按下播放的那一刻就定死了。 你在家用 Wi-Fi 開了 1080p,出門搭捷運訊號一掉,它沒辦法半路換成低畫質,只能停在那邊轉圈圈。
串流的解法說穿了很暴力:既然網速會變,那就乾脆把影片切碎,讓播放器每隔幾秒重新挑一次。
這個做法叫「自適應串流」(Adaptive Bitrate Streaming,簡稱 ABR)。HLS 跟 DASH,就是兩種把它做出來的規格。
| 漸進式下載 | 自適應串流 | |
|---|---|---|
| 伺服器上放什麼 | 一個 MP4 | 好幾種畫質,每種切成上千段 |
| 畫質什麼時候決定 | 按下播放那一刻 | 每抓一段就重新決定一次 |
| 網速變差的時候 | 卡住、轉圈圈 | 下一段改抓低畫質,畫面糊一點但不會停 |
把它想成一間只賣 6 秒便當的店
這間店每道菜都有大、中、小三種份量,而且每個便當只裝 6 秒的劇情。櫃台貼著一張菜單,寫清楚大份要多少頻寬、小份要多少頻寬,還有每個便當放在哪一格。
你(播放器)坐在店裡,每吃完一個就瞄一下窗外的路況。路很順,下一個就點大份;開始塞車了,改點小份。畫面會糊一點,但至少不會停。
聰明一點的客人,還會在桌上先囤兩三個便當,就算外送突然延遲也不會斷糧。這疊存糧,就是播放器的緩衝區(buffer)。
至於老闆(伺服器),他根本不管你點什麼。他只負責把每種份量先做好、整整齊齊擺在櫃台,誰來拿就給誰。
聰明的全是客人,老闆越笨越好。 串流之所以便宜、又撐得住超大流量,關鍵就在這個分工,後面會一直繞回來講。
轉檔:同一部片,要先做成大中小好幾種份量
便當店開門之前,得先把每種份量都做好。這一步叫轉檔(transcoding),也就是我當年在 CatchPlay 主要在做的事。
為了寫這篇,我在自己的 M2 MacBook 上用 FFmpeg 8.0.1 重做了一個迷你版:拿一支 60 秒的彩色測試畫面,轉成 1080p、720p、360p 三種畫質的 HLS。
ffmpeg -i source.mp4 \
-filter_complex "[0:v]split=3[a][b][c];[a]scale=1920:1080[v1];[b]scale=1280:720[v2];[c]scale=640:360[v3]" \
-map "[v1]" -map "[v2]" -map "[v3]" -map 0:a -map 0:a -map 0:a \
-c:v libx264 -preset veryfast -g 60 -keyint_min 60 -sc_threshold 0 \
-b:v:0 5000k -maxrate:v:0 5350k -bufsize:v:0 7500k \
-b:v:1 2800k -maxrate:v:1 2996k -bufsize:v:1 4200k \
-b:v:2 800k -maxrate:v:2 856k -bufsize:v:2 1200k \
-c:a aac -b:a 128k -ac 2 \
-f hls -hls_time 6 -hls_playlist_type vod \
-hls_segment_filename "%v/seg_%03d.ts" \
-master_pl_name master.m3u8 \
-var_stream_map "v:0,a:0,name:1080p v:1,a:1,name:720p v:2,a:2,name:360p" \
"%v/index.m3u8"
跑出來長這樣:
| 畫質 | 解析度 | 設定的影像位元率 | 實測(第 4 段,含音訊) | 平均每段大小 |
|---|---|---|---|---|
| 1080p | 1920×1080 | 5,000 kbps | 5,217 kbps | 3.96 MB |
| 720p | 1280×720 | 2,800 kbps | 2,993 kbps | 2.28 MB |
| 360p | 640×360 | 800 kbps | 964 kbps | 0.74 MB |
這張表,業界叫它 bitrate ladder(位元率階梯)。實測會比設定值高一點點,多出來的是 128 kbps 的音訊,加上封裝本身的開銷。
階梯要排幾階、每一階給多少,其實是一門學問。Netflix 2015 年 12 月有一篇〈Per-Title Encode Optimization〉,講的就是「所有片子共用同一張階梯」的毛病:動畫根本用不到那麼高的位元率,底片顆粒很重的片子給到最高階卻還是有色塊。所以他們後來改成每部片各算一張自己的階梯。
60 秒的片、三種畫質,我的 M2 跑了 19.3 秒。
照這個速度粗估,一部兩小時的電影大概要 39 分鐘。而且這已經是最樂觀的算法了:正式上線的階梯通常不只三階,壓縮也會選更慢、更省頻寬的設定,再加上多語音軌跟字幕,每一項都會讓時間再往上乘。
這也是為什麼 CatchPlay 那套系統最忙的時候,會同時開 500 多台 AWS EC2 在轉檔。機器一開起來,會自己去中控資料庫問「我這台要做什麼」,領到工作就去把片子拉下來轉。那套系統是怎麼蓋起來的,我另外寫在〈500 台 EC2 同時轉檔的日子——CatchPlay〉。
切片:每 6 秒切一刀,而且每種畫質都得切在同一秒
轉好的影片不是一個大檔案,而是一疊小檔案:
$ ls -lh 1080p | head -5
total 77416
-rw-r--r--@ 1 bobochen wheel 403B Sep 22 14:00 index.m3u8
-rw-r--r--@ 1 bobochen wheel 3.8M Sep 22 14:00 seg_000.ts
-rw-r--r--@ 1 bobochen wheel 3.8M Sep 22 14:00 seg_001.ts
-rw-r--r--@ 1 bobochen wheel 3.8M Sep 22 14:00 seg_002.ts
1080p: 10 segments, 38M
720p: 10 segments, 22M
360p: 10 segments, 7.1M
三種畫質都是 10 段、每段 6 秒,加起來剛好 60 秒。(ls -h 顯示的 3.8M 是 1,024 進位的 MiB,換成上面表格用的 MB,大約是 3.96。)
重點在「同一秒」這件事。我把三份清單裡每一段的長度抓出來算雜湊值,結果三個一模一樣:
$ for d in 1080p 720p 360p; do grep EXTINF $d/index.m3u8 | md5; done
06e1bcc5a050beeb02f0b00dbd7e3f38
06e1bcc5a050beeb02f0b00dbd7e3f38
06e1bcc5a050beeb02f0b00dbd7e3f38
意思就是:三種畫質的切點完全對齊。播放器看完 720p 的第 3 段,下一段換成 360p 的第 4 段,畫面還是能無縫接上,因為這兩段都是從同一個畫面開始的。
能做到這樣,靠的是「關鍵影格」(keyframe)。
影片壓縮的時候,只有關鍵影格是一張完整的畫面,其他影格都只記「跟前一格差在哪」。所以每一段都一定要從關鍵影格開始。不然播放器一接手,手上只有一堆「差異」、沒有底圖,畫面就會花掉。
上面那串指令裡,有兩個參數就是在管這件事:
-g 60 -keyint_min 60:每 60 格固定放一個關鍵影格。這支片是 30fps,等於每 2 秒一個,所以每 6 秒的切點一定會落在關鍵影格上-sc_threshold 0:關掉「偵測到換場,就自動多插一個關鍵影格」的功能。不關的話,每種畫質可能在不一樣的地方多插,切點就對不齊了
那每段到底要切多長?這是取捨。切短一點,換畫質反應比較快、直播延遲也比較低,可是請求數會變多、壓縮效率也會變差;切長一點就剛好反過來。
我會選 6 秒,不是隨便挑的。Apple 的〈HLS Authoring Specification for Apple Devices〉(最近一次修訂是 2025 年 6 月)第 7 節講得很白:
7.5. Target durations SHOULD be 6 seconds.
翻成中文就是:每段的目標長度「應該」是 6 秒。規格裡的 SHOULD 分量很重,意思是「強烈建議,除非你有很好的理由」。
m3u8 打開來看,其實就是一張純文字菜單
這是剛剛產出來的總菜單 master.m3u8,一字不漏:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=5330928,AVERAGE-BANDWIDTH=5280894,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
1080p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3057882,AVERAGE-BANDWIDTH=3038982,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1021968,AVERAGE-BANDWIDTH=990409,RESOLUTION=640x360,CODECS="avc1.64001e,mp4a.40.2"
360p/index.m3u8
每一個 #EXT-X-STREAM-INF,就代表一種份量:
BANDWIDTH=5330928:這個畫質最高大概要 5.3 Mbps。播放器就是拿自己的網速跟這個數字比RESOLUTION:解析度CODECS:要用哪種解碼器。avc1是 H.264 影像,mp4a.40.2是 AAC 音訊。播放器如果看到自己不支援的格式,就會直接跳過這一行- 再下一行:這個份量自己的清單放在哪
點進 720p/index.m3u8,就是第二層:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:6.000000,
seg_000.ts
#EXTINF:6.000000,
seg_001.ts
...
#EXTINF:6.000000,
seg_009.ts
#EXT-X-ENDLIST
#EXTINF:6.000000:下面這個檔案長 6 秒#EXT-X-PLAYLIST-TYPE:VOD加上最後的#EXT-X-ENDLIST:這是隨選影片,清單不會再變長了
直播跟隨選影片,差就差在這裡。直播的清單沒有 ENDLIST,播放器會每隔幾秒重新下載一次清單,看看最後面有沒有多出新的段落。
所以說,整部電影的「串流」,底層就是這兩層文字檔,加上一疊 .ts 小檔案。沒有什麼神秘的串流黑科技。
播放器才是老大,伺服器只是負責擺便當的
口說無憑,我用 Python 內建的 http.server 開了一個最陽春的靜態檔案伺服器,它完全不懂什麼是影片。然後叫 FFmpeg 從它那邊讀 720p、播 15 秒。
伺服器的存取紀錄長這樣:
"GET /master.m3u8 HTTP/1.1"
"GET /1080p/index.m3u8 HTTP/1.1"
"GET /720p/index.m3u8 HTTP/1.1"
"GET /360p/index.m3u8 HTTP/1.1"
"GET /1080p/seg_000.ts HTTP/1.1"
"GET /720p/seg_000.ts HTTP/1.1"
"GET /360p/seg_000.ts HTTP/1.1"
"GET /720p/seg_001.ts HTTP/1.1"
"GET /720p/seg_002.ts HTTP/1.1"
我們一行一行看:
- 先拿總菜單
- 三份子清單全都拿了
- 三種畫質的第 1 段各抓一次
- 接下來就只抓 720p 的第 2、3 段。15 秒剛好落在第 3 段裡,所以就停在這裡
第 3 步我要老實講:FFmpeg 其實不是會自己換畫質的播放器。它一開始把三種畫質都摸過一遍、確認格式,之後就照我指定的只抓 720p。會一邊量網速、一邊換畫質的,是 Safari、hls.js 這種真正的播放器。
不過伺服器這邊的結論不會變:它從頭到尾只做一件事,你要哪個檔案,它就給你哪個檔案。 誰在看、看到哪、網速多快,它通通不知道。
那真正的播放器是怎麼決定的?拿上面的實測數字算一次就懂了:
| 畫質 | 平均每段 | 要在 6 秒內抓完,網速至少要 | 網速 4 Mbps 時抓一段要 |
|---|---|---|---|
| 1080p | 3.96 MB | 5.3 Mbps | 7.9 秒(比播放還慢) |
| 720p | 2.28 MB | 3.0 Mbps | 4.6 秒 |
| 360p | 0.74 MB | 1.0 Mbps | 1.5 秒 |
假設網速掉到 4 Mbps,1080p 抓一段要 7.9 秒,可是播一段只要 6 秒。每播一段,緩衝區就少快 2 秒,撐沒多久就會卡住。所以播放器會在下一段改抓 720p。
整個過程畫成時序圖,大概是這樣(這是示意圖,不是上面的實測紀錄):
sequenceDiagram
participant P as 播放器
participant C as CDN(一般的 HTTP 伺服器)
P->>C: GET master.m3u8
C-->>P: 總菜單(三種畫質)
P->>C: GET 1080p/index.m3u8
C-->>P: 1080p 的分段清單
P->>C: GET 1080p/seg_000.ts
C-->>P: 第 1 段(下載很快,網速夠)
Note over P: 進隧道,量到的網速掉到 4 Mbps
P->>C: GET 720p/index.m3u8
P->>C: GET 720p/seg_001.ts
C-->>P: 第 2 段改用 720p,畫面無縫接上
也因為全都是普通的 HTTP 檔案,所以可以直接丟給 CDN 快取。一萬個人同時看同一部片的第 3 段,每個 CDN 節點只要回源站拿一次就好。
不過這也有副作用:檔案一旦被 CDN 記住,你在源站把它換掉,CDN 也不會知道,得手動叫它清掉。我在 CatchPlay 的時候,同事每天都要做這件事,後來我乾脆寫了一個清 CDN 快取的小工具。
HLS 跟 DASH 在做同一件事,只是分成兩個陣營
講到這邊,其實 HLS 已經講完了。上面那兩層 .m3u8 加一疊 .ts 小檔,就是 HLS。
它是 Apple 在 2009 年跟著 iPhone OS 3.0 一起推出的,2017 年 8 月整理成 RFC 8216。這份 RFC 摘要的第一句是這樣寫的:
This document describes a protocol for transferring unbounded streams of multimedia data.
白話一點講:這是一個用來傳送「沒有固定長度」影音資料的協定。「沒有固定長度」是重點,代表它一開始就把直播考慮進去了,所以清單才會設計成可以一直往後長。RFC 8216 描述的是協定第 7 版;更新的第二版,到 2026 年 9 月都還是草案,還沒正式定案。
DASH 做的也是同一件事,只是菜單長得不一樣。我用同樣的三階設定轉了一份 DASH,它的菜單叫 manifest.mpd,是一份 XML(節錄):
<MPD type="static" mediaPresentationDuration="PT1M0.0S" maxSegmentDuration="PT6.0S" minBufferTime="PT12.0S">
<Period id="0" start="PT0.0S">
<AdaptationSet id="0" contentType="video" startWithSAP="1" segmentAlignment="true">
<Representation id="0" codecs="avc1.640028" bandwidth="5000000" width="1920" height="1080">
<SegmentTemplate timescale="1000000" duration="6000000"
initialization="init-stream$RepresentationID$.m4s"
media="chunk-stream$RepresentationID$-$Number%05d$.m4s" startNumber="1"/>
</Representation>
<Representation id="1" codecs="avc1.64001f" bandwidth="2800000" width="1280" height="720">...</Representation>
<Representation id="2" codecs="avc1.64001e" bandwidth="800000" width="640" height="360">...</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="audio">...</AdaptationSet>
</Period>
</MPD>
一樣拿便當店來對照:
AdaptationSet是一道菜(影像一道、音訊一道)Representation是這道菜的其中一種份量SegmentTemplate用樣板來描述檔名。$Number%05d$會被換成 00001、00002……這樣就不用把每一段都列出來
另外可以注意 startWithSAP="1" 跟 segmentAlignment="true" 這兩個屬性。它們其實就是白紙黑字寫出上一節講的事:每一段都從關鍵影格開始,而且各種畫質的切點是對齊的。
兩種格式的差別,整理成表:
| HLS | DASH | |
|---|---|---|
| 誰定的 | Apple,2009 年推出,2017 年變成 RFC 8216 | MPEG,國際標準 ISO/IEC 23009-1,2012 年 4 月第一版,已公開的最新版是 2022 年第 5 版 |
| 菜單 | .m3u8,純文字 | .mpd,XML |
| 片段 | 早年是 .ts,2016 年起也能用 fMP4 | fMP4(.m4s) |
| 誰能直接播 | Safari、iPhone、iPad、Android;桌面版 Chrome 從 142 版(2025 年 10 月)起也行 | 瀏覽器都沒有內建,要靠 JavaScript 播放器,像 dash.js、Shaka Player |
| 常見的 DRM 搭配 | FairPlay(Apple) | Widevine(Google)、PlayReady(Microsoft) |
技術上它們比較像表兄弟,要選哪個,幾乎不是看誰技術比較強。真正決定你要做哪一個的,是你的使用者拿什麼裝置,還有片商要求你用什麼 DRM。
2016 年我們為什麼兩套都要做:因為 DRM 各自選了邊
CatchPlay 播的是片商授權的電影,影片得加密,也就是大家常聽到的 DRM(數位版權管理)。
DRM 大概是這樣運作的:片段本身是加密過的,就算被偷下載,拿到的也只是一堆亂碼。播放器要播之前,得先拿憑證去授權伺服器換一把金鑰。這把金鑰會被送進瀏覽器或手機裡一個你碰不到的解密模組,解出來的畫面直接送上螢幕,中間你完全摸不到。
麻煩的是,DRM 也分陣營:
| DRM | 誰家的 | 用在哪裡 | 搭配的格式 |
|---|---|---|---|
| FairPlay | Apple | Safari、iPhone、iPad | HLS |
| Widevine | Chrome、Android | DASH |
Apple 的〈FairPlay Streaming Overview〉一開頭就寫,FairPlay 保護的內容 “is delivered over the Web using HTTP Live Streaming (HLS)“,HLS 規格第 13 節也只定義了 FairPlay 搭配 HLS 的加密方式。FairPlay 搭 DASH 要怎麼做,Apple 從來沒公開過。
反過來看,DASH 在 2016 年的 iPhone 上根本沒戲唱。瀏覽器要播 DASH,得靠 JavaScript 播放器把片段一塊一塊餵給瀏覽器,這需要一個叫 Media Source Extensions 的 API。而 iPhone 的 Safari,一直等到 iOS 17.1(2023 年 10 月)才開放類似的功能。
所以要讓 iPhone 跟 Android 的使用者都能看,就只能兩邊都做。我那時候就是 Widevine 跟 FairPlay 一起實作,DASH 跟 HLS 兩種封裝也一起產。
兩套並行的代價,就是每部片的封裝、加密、測試全都變成兩份。只要有一個環節沒對上,不管是金鑰錯了、格式切不過去,還是授權過期,使用者看到的都是同一個畫面:一片黑。
如果今天重做:CMAF 讓兩張菜單共用同一批片段
2016 年的時候,兩套格式的片段容器還不一樣:HLS 用 .ts,DASH 用 fMP4。同一部片就得存兩份。
巧的是,轉機剛好就在那一年。2016 年 6 月的 WWDC,Apple 宣布 HLS 也能用 fMP4 了,剛好就是我去 CatchPlay 報到的那個月。同一年 2 月,Apple 跟 Microsoft 一起向 MPEG 提案,想把這種兩邊都能用的片段格式定成標準,最後在 2018 年 1 月以 ISO/IEC 23000-19 發布,名字叫 CMAF(Common Media Application Format)。
從此片段只要做一份,HLS 跟 DASH 兩張菜單都指向同一批檔案就好。
FFmpeg 在輸出 DASH 的時候加上 -hls_playlist 1,就能一次產出兩張菜單。我跑出來的資料夾長這樣:
$ ls dash/
chunk-stream0-00001.m4s ... chunk-stream3-00011.m4s (影像與音訊片段)
init-stream0.m4s ... init-stream3.m4s (初始化片段)
manifest.mpd (DASH 的菜單)
master.m3u8 media_0.m3u8 ... media_3.m3u8 (HLS 的菜單)
打開 HLS 那邊的 media_1.m3u8,你會發現它指的就是 DASH 在用的同一批檔案:
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-MAP:URI="init-stream1.m4s"
#EXTINF:6.000000,
chunk-stream1-00001.m4s
#EXTINF:6.000000,
chunk-stream1-00002.m4s
用同一支 60 秒測試片算算看儲存量:
| 做法 | 片段總大小 | 能給誰用 |
|---|---|---|
HLS 用 .ts | 69.8 MB | 只有 HLS |
| DASH 用 fMP4 | 65.6 MB | 只有 DASH |
| 2016 年的做法:兩份都存 | 135.4 MB | 兩邊 |
| CMAF:一份 fMP4、兩張菜單 | 65.6 MB | 兩邊 |
(CMAF 那一列的 65.6 MB 已經把兩張菜單算進去了,反正文字檔也才幾 KB。)
如果今天要我重做這套系統,片段我只會做一份。
加密這關也在慢慢合流,只是腳步慢一點。FairPlay 用的加密模式叫 cbcs,Widevine 跟 PlayReady 早年只支援另一種叫 cenc 的模式。PlayReady 是從 4.0 版(2017 年 9 月)、Chrome 裡的 Widevine 大約是 2018 年中,才開始支援 cbcs。
換句話說,只要裝置夠新,連加密過的片段也能只做一份;但如果還要照顧舊電視、舊機上盒,就還是得多留一份 cenc。加密這段我這次沒有實測,上面的時間點是我查 Microsoft 跟 Chrome 的公開文件整理出來的。
3 分鐘,自己動手做一個 HLS
只要有 FFmpeg 跟 Python(macOS 內建就有)就能玩:
# 1. 先裝 FFmpeg(我用的是 8.0.1)
brew install ffmpeg
# 2. 隨便拿一支影片,轉成 6 秒一段的 HLS(每 6 秒放一個關鍵影格,不管影格率多少都適用)
ffmpeg -i input.mp4 -c:v libx264 -force_key_frames "expr:gte(t,n_forced*6)" \
-c:a aac -f hls -hls_time 6 -hls_playlist_type vod out.m3u8
# 3. 開一個最普通的網頁伺服器
python3 -m http.server 8000
最小可用流程:跑完第 2 步 → cat out.m3u8 看看切了幾段 → 第 3 步把伺服器開起來 → 另開一個終端機跑 ffprobe http://127.0.0.1:8000/out.m3u8,有印出影像跟音訊資訊就代表讀得到。想看畫面的話,用 Safari 或 142 版以後的 Chrome 打開同一個網址,它們都能直接播 HLS。
我拿上面那支 60 秒測試片跑第 2 步,M2 花了 22.4 秒,產出 out0.ts 到 out9.ts,一共 10 段。
常用參數:
-hls_time 6:每段切幾秒-hls_playlist_type vod:隨選影片模式,清單最後會加上#EXT-X-ENDLIST-force_key_frames或-g:控制關鍵影格的間隔,也就是決定能在哪裡下刀-sc_threshold 0:做多畫質階梯時記得加,免得換場時多插關鍵影格、害切點對不齊-var_stream_map加-master_pl_name:一次產出多種畫質跟總菜單-f dash -seg_duration 6 -hls_playlist 1:產出 DASH,順便產一份共用片段的 HLS 菜單
反思:串流聰明的地方,全在你手上那台裝置
技術面
串流能撐住一萬個人同時看同一部片,靠的就是伺服器夠笨。所有判斷都丟給播放器,伺服器只剩「給檔案」這一個動作,所以可以整個交給 CDN,要複製幾份都行。
到了 2026 年,HLS 跟 DASH 的差別主要在 DRM 跟裝置生態,不是誰技術比較好。把 2016 年跟今天擺在一起看最清楚:
| 2016 年的做法 | 今天重做 | |
|---|---|---|
| 片段 | HLS 用 .ts、DASH 用 fMP4,存兩份 | CMAF,fMP4 存一份 |
| 菜單 | .m3u8 跟 .mpd 各一份 | 一樣各一份,幾 KB 的文字檔 |
| 加密 | FairPlay 跟 Widevine 各加密一次 | 裝置夠新就用 cbcs 加密一次 |
| iPhone 網頁播 DASH | 不可能 | iOS 17.1 起可以,但通常還是直接給 HLS |
心態面
當年在 CatchPlay,我一直覺得轉檔系統最難的是「規模」:500 台機器要怎麼調度、哪台掛了要怎麼重派。
現在回頭看,規模反而是最能用錢解決的那一塊。真正應該一開始就想清楚的,是每段切幾秒、關鍵影格怎麼放、階梯排幾階這些決定。它們在指令裡只是幾個參數,可是一旦設錯,就得把整個片庫重轉一遍。這點我當年其實沒看得這麼清楚。
有趣發現
- 我本來想拿 FFmpeg 示範「播放器換畫質」,結果一看伺服器紀錄才發現,它根本不會換,只是一個看得懂 m3u8 的下載器。果然看紀錄比相信自己的印象可靠。
- m3u8 這個副檔名,來自 MP3 年代的播放清單格式 M3U,後面那個 8 代表 UTF-8 編碼。所以串流影片的菜單,本質上跟你二十年前存在電腦裡的歌單是同一種東西。
說到底,串流其實沒有在「流」,它是你的播放器每 6 秒做一次的選擇。