某天在 聯合影音網 看到了影片的 HTML 標籤
<video
class="ui-video"
width="100%"
height="100%"
preload="metadata"
playsinline=""
webkit-playsinline=""
muted=""
src="https://cdn.udn.com/vods3/251221183729786/playlist.m3u8"
>
<track
src="//video.udn.com/api-player/video-subtitle/news/1316728"
default="true"
/>
</video>
其中 preload="metadata"、m3u8 跟 <track> 都是我沒看過的,就趁這個機會來研究看看吧!
由於 iThome 使用 cloudflare,發文若有一些 XSS 的關鍵字會被擋下,所以本文若有用到 script 標籤,都會改成 <xcript>
<video preload="none"></video>
<video preload="metadata"></video>
<video preload="auto"></video>
這個應該沒有爭議,就是 media 開始播放,才會發起 HTTP request 載入資源
告訴瀏覽器 "可以開始下載資源,即便使用者還沒按播放"
這邊要注意的是,不同瀏覽器的實作方式不同,以 Chrome 瀏覽器為例,一開始只會先下載幾 MB 的資料
<video
muted
controls
playsinline
preload="auto"
width="100%"
height="100%"
src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"
></video>

之後使用者開始播放影片後,才會接收後續的資源

在資源完整接收完之前,都會顯示 CAUTION: request is not finished yet!
上述情境,僅限於沒有跳轉影片的情況
若有跳轉,則會發起新的 Range Request
總結來說,preload="auto" 並沒有強制瀏覽器要馬上下載 "整個" 資源
firebase/storage 的 getMetadata我很好奇所謂的 metadata 有哪些
我第一個想到的是 firebase/storage 有 getMetadata 這個方法,順手實測看看
import { initializeApp, FirebaseOptions } from "firebase/app";
import { getStorage, ref, getMetadata } from "firebase/storage";
const firebaseOptions: FirebaseOptions = {
apiKey: process.env.apiKey,
authDomain: process.env.authDomain,
databaseURL: process.env.databaseURL,
projectId: process.env.projectId,
storageBucket: process.env.storageBucket,
messagingSenderId: process.env.messagingSenderId,
appId: process.env.appId,
measurementId: process.env.measurementId,
};
const firebaseApp = initializeApp(firebaseOptions);
const firebaseStorage = getStorage(firebaseApp);
const storageRef = ref(firebaseStorage, "20230519/20230519_123754.jpg");
getMetadata(storageRef).then(console.log);
結果
{
"type": "file",
"bucket": "xxxx-xxxxxx-xx.appspot.com",
"generation": "1726576002013836",
"metageneration": "4",
"fullPath": "20230519/20230519_123754.jpg",
"name": "20230519_123754.jpg",
"size": 4312957,
"timeCreated": "2024-09-17T12:26:42.015Z",
"updated": "2024-11-13T12:35:07.690Z",
"md5Hash": "wkMu9YKP/eCW/BCsvgfRkg==",
"contentEncoding": "identity",
"contentLanguage": "en",
"contentType": "image/jpeg"
}
metadata 的中文很難解釋,如果以上述例子來看,就是 "描述這張圖片" 的資料
有了 metadata 的概念,我們接著來實測 preload="metadata" 到底包含哪些屬性
基本的 HTML
<video
preload="metadata"
width="100%"
height="100%"
src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"
></video>
寫個 JS 印出 loadedmetadata 前後的 HTMLVideoElement 差異
const videoEl = document.getElementsByTagName("video")[0];
const oldVideoEl = {};
for (const attr in videoEl) {
oldVideoEl[attr] = videoEl[attr];
}
console.log(oldVideoEl);
videoEl.addEventListener("loadedmetadata", (e) => {
const newVideoEl = {};
for (const attr in videoEl) {
newVideoEl[attr] = videoEl[attr];
}
console.log(newVideoEl);
});
使用 git 比較 oldVideoEl 跟 newVideoEl 的差異

可以得知 video 的 metadata "至少" 包含以下屬性
{
"videoWidth": 1280,
"videoHeight": 720,
"duration": 653.804263
}
實際上觀察瀏覽器的 Network,瀏覽器只會先載入部分 mp4 的內容,且載入的資料量明顯比 preload="auto" 還要少

