操控影片串流元件。
現代網路技術提供多種處理影片的方式。Media Stream API、Media Recording API、Media Source API 和 WebRTC API 組合起來,就是一套用於錄製、傳輸和播放串流影片的豐富工具組合。這些 API 可解決特定高層級工作,但不允許網頁程式設計師使用影片串流的個別元件,例如影格和未經多路復用編碼的影片或音訊區塊。為了低階存取這些基本元件,開發人員一直使用 WebAssembly 在瀏覽器中加入影片和音訊轉碼器。不過,由於新式瀏覽器已提供各種編解碼器 (通常由硬體加速),因此將這些編解碼器重新包裝為 WebAssembly 似乎會浪費人力和電腦資源。
WebCodecs API 可讓程式設計師使用瀏覽器中現有的媒體元件,藉此消除這種低效率問題。詳細說明:
- 影片和音訊解碼器
- 影片和音訊編碼器
- 原始影片影格
- 圖片解碼器
WebCodecs API 適用於需要完全控制媒體內容處理方式的網頁應用程式,例如影片編輯器、視訊會議、影片串流等。
影片處理工作流程
影格是影片處理作業的核心,因此,在 WebCodecs 中,大多數類別都會使用或產生影格。影片編碼器會將影格轉換為已編碼的區塊。而影片解碼器則是相反。
此外,VideoFrame
可藉由做為 CanvasImageSource
且具有可接受 CanvasImageSource
的constructor,可與其他 Web API 完美搭配運作。因此,您可以在 drawImage()
和 texImage2D()
等函式中使用此函式。也可以利用畫布、點陣圖、影片元素和其他影片影格建構。
WebCodecs API 可與 Insertable Streams API 的類別搭配使用,將 WebCodecs 連結至媒體串流音軌。
MediaStreamTrackProcessor
會將媒體音軌拆解為個別影格。MediaStreamTrackGenerator
會從影格串流建立媒體音軌。
WebCodecs 和 Web Workers
根據設計,WebCodecs API 會以非同步方式執行所有繁重工作,並在主執行緒外執行。不過,由於影格和區塊回呼通常每秒可呼叫多次,因此可能會造成主執行緒雜亂,進而導致網站的回應速度變慢。因此,建議將個別影格和已編碼的區塊處理作業移至 Web worker。
為方便起見,ReadableStream 提供簡單的方式,可將來自媒體追蹤的所有影格自動傳輸到 worker。舉例來說,MediaStreamTrackProcessor
可用於取得來自網路攝影機的媒體串流音軌的 ReadableStream
。之後,這項作業會將串流傳送至 Web Worker,在該處逐一讀取影格,並將影格排入 VideoEncoder
的佇列。
有了 HTMLCanvasElement.transferControlToOffscreen
,您甚至可以在主執行緒外執行算繪作業。但如果所有高階工具都變得不方便使用,VideoFrame
本身就會可轉移,並且可以在工作站之間移動。
WebCodecs 的實際應用
編碼
一切都以 VideoFrame
開頭。建構影片影格的方式有三種。
從圖片來源 (例如畫布、圖片位圖或影片元素) 擷取。
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
使用
MediaStreamTrackProcessor
從MediaStreamTrack
提取影格const stream = await navigator.mediaDevices.getUserMedia({…}); const track = stream.getTracks()[0]; const trackProcessor = new MediaStreamTrackProcessor(track); const reader = trackProcessor.readable.getReader(); while (true) { const result = await reader.read(); if (result.done) break; const frameFromCamera = result.value; }
根據
BufferSource
中的二進位像素表示法建立影格const pixelSize = 4; const init = { timestamp: 0, codedWidth: 320, codedHeight: 200, format: "RGBA", }; const data = new Uint8Array(init.codedWidth * init.codedHeight * pixelSize); for (let x = 0; x < init.codedWidth; x++) { for (let y = 0; y < init.codedHeight; y++) { const offset = (y * init.codedWidth + x) * pixelSize; data[offset] = 0x7f; // Red data[offset + 1] = 0xff; // Green data[offset + 2] = 0xd4; // Blue data[offset + 3] = 0x0ff; // Alpha } } const frame = new VideoFrame(data, init);
無論來源為何,影格都可以透過 VideoEncoder
編碼至 EncodedVideoChunk
物件。
在編碼前,VideoEncoder
需要提供兩個 JavaScript 物件:
- 使用兩個函式初始化字典,以便處理已編碼的區塊和錯誤。這些函式是由開發人員定義,且在傳遞至
VideoEncoder
建構函式後就無法變更。 - 編碼器設定物件,其中包含輸出影片串流的參數。您之後可以呼叫
configure()
變更這些參數。
如果瀏覽器不支援設定,configure()
方法就會擲回 NotSupportedError
。建議您使用設定呼叫靜態方法 VideoEncoder.isConfigSupported()
,以便事先檢查是否支援設定,並等待承諾。
const init = {
output: handleChunk,
error: (e) => {
console.log(e.message);
},
};
const config = {
codec: "vp8",
width: 640,
height: 480,
bitrate: 2_000_000, // 2 Mbps
framerate: 30,
};
const { supported } = await VideoEncoder.isConfigSupported(config);
if (supported) {
const encoder = new VideoEncoder(init);
encoder.configure(config);
} else {
// Try another config.
}
編碼器設定完成後,即可透過 encode()
方法接受影格。configure()
和 encode()
都會立即傳回,不會等待實際工作完成。這可讓多個影格同時排入編碼佇列,而 encodeQueueSize
則會顯示佇列中等待先前編碼完成的請求數量。當引數或方法呼叫順序違反 API 合約時,系統會立即擲回例外狀況來回報錯誤,或是呼叫 error()
回呼,以便處理編解碼實作中遇到的問題。如果編碼順利完成,系統會呼叫 output()
回呼,並將新的已編碼區塊做為引數。這裡的另一個重要細節是,您需要呼叫 close()
來通知不再需要影格的情況。
let frameCounter = 0;
const track = stream.getVideoTracks()[0];
const trackProcessor = new MediaStreamTrackProcessor(track);
const reader = trackProcessor.readable.getReader();
while (true) {
const result = await reader.read();
if (result.done) break;
const frame = result.value;
if (encoder.encodeQueueSize > 2) {
// Too many frames in flight, encoder is overwhelmed
// let's drop this frame.
frame.close();
} else {
frameCounter++;
const keyFrame = frameCounter % 150 == 0;
encoder.encode(frame, { keyFrame });
frame.close();
}
}
最後,編寫函式來處理從編碼器輸出的已編碼影片區塊,完成編碼程式碼。此函式通常是透過網路傳送資料區塊,或是將資料區塊多工至媒體容器中儲存。
function handleChunk(chunk, metadata) {
if (metadata.decoderConfig) {
// Decoder needs to be configured (or reconfigured) with new parameters
// when metadata has a new decoderConfig.
// Usually it happens in the beginning or when the encoder has a new
// codec specific binary configuration. (VideoDecoderConfig.description).
fetch("/upload_extra_data", {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: metadata.decoderConfig.description,
});
}
// actual bytes of encoded data
const chunkData = new Uint8Array(chunk.byteLength);
chunk.copyTo(chunkData);
fetch(`/upload_chunk?timestamp=${chunk.timestamp}&type=${chunk.type}`, {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: chunkData,
});
}
如果您需要確保所有待處理的編碼要求都已完成,可以呼叫 flush()
並等待其承諾。
await encoder.flush();
解碼
設定 VideoDecoder
的做法與 VideoEncoder
類似:建立解碼器時會傳遞兩個函式,並將編碼器參數提供給 configure()
。
轉碼器參數組合因轉碼器而異。舉例來說,H.264 轉碼器可能需要 AVCC 的二進位資料 blob,除非是以所謂的附錄 B 格式 (encoderConfig.avc = { format: "annexb" }
) 編碼。
const init = {
output: handleFrame,
error: (e) => {
console.log(e.message);
},
};
const config = {
codec: "vp8",
codedWidth: 640,
codedHeight: 480,
};
const { supported } = await VideoDecoder.isConfigSupported(config);
if (supported) {
const decoder = new VideoDecoder(init);
decoder.configure(config);
} else {
// Try another config.
}
解碼器初始化後,您就可以開始使用 EncodedVideoChunk
物件進行餵入。如要建立區塊,您需要:
- 已編碼影片資料的
BufferSource
- 區塊的開始時間戳記 (以微秒為單位) (區塊中第一個已編碼影格中的媒體時間)
- 區塊的類型,可選項目如下:
key
若區塊可獨立於先前的區塊解碼delta
如果只有在解碼一或多個先前區塊後,才能解碼該區塊
此外,編碼器發出的任何區塊都可以讓解碼器能使用。對於解碼器來說,上述有關錯誤報告和編碼器方法的非同步性質所討論的一切事項,同樣適用於解碼器。
const responses = await downloadVideoChunksFromServer(timestamp);
for (let i = 0; i < responses.length; i++) {
const chunk = new EncodedVideoChunk({
timestamp: responses[i].timestamp,
type: responses[i].key ? "key" : "delta",
data: new Uint8Array(responses[i].body),
});
decoder.decode(chunk);
}
await decoder.flush();
現在要展示如何在網頁上顯示新解碼的頁框。建議您確保解碼器輸出回呼 (handleFrame()
) 能快速傳回。在下方範例中,它只會將影格新增至可供轉譯的影格佇列。轉譯作業會分開進行,並包含兩個步驟:
- 等待適當的時間顯示影格。
- 在畫布上繪製影格。
不再需要影格時,請呼叫 close()
釋出基礎記憶體,再進行垃圾收集器存取,這樣可減少網頁應用程式使用的平均記憶體量。
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let pendingFrames = [];
let underflow = true;
let baseTime = 0;
function handleFrame(frame) {
pendingFrames.push(frame);
if (underflow) setTimeout(renderFrame, 0);
}
function calculateTimeUntilNextFrame(timestamp) {
if (baseTime == 0) baseTime = performance.now();
let mediaTime = performance.now() - baseTime;
return Math.max(0, timestamp / 1000 - mediaTime);
}
async function renderFrame() {
underflow = pendingFrames.length == 0;
if (underflow) return;
const frame = pendingFrames.shift();
// Based on the frame's timestamp calculate how much of real time waiting
// is needed before showing the next frame.
const timeUntilNextFrame = calculateTimeUntilNextFrame(frame.timestamp);
await new Promise((r) => {
setTimeout(r, timeUntilNextFrame);
});
ctx.drawImage(frame, 0, 0);
frame.close();
// Immediately schedule rendering of the next frame
setTimeout(renderFrame, 0);
}
開發人員秘訣
使用 Chrome 開發人員工具中的媒體面板查看媒體記錄並為 WebCodecs 偵錯。
示範
以下示範如何使用畫布中的動畫影格:
MediaStreamTrackProcessor
以 25 FPS 擷取到ReadableStream
- 已轉移至網路工作者
- 已編碼為 H.264 影片格式
- 再次解碼為一系列的影片影格
- 並使用
transferControlToOffscreen()
在第二個畫布上算繪
其他示範
另請觀賞其他示範:
使用 WebCodecs API
特徵偵測
如何檢查 WebCodecs 支援:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
請注意,WebCodecs API 僅適用於安全情境,因此如果 self.isSecureContext
為 false,偵測作業就會失敗。
意見回饋
Chrome 小組想瞭解您的 WebCodecs API 使用體驗。
請說明 API 設計
您覺得這個 API 有什麼不如預期的運作方式?或者,您是否缺少實作想法所需的方法或屬性?對安全性模型有疑問或意見嗎?在對應的 GitHub 存放區中提出規格問題,或在現有問題中加入您的想法。
回報實作問題
你是否發現 Chrome 實作項目有錯誤?還是實作方式與規格不同?請前往 new.crbug.com 回報錯誤。請務必盡可能提供詳細資料,提供簡單的複現操作說明,並在「元件」方塊中輸入 Blink>Media>WebCodecs
。Glitch 可讓您輕鬆快速地分享重現內容。
顯示 API 支援
您打算使用 WebCodecs API 嗎?您的公開支持有助於 Chrome 團隊決定功能優先順序,並向其他瀏覽器供應商顯示支援這些功能的重要性。
請傳送電子郵件至 media-dev@chromium.org,或使用主題標記 #WebCodecs
發送推文給 @ChromiumDev,並告訴我們你在何處使用這項功能,以及使用方式。
主頁橫幅,圖片來源:Denise Jans,圖片來源:Unsplash。