使用 WebCodecs 處理影片

操控影片串流元件。

Eugene Zemtsov
Eugene Zemtsov
François Beaufort
François Beaufort

現代化的網路技術提供多種影片製作方式。 Media Stream APIMedia Recording API Media Source APIWebRTC API 增加 是一項豐富的工具集,可用於錄製、傳輸及播放影片串流。 這些 API 在解決特定高階工作時,無法讓使用者 程式設計師會與影片串流的個別元件 (如影格) 合作 和非混合編碼影片或音訊片段 為了取得這些基本元件的低階存取權,開發人員一直以來都使用 WebAssembly 可將影片和音訊轉碼器匯入瀏覽器。但有了 新世代瀏覽器已經搭載許多轉碼器 ( WebAssembly 是硬體加速的 人類和電腦資源

WebCodecs API 可排除這種效率不彰的做法 讓程式設計師能夠使用 。詳細說明:

  • 影片和音訊解碼器
  • 影片和音訊編碼器
  • 原始影片影格
  • 圖片解碼器

WebCodecs API 非常適合需要完全掌控 視媒體內容的處理方式,例如視訊編輯器、視訊會議、視訊 即時串流等

影片處理工作流程

影格是影片處理過程中的核心部分。因此,在 WebCodecs 中 會消耗或產生影格影片編碼器會將影格轉換為編碼格式 分為多個片段反之,影片解碼器則相反。

此外,VideoFrame 可藉由做為 CanvasImageSource 且具有接受 CanvasImageSource建構函式,與其他 Web API 完美搭配運作。 因此可用於 drawImage()texImage2D() 等函式。也可以利用畫布、點陣圖、影片元素和其他影片影格建構。

WebCodecs API 可與 Insertable Streams API 的類別搭配使用 可將 WebCodecs 連結至媒體串流音軌

  • MediaStreamTrackProcessor 會將媒體音軌拆解成個別影格。
  • MediaStreamTrackGenerator 會從影格串流建立媒體音軌。

WebCodecs 和網路工作站

設計 WebCodecs API 後,所有繁重工作都會以非同步方式,在主執行緒外執行。 但由於影格和區塊回呼通常可以每秒多次呼叫, 可能會讓主執行緒變得雜亂無章,進而導致網站的回應速度變慢。 因此,最好將個別影格和編碼區塊的處理方式移至 網路工作站。

為解決這個問題,ReadableStream 可自動傳輸所有來自媒體的影格 向 worker 提交追蹤舉例來說,MediaStreamTrackProcessor 可用來取得 為來自網路攝影機的媒體串流音軌 ReadableStream。之後 串流會傳輸到網路工作站,其中會逐一讀取影格並排入佇列 放入 VideoEncoder 中。

使用 HTMLCanvasElement.transferControlToOffscreen 甚至可以透過主執行緒完成轉譯。但如果所有高階工具都採用 操作起來比較不方便,VideoFrame 本身可以轉讓, 並在工作站之間移動。

WebCodecs 實際操作

編碼

從 Canvas 或 ImageBitmap 到網路或儲存空間的路徑
CanvasImageBitmap 至網路或儲存空間的路徑

一切都以 VideoFrame 開頭。 建構影片影格的方式有三種。

  • 圖片來源包含圖片來源,例如畫布、圖片點陣圖或影片元素。

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • 使用 MediaStreamTrackProcessorMediaStreamTrack 提取影格

    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);
    

無論影格來源為何,都可以將影格編碼為 具有 VideoEncoderEncodedVideoChunk 物件。

在編碼之前,需要提供兩個 JavaScript 物件 VideoEncoder

  • 包含兩個函式,用於處理已編碼區塊和 發生錯誤。這些函式是由開發人員定義,之後即無法變更 會傳遞至 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();

解碼中

從網路或儲存空間到 Canvas 或 ImageBitmap 的路徑。
從網路或儲存空間到 CanvasImageBitmap 的路徑。

設定 VideoDecoderVideoEncoder:解碼器建立時會傳送兩個函式,轉碼器 參數傳遞至 configure()

每組轉碼器參數都有不同的轉碼器。例如 H.264 轉碼器 可能需要二進位 blob 的 AVCC 格式。除非編碼為 Annex 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()) 可快速傳回值在以下範例中,它只會將影格新增至 準備轉譯的幾個影格 轉譯作業會獨立進行,包含兩個步驟:

  1. 等待適當時機顯示影格。
  2. 在畫布上繪製影格。

不再需要影格後,請呼叫 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);
}

開發人員秘訣

使用媒體面板 查看媒體記錄並對 WebCodecs 偵錯。

用於對 WebCodecs 偵錯的媒體面板螢幕截圖
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 回報錯誤。 請盡可能提供最詳細的細節和簡單的說明 請在「Components」(元件) 方塊中輸入 Blink>Media>WebCodecsGlitch 有便捷的報復工具,

顯示對 API 的支援

您是否打算使用 WebCodecs API?你的公開支援讓 優先考慮各項功能,並向其他瀏覽器供應商說明其重要性 就必須支持他們

將電子郵件傳送到 media-dev@chromium.org 或傳送推文 使用主題標記套用至 @ChromiumDev #WebCodecs ,並說明你使用這項服務的位置和方式。

主頁橫幅製作者: 丹妮絲 Unsplash 頁面。