<track>再來談到 <track>,可以幫影片或音樂加上字幕,具體使用方法為
<video src="video.mp4">
<track default srclang="en" label="English" src="subtitle-en.vtt" />
<track srclang="zh-TW" label="繁體中文" src="subtitle-zh-TW.vtt" />
</video>
根據 HTMLMediaElement: loadedmetadata 的描述,字幕也在 preload="metadata" 的範圍內
寫個 PoC 來驗證:
http.Server
import { readFileSync } from "fs";
import http from "http";
import { join } from "path";
const httpServer = http.createServer((req, res) => {
const url = new URL(req.url || "/", "http://localhost:5000");
if (url.pathname === "/") {
const filePath = join(import.meta.dirname, "index.html");
const nonSharedBuffer = readFileSync(filePath);
res.setHeader("Content-Type", "text/html");
res.end(nonSharedBuffer);
return;
}
if (url.pathname === "/copy-from-mdn.vtt") {
const filePath = join(import.meta.dirname, "copy-from-mdn.vtt");
const nonSharedBuffer = readFileSync(filePath);
res.setHeader("Content-Type", "text/vtt");
res.end(nonSharedBuffer);
return;
}
if (url.pathname === "/ElephantsDream.mp4") {
const filePath = join(import.meta.dirname, "../range/ElephantsDream.mp4");
const nonSharedBuffer = readFileSync(filePath);
res.setHeader("Content-Type", "video/mp4");
res.end(nonSharedBuffer);
return;
}
});
httpServer.listen(5000);
<video
muted
controls
playsinline
preload="metadata"
width="100%"
height="100%"
src="ElephantsDream.mp4"
>
<track default srclang="en" label="English" src="copy-from-mdn.vtt" />
</video>
實測確實會預先載入字幕

vtt 全名是 Web Video Text Tracks Format (WEBVTT),是一個純文本的格式

說到字幕,我第一個想到的是 srt,也是純文本的格式
1
00:00:01,000 --> 00:00:04,000
這是第一行字幕。
2
00:00:05,000 --> 00:00:07,000
這是第二行字幕。
3
00:00:08,000 --> 00:00:10,000
這是第三行字幕。
Content-Type: application/vnd.apple.mpegurl
<video> + m3u8 是如何載入影片我們以 https://cdn.udn.com/vods3/251221183729786/playlist.m3u8 為範例
首先,瀏覽器會載入 playlist.m3u8
接著,瀏覽器會有一套機制,選擇出對應畫質(下圖選的是 1920x1080)的 chunklist_b4628000.m3u8
最後,就會根據 chunklist_b4628000.m3u8 回傳的內容,載入對應的 ts 影片
假設今天你收到一個需求,要在網頁上呈現一個 10 分鐘的影片,並且要確保跨裝置的用戶都能獲得最好的使用者體驗
總共有三個影片素材
你很開心的寫了一版 HTML,根據使用者的裝置寬度,來決定要載入哪個畫質的影片
<video src="video_480P.mp4" v-if="width <= 600"></video>
<video src="video_720P.mp4" v-if="width > 600 && width <= 1024"></video>
<video src="video_1080P.mp4" v-if="width > 1024"></video>
結果發現用手機觀看畫質超爛...
接著你開始研究
但你發現,最終也只能決定 "初始載入的影片畫質",如果今天在移動的火車上,或是剛好搭電梯,都會遇到網路不穩定的狀況
為什麼我們平常看影片的平台(YouTube, Netfilx...)都可以做到動態切換畫質呢?其中一個原因就是 m3u8!
直接來張圖比較 m3u8 跟傳統 HTTP range request + 大型 mp4 檔案的差別
另外,由於 m3u8 是 server 決定要把影片切成幾等份,所以檔名是固定的,非常適合快取!
雖然 HTTP range request 也可以快取,但不同瀏覽器發送 Range 的策略不同
都會被視為不同的 Cache Key,所以快取命中的機率就很低,也很難預先把 Cache 準備好
實測不同瀏覽器搭配 HTML 原生的 <video src="playlist.m3u8">,並且在中途使用瀏覽器內建功能切換網速
<video src="playlist.m3u8">
想依賴瀏覽器原生的機制來處理 m3u8 是不可靠的,所以這時候就需要專門的套件
很幸運的是,JavaScript 生態系有個 hls.js 套件,幫我們處理好細節了
只需要以下 HTML
<xcript src="https://cdn.jsdelivr.net/npm/hls.js@1"></xcript>
<video id="video" width="100%" height="100%"></video>
<xcript>
if (Hls.isSupported()) { const video =
document.getElementsByTagName("video")[0]; const hls = new Hls();
hls.loadSource("https://cdn.udn.com/vods3/260105182347770/playlist.m3u8");
hls.attachMedia(video); }
</xcript>
就可以根據網速,動態載入不同畫質的影片
在這篇文章,我們學到了
<track>, vtt, srt其實還有很多可以深入,包含
#EXT-X-BYTERANGE 的功用礙於篇幅關係,若未來有機會再來研究上述這些題目~