Xử lý video bằng WebCodecs

Thao túng các thành phần trong luồng video.

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

Các công nghệ web hiện đại mang đến nhiều cách làm việc với video. API Luồng nội dung nghe nhìn, API Ghi nội dung nghe nhìn. API nguồn nội dung nghe nhìn, và API WebRTC thành một bộ công cụ đa dạng dùng để ghi, chuyển và phát video trực tuyến. Trong khi giải quyết một số nhiệm vụ cấp cao, các API này không cho phép các lập trình viên làm việc với các thành phần riêng lẻ của luồng video như khung và các đoạn video hoặc âm thanh chưa được mã hoá. Để có quyền truy cập cấp thấp vào những thành phần cơ bản này, nhà phát triển đã sử dụng WebAssembly để đưa bộ mã hoá và giải mã video và âm thanh vào trình duyệt. Nhưng được cung cấp các trình duyệt hiện đại đã gửi kèm nhiều bộ mã hoá và giải mã (thường được tăng tốc bằng phần cứng), việc đóng gói lại chúng vì WebAssembly có vẻ lãng phí con người và máy tính.

API WebCodecs loại bỏ sự kém hiệu quả này bằng cách cung cấp cho các lập trình viên cách sử dụng các thành phần phương tiện đã có trong trình duyệt. Cụ thể:

  • Bộ giải mã video và âm thanh
  • Bộ mã hoá video và âm thanh
  • Khung hình video thô
  • Bộ giải mã hình ảnh

WebCodecs API rất hữu ích cho các ứng dụng web yêu cầu toàn quyền kiểm soát đối với cách xử lý nội dung truyền thông, chẳng hạn như trình chỉnh sửa video, hội nghị truyền hình, phát trực tuyến, v.v.

Quy trình xử lý video

Khung hình là thành phần trung tâm trong quá trình xử lý video. Do đó, trong WebCodecs, hầu hết các lớp sử dụng hoặc tạo khung hình. Bộ mã hoá video chuyển đổi khung hình thành khung hình được mã hoá phân đoạn. Bộ giải mã video có chức năng ngược lại.

Ngoài ra, VideoFrame hoạt động tốt với các API web khác bằng cách là CanvasImageSource và có một hàm khởi tạo chấp nhận CanvasImageSource. Vì vậy, bạn có thể dùng thuộc tính này trong các hàm như drawImage()texImage2D(). Ngoài ra, thành phần này có thể được xây dựng từ canvas, bitmap, phần tử video và các khung video khác.

WebCodecs API hoạt động tốt song song với các lớp trong API Luồng có thể chèn giúp kết nối WebCodecs với các bản nhạc luồng nội dung nghe nhìn.

  • MediaStreamTrackProcessor chia các bản nhạc nội dung đa phương tiện thành từng khung hình riêng lẻ.
  • MediaStreamTrackGenerator tạo một bản nhạc nội dung nghe nhìn từ một luồng khung hình.

WebCodec và nhân viên web

Theo thiết kế, API WebCodecs thực hiện tất cả các công việc khó khăn một cách không đồng bộ và ra khỏi luồng chính. Nhưng vì các lệnh gọi lại khung và phân đoạn thường có thể được gọi nhiều lần trong một giây, chúng có thể làm lộn xộn luồng chính, từ đó làm cho trang web kém phản hồi hơn. Do đó, bạn nên di chuyển việc xử lý từng khung hình riêng lẻ và các đoạn được mã hoá thành một nhân viên web.

Để giúp bạn làm việc đó, ReadableStream cung cấp một cách thuận tiện để tự động chuyển tất cả khung hình từ nội dung nghe nhìn theo dõi đối với worker. Ví dụ: bạn có thể dùng MediaStreamTrackProcessor để lấy một ReadableStream cho bản nhạc luồng nội dung nghe nhìn đến từ máy ảnh web. Sau đó luồng được chuyển đến một worker web, trong đó các khung được đọc từng khung một và được đưa vào hàng đợi vào VideoEncoder.

Với HTMLCanvasElement.transferControlToOffscreen, bạn thậm chí có thể kết xuất cả ngoài luồng chính. Nhưng nếu tất cả các công cụ cấp cao đều thực sự bất tiện, bản thân VideoFrame có thể chuyển nhượng và có thể được di chuyển giữa các trình thực thi.

WebCodec trong thực tế

Mã hoá

Đường dẫn từ Canvas hoặc ImageBitmap đến mạng hoặc đến bộ nhớ
Đường dẫn từ Canvas hoặc ImageBitmap đến mạng hoặc đến bộ nhớ

Tất cả đều bắt đầu bằng một VideoFrame. Có 3 cách để tạo khung video.

  • Từ một nguồn hình ảnh như canvas, bitmap hình ảnh hoặc phần tử video.

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • Sử dụng MediaStreamTrackProcessor để lấy khung hình từ 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;
    }
    
  • Tạo một khung từ cách biểu diễn pixel nhị phân của nó trong 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);
    

Bất kể khung hình đến từ đâu, khung hình đều có thể được mã hoá thành Đối tượng EncodedVideoChunkVideoEncoder.

Trước khi mã hoá, VideoEncoder cần được cung cấp hai đối tượng JavaScript:

  • Khởi tạo từ điển với hai hàm để xử lý các đoạn mã hoá và . Các hàm này do nhà phát triển xác định và không thể thay đổi sau chúng sẽ được truyền đến hàm khởi tạo VideoEncoder.
  • Đối tượng cấu hình bộ mã hoá chứa các tham số cho đầu ra luồng video. Bạn có thể thay đổi các tham số này sau bằng cách gọi configure().

Phương thức configure() sẽ gửi NotSupportedError nếu cấu hình không được mà trình duyệt hỗ trợ. Bạn nên gọi phương thức tĩnh VideoEncoder.isConfigSupported() với cấu hình để kiểm tra trước xem cấu hình này được hỗ trợ và chờ đợi cấu hình được hứa hẹn.

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.
}

Sau khi thiết lập, bộ mã hoá có thể chấp nhận khung hình thông qua phương thức encode(). Cả configure()encode() đều sẽ quay lại ngay lập tức mà không cần đợi công việc thực tế cần hoàn thành. Tính năng này cho phép một vài khung hình xếp hàng đợi để mã hoá ở encodeQueueSize cho biết số lượng yêu cầu đang chờ trong hàng đợi để các mã hoá trước đó kết thúc. Lỗi được báo cáo bằng cách ngay lập tức gửi một ngoại lệ, trong trường hợp các đối số hoặc thứ tự của các lệnh gọi phương thức vi phạm hợp đồng API, hoặc bằng cách gọi error() lệnh gọi lại cho các sự cố gặp phải trong quá trình triển khai bộ mã hoá và giải mã. Nếu quá trình mã hoá hoàn tất thành công, output() lệnh gọi lại được gọi với một đoạn mã hoá mới làm đối số. Một chi tiết quan trọng khác ở đây là khung hình cần được thông báo khi không có cần nhiều thời gian hơn bằng cách gọi 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();
  }
}

Cuối cùng, đã đến lúc hoàn tất mã hoá bằng cách viết một hàm xử lý các đoạn video đã mã hoá khi chúng đi ra khỏi bộ mã hoá. Thông thường, hàm này sẽ gửi các đoạn dữ liệu qua mạng hoặc kết hợp chúng vào một phương tiện vùng chứa để lưu trữ.

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

Nếu tại một thời điểm nào đó, bạn cần đảm bảo rằng tất cả các yêu cầu mã hoá đang chờ xử lý đã đã hoàn tất, bạn có thể gọi flush() và chờ lời hứa.

await encoder.flush();

Giải mã

Đường dẫn từ mạng hoặc bộ nhớ đến Canvas hoặc ImageBitmap.
Đường dẫn từ mạng hoặc bộ nhớ đến Canvas hoặc ImageBitmap.

Việc thiết lập VideoDecoder tương tự như những gì đã thực hiện cho VideoEncoder: hai hàm được truyền khi bộ giải mã được tạo và bộ mã hoá và giải mã tham số được cấp cho configure().

Các bộ tham số của bộ mã hoá và giải mã sẽ thay đổi theo từng bộ mã hoá và giải mã. Ví dụ: bộ mã hoá và giải mã H.264 có thể cần một blob nhị phân của AVCC, trừ phi được mã hoá theo định dạng Phụ lục 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.
}

Sau khi khởi chạy bộ giải mã, bạn có thể bắt đầu cung cấp bộ giải mã bằng các đối tượng EncodedVideoChunk. Để tạo một đoạn, bạn cần có:

  • BufferSource dữ liệu video đã mã hoá
  • dấu thời gian bắt đầu của phân đoạn tính bằng micrô giây (thời gian phát nội dung đa phương tiện của khung được mã hoá đầu tiên trong phân đoạn)
  • loại phân đoạn, một trong:
    • key nếu đoạn có thể được giải mã độc lập với các đoạn trước
    • delta nếu đoạn chỉ có thể được giải mã sau khi một hoặc nhiều đoạn trước đó đã được giải mã

Ngoài ra, bất kỳ đoạn nào do bộ mã hoá đưa ra cũng đã sẵn sàng cho bộ giải mã như nguyên trạng. Tất cả những điều đã nói ở trên về báo cáo lỗi và tính chất không đồng bộ phương thức của bộ mã hoá cũng đúng như nhau đối với bộ giải mã.

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

Bây giờ, đã đến lúc cho thấy cách hiển thị một khung được giải mã mới trên trang. Bây giờ để đảm bảo rằng lệnh gọi lại đầu ra của bộ giải mã (handleFrame()) nhanh chóng quay trở lại. Trong ví dụ bên dưới, nó chỉ thêm một khung vào hàng đợi của khung hình sẵn sàng để kết xuất. Quá trình kết xuất diễn ra riêng biệt và bao gồm 2 bước:

  1. Chờ thời điểm thích hợp để hiển thị khung hình.
  2. Vẽ khung trên canvas.

Khi không cần khung hình nữa, hãy gọi close() để giải phóng bộ nhớ cơ bản trước khi bộ thu gom rác đến được, việc này sẽ làm giảm lượng bộ nhớ được ứng dụng web sử dụng.

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

Mẹo dành cho nhà phát triển

Sử dụng Bảng điều khiển nội dung nghe nhìn trong Công cụ của Chrome cho nhà phát triển để xem nhật ký phương tiện và gỡ lỗi WebCodec.

Ảnh chụp màn hình Bảng điều khiển nội dung nghe nhìn để gỡ lỗi WebCodecs
Bảng điều khiển nội dung nghe nhìn trong Công cụ của Chrome cho nhà phát triển để gỡ lỗi WebCodec.

Bản minh hoạ

Bản minh hoạ dưới đây cho thấy cách tạo khung ảnh động trong canvas:

  • được MediaStreamTrackProcessor chụp ở tốc độ 25 khung hình/giây vào ReadableStream
  • được chuyển sang một nhân viên web
  • được mã hoá thành định dạng video H.264
  • được giải mã lại thành một chuỗi các khung video
  • và hiển thị trên canvas thứ hai bằng cách sử dụng transferControlToOffscreen()

Các bản minh hoạ khác

Ngoài ra, hãy xem các bản minh hoạ khác của chúng tôi:

Sử dụng API WebCodecs

Phát hiện tính năng

Cách kiểm tra tính năng hỗ trợ của WebCodecs:

if ('VideoEncoder' in window) {
  // WebCodecs API is supported.
}

Xin lưu ý rằng API WebCodecs chỉ dùng được trong bối cảnh bảo mật, vì vậy, việc phát hiện sẽ không thành công nếu self.isSecureContext là false.

Phản hồi

Nhóm Chrome muốn biết ý kiến của bạn về trải nghiệm với API WebCodecs.

Cho chúng tôi biết về thiết kế API

Có điều gì về API không hoạt động như bạn mong đợi không? Hoặc có thiếu phương thức hoặc thuộc tính nào mà bạn cần để triển khai ý tưởng của mình không? Có bạn có câu hỏi hoặc nhận xét gì về mô hình bảo mật không? Báo cáo vấn đề về thông số kỹ thuật trên kho lưu trữ GitHub tương ứng, hoặc thêm suy nghĩ của bạn về vấn đề hiện tại.

Báo cáo sự cố về triển khai

Bạn có phát hiện lỗi trong quá trình triển khai Chrome không? Hoặc là triển khai khác với thông số kỹ thuật không? Báo cáo lỗi tại new.crbug.com. Hãy nhớ cung cấp càng nhiều thông tin chi tiết càng tốt, hướng dẫn đơn giản để tái tạo và nhập Blink>Media>WebCodecs vào hộp Components (Thành phần). Glitch rất hữu ích khi chia sẻ các bản dựng lại nhanh chóng và dễ dàng.

Hiện thông tin hỗ trợ về API này

Bạn có định sử dụng API WebCodecs không? Sự hỗ trợ công khai của bạn giúp Nhóm Chrome ưu tiên các tính năng và cho các nhà cung cấp trình duyệt khác thấy mức độ quan trọng đó là hỗ trợ họ.

Gửi email đến media-dev@chromium.org hoặc gửi bài đăng trên Twitter tới @ChromiumDev bằng cách sử dụng hashtag #WebCodecs đồng thời cho chúng tôi biết bạn đang sử dụng ở đâu và như thế nào.

Hình ảnh chính của Tháng 1 chuyên biệt trên Unsplash.