瀏覽器影片播放原理:video 標籤、MSE、EME、WebCodecs 完整解析
點開 YouTube 影片就開始播放。背後涉及 HTML5 video 標籤、MSE、EME、WebCodecs 等技術。這篇解析瀏覽器播放影片的底層原理。
瀏覽器影片播放技術演進
HTML5 video(2010)→ MSE(2013)→ EME(2017)→ WebCodecs(2022)| 技術 | 標準化年 | 用途 |
|---|---|---|
| HTML5 video | 2010 | 基本影片播放 |
| MSE | 2013 | 動態串流(HLS、DASH) |
| EME | 2017 | DRM 保護 |
| WebCodecs | 2022 | 低階編解碼 |
HTML5 video 標籤
基本用法
html
<video src="video.mp4" controls></video>多來源
html
<video controls>
<source src="video.webm" type="video/webm">
<source src="video.mp4" type="video/mp4">
您的瀏覽器不支援 video 標籤。
</video>瀏覽器依序嘗試,選擇支援的格式。
JavaScript 控制
javascript
const video = document.querySelector('video');
// 播放控制
video.play();
video.pause();
video.currentTime = 30; // 跳到 30 秒
// 屬性
console.log(video.duration); // 總時長
console.log(video.currentTime); // 當前位置
console.log(video.volume); // 音量(0-1)
console.log(video.playbackRate); // 播放速度
// 事件
video.addEventListener('play', () => console.log('開始播放'));
video.addEventListener('pause', () => console.log('暫停'));
video.addEventListener('ended', () => console.log('播放結束'));
video.addEventListener('timeupdate', () => {
console.log(`當前位置: ${video.currentTime}`);
});
video.addEventListener('error', (e) => console.error('錯誤:', e));video 標籤限制
- 只能播放完整檔案
- 不支援 HLS、DASH 等串流協定
- 不支援 DRM 保護內容
- 無法動態控制緩衝
Media Source Extensions(MSE)
概述
MSE 是 W3C 標準,讓 JavaScript 能動態生成媒體串流餵給 video 元素。MSE 是 hls.js、dash.js 等串流播放器的基礎。
MSE 工作流程
1. 建立 MediaSource
2. 將 MediaSource 綁定到 video.src
3. 等 sourceopen 事件
4. 建立 SourceBuffer
5. 下載媒體片段
6. 用 appendBuffer() 餵給 SourceBuffer
7. 播放器播放基本 MSE 實作
javascript
// 1. 建立 MediaSource
const mediaSource = new MediaSource();
const video = document.querySelector('video');
video.src = URL.createObjectURL(mediaSource);
// 2. 等 sourceopen
mediaSource.addEventListener('sourceopen', () => {
// 3. 建立 SourceBuffer
const sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');
// 4. 下載第一個片段
fetch('segment1.mp4')
.then(response => response.arrayBuffer())
.then(data => {
sourceBuffer.appendBuffer(data);
});
// 5. 監聽 updateend 繼續下載
sourceBuffer.addEventListener('updateend', () => {
if (!sourceBuffer.updating && hasMoreSegments) {
fetchNextSegment();
}
});
});
// 6. 播放
video.play();ABR 自適應位元率
MSE 可以實現 ABR:
javascript
// 依網路狀況選擇位元率
function selectBitrate(networkSpeed) {
if (networkSpeed > 5000000) return '1080p';
if (networkSpeed > 2500000) return '720p';
return '480p';
}
// 切換位元率
function switchBitrate(newBitrate) {
// 等待當前片段下載完成
sourceBuffer.addEventListener('updateend', function switchSegment() {
sourceBuffer.removeEventListener('updateend', switchSegment);
// 下載新位元率的片段
fetch(`segment_${newBitrate}.mp4`)
.then(response => response.arrayBuffer())
.then(data => sourceBuffer.appendBuffer(data));
});
}hls.js 內部用 MSE
hls.js 簡化了 MSE 的使用:
javascript
const video = document.querySelector('video');
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource('stream.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play();
});
}詳見 MPEG-DASH 完整指南 的 Dash.js 實作。
Encrypted Media Extensions(EME)
概述
EME 是 W3C 標準,讓瀏覽器播放 DRM 保護內容。Netflix、Disney+ 等串流服務用 EME 實現 DRM。
EME 工作流程
1. video 元素遇到加密內容
2. 觸發 encrypted 事件
3. JS 請求 MediaKeySystemAccess
4. 建立 MediaKeys
5. 從授權伺服器取得金鑰
6. 載入金鑰到瀏覽器
7. 瀏覽器內部 CDM 解密播放基本 EME 實作
javascript
const video = document.querySelector('video');
// 1. 請求 DRM 系統
navigator.requestMediaKeySystemAccess('com.widevine.alpha', [{
initDataTypes: ['cenc'],
videoCapabilities: [{
contentType: 'video/mp4; codecs="avc1.42E01E"',
robustness: 'SW_SECURE_CRYPTO'
}],
audioCapabilities: [{
contentType: 'audio/mp4; codecs="mp4a.40.2"',
robustness: 'SW_SECURE_CRYPTO'
}]
}])
.then(keySystemAccess => {
// 2. 建立 MediaKeys
return keySystemAccess.createMediaKeys();
})
.then(mediaKeys => {
// 3. 綁定到 video
video.setMediaKeys(mediaKeys);
// 4. 監聽加密事件
video.addEventListener('encrypted', (e) => {
const session = mediaKeys.createSession();
// 5. 向授權伺服器請求金鑰
session.addEventListener('message', (event) => {
// event.message 是挑戰,送到授權伺服器
fetch('https://license-server.com/license', {
method: 'POST',
body: event.message
})
.then(response => response.arrayBuffer())
.then(response => {
// 6. 載入金鑰
session.update(response);
});
});
// 生成請求
session.generateRequest(e.initDataType, e.initData);
});
});三大 DRM 系統
| DRM | 瀏覽器 |
|---|---|
| Widevine | Chrome、Firefox、Edge |
| FairPlay | Safari、iOS |
| PlayReady | Edge、IE |
詳見 常見影片下載問題 的 DRM 部分。
WebCodecs API
概述
WebCodecs 是 2022 年標準化的瀏覽器編解碼 API,讓 JS 直接存取編碼器和解碼器。比 MSE 更底層。
WebCodecs 用途
- 低延遲直播:自己處理編碼,不用走 RTMP
- 影片剪輯:逐幀存取
- 視訊會議:自訂編碼策略
- 機器學習:直接存取幀資料
VideoDecoder 基本
javascript
const decoder = new VideoDecoder({
output: (frame) => {
// 處理解碼後的幀
console.log('解碼幀:', frame.timestamp);
frame.close(); // 釋放記憶體
},
error: (e) => console.error('解碼錯誤:', e)
});
// 設定解碼器
decoder.configure({
codec: 'avc1.42001E', // H.264 Baseline
codedWidth: 1920,
codedHeight: 1080
});
// 解碼壓縮資料
const chunk = new EncodedVideoChunk({
type: 'key', // 或 'delta'
timestamp: 0,
data: compressedData // Uint8Array
});
decoder.decode(chunk);VideoEncoder 基本
javascript
const encoder = new VideoEncoder({
output: (chunk, metadata) => {
// 處理編碼後的資料
console.log('編碼 chunk:', chunk.type, chunk.timestamp);
},
error: (e) => console.error('編碼錯誤:', e)
});
// 設定編碼器
encoder.configure({
codec: 'avc1.42001E', // H.264 Baseline
width: 1920,
height: 1080,
bitrate: 5_000_000, // 5 Mbps
framerate: 30
});
// 從 canvas 取得幀並編碼
const canvas = document.querySelector('canvas');
const frame = new VideoFrame(canvas, { timestamp: 0 });
encoder.encode(frame, { keyFrame: true });
frame.close();WebCodecs 用於低延遲直播
javascript
// 從攝影機取得影像
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const track = stream.getVideoTracks()[0];
const processor = new MediaStreamTrackProcessor({ track });
const reader = processor.readable.getReader();
// 編碼器
const encoder = new VideoEncoder({
output: (chunk) => {
// 透過 WebRTC 或 WebSocket 發送
sendChunk(chunk);
},
error: (e) => console.error(e)
});
encoder.configure({
codec: 'avc1.42001E',
width: 1280,
height: 720,
bitrate: 2_000_000,
latencyMode: 'realtime' // 低延遲模式
});
// 持續編碼
while (true) {
const { done, value: frame } = await reader.read();
if (done) break;
encoder.encode(frame, { keyFrame: isKeyFrameTime() });
frame.close();
}瀏覽器編碼器支援
視訊編碼器
| 編碼器 | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| H.264 | ✓ | ✓ | ✓ | ✓ |
| H.265 | 部分 | 部分 | ✓ | 部分 |
| VP8 | ✓ | ✓ | 部分 | ✓ |
| VP9 | ✓ | ✓ | 部分 | ✓ |
| AV1 | ✓ | ✓ | 部分 | ✓ |
音訊編碼器
| 編碼器 | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| AAC | ✓ | ✓ | ✓ | ✓ |
| MP3 | ✓ | ✓ | ✓ | ✓ |
| Opus | ✓ | ✓ | 部分 | ✓ |
| FLAC | ✓ | ✓ | ✓ | ✓ |
詳見 H.264 vs H.265 vs AV1 編碼器比較。
各技術使用場景
1. 基本影片播放
用 HTML5 video:
html
<video src="video.mp4" controls></video>2. HLS/DASH 串流
用 MSE(透過 hls.js 或 dash.js):
javascript
// HLS
const hls = new Hls();
hls.loadSource('stream.m3u8');
hls.attachMedia(video);
// DASH
const player = dashjs.MediaPlayer().create();
player.initialize(video, 'stream.mpd', true);詳見 MP4 轉 M3U8 教學。
3. DRM 保護內容
用 EME:
javascript
// 請求 Widevine
navigator.requestMediaKeySystemAccess('com.widevine.alpha', config)
.then(keySystemAccess => keySystemAccess.createMediaKeys())
.then(mediaKeys => video.setMediaKeys(mediaKeys));4. 低延遲直播
用 WebCodecs:
javascript
// 自己處理編碼,延遲可低於 500ms
const encoder = new VideoEncoder({ /* ... */ });
encoder.configure({ latencyMode: 'realtime', /* ... */ });5. 視訊會議
用 WebRTC(內部用 WebCodecs):
javascript
// WebRTC 封裝了 WebCodecs
const pc = new RTCPeerConnection();
pc.addTrack(track);常見問題
1. video 標籤播放 MP4 失敗
原因: 編碼器或容器不相容
解決方案: 用 H.264 視訊 + AAC 音訊 + yuv420p 像素格式
2. MSE appendBuffer 失敗
原因: 編碼器不相容或資料格式錯誤
解決方案: 確認 SourceBuffer 的 MIME 類型正確
3. EME 初始化失敗
原因: DRM 系統不支援或伺服器設定問題
解決方案: 確認瀏覽器支援對應 DRM 系統
4. WebCodecs 不可用
原因: 瀏覽器版本太舊
解決方案: 用最新版 Chrome 或 Firefox
總結
瀏覽器影片播放技術:
- HTML5 video:基本播放
- MSE:動態串流(HLS、DASH)
- EME:DRM 保護
- WebCodecs:低階編解碼
選擇指南:
- 簡單播放: HTML5 video
- 串流播放: MSE + hls.js/dash.js
- DRM 內容: EME
- 低延遲直播: WebCodecs
- 視訊會議: WebRTC
快速參考:
- MSE:
new MediaSource()+addSourceBuffer() - EME:
requestMediaKeySystemAccess()+MediaKeys - WebCodecs:
new VideoDecoder()+new VideoEncoder()