Pemrosesan video dengan WebCodec

Memanipulasi komponen streaming video.

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

Teknologi web modern menyediakan banyak cara untuk bekerja dengan video. Media Stream API, Media Recording API, Media Source API, dan WebRTC API menghadirkan rangkaian alat lengkap untuk merekam, mentransfer, dan memutar streaming video. Meskipun memecahkan tugas tingkat tinggi tertentu, API ini tidak mengizinkan pemrogram web bekerja dengan komponen individual streaming video seperti frame dan potongan video atau audio yang dienkode dan tidak dimuxasi. Untuk mendapatkan akses tingkat rendah ke komponen dasar ini, developer telah menggunakan WebAssembly untuk menghadirkan codec video dan audio ke browser. Namun, mengingat bahwa browser modern sudah diluncurkan dengan berbagai codec (yang sering kali diakselerasi oleh hardware), mengemas ulang browser tersebut karena WebAssembly tampak seperti pemborosan resource komputer dan manusia.

WebCodecs API menghilangkan inefisiensi ini dengan memberi programmer cara untuk menggunakan komponen media yang sudah ada di browser. Khususnya:

  • Decoder video dan audio
  • Encoder video dan audio
  • Frame video mentah
  • Decoder gambar

WebCodecs API berguna untuk aplikasi web yang memerlukan kontrol penuh atas cara pemrosesan konten media, seperti editor video, konferensi video, streaming video, dll.

Alur kerja pemrosesan video

Bingkai adalah elemen utama dalam pemrosesan video. Jadi, di WebCodecs, sebagian besar class menggunakan atau menghasilkan frame. Encoder video mengonversi frame menjadi bagian yang dienkode. Decoder video melakukan hal yang sebaliknya.

Selain itu, VideoFrame berfungsi baik dengan Web API lainnya dengan menjadi CanvasImageSource dan memiliki konstruktor yang menerima CanvasImageSource. Agar dapat digunakan dalam fungsi seperti drawImage() dan texImage2D(). Elemen ini juga dapat dibuat dari kanvas, bitmap, elemen video, dan frame video lainnya.

WebCodecs API dapat berfungsi dengan baik bersama class dari Insertable Streams API yang menghubungkan WebCodecs ke trek streaming media.

  • MediaStreamTrackProcessor membagi trek media menjadi frame individual.
  • MediaStreamTrackGenerator membuat trek media dari aliran frame.

WebCodecs dan web worker

Secara desain, WebCodecs API melakukan semua tugas berat secara asinkron dan di luar thread utama. Namun, karena callback frame dan potongan sering kali dapat dipanggil beberapa kali per detik, callback dapat mengacaukan thread utama dan membuat situs kurang responsif. Oleh karena itu, sebaiknya pindahkan penanganan setiap frame dan potongan yang dienkode ke pekerja web.

Untuk membantu melakukannya, ReadableStream menyediakan cara yang mudah untuk mentransfer semua frame yang berasal dari trek media ke pekerja secara otomatis. Misalnya, MediaStreamTrackProcessor dapat digunakan untuk mendapatkan ReadableStream untuk trek streaming media yang berasal dari kamera web. Setelah itu, streaming akan ditransfer ke pekerja web tempat frame dibaca satu per satu dan diantrekan ke dalam VideoEncoder.

Dengan HTMLCanvasElement.transferControlToOffscreen, rendering bahkan dapat dilakukan di luar thread utama. Namun, jika semua alat tingkat tinggi ternyata merepotkan, VideoFrame itu sendiri dapat ditransfer dan dapat dipindahkan antar-pekerja.

Cara kerja WebCodecs

Encoding

Jalur dari Canvas atau ImageBitmap ke jaringan atau ke penyimpanan
Jalur dari Canvas atau ImageBitmap ke jaringan atau ke penyimpanan

Semuanya dimulai dengan VideoFrame. Ada tiga cara untuk membuat {i>frame<i} video.

  • Dari sumber gambar seperti kanvas, bitmap gambar, atau elemen video.

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • Gunakan MediaStreamTrackProcessor untuk mengambil frame dari 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;
    }
    
  • Buat frame dari representasi piksel binernya dalam 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);
    

Dari mana pun asalnya, frame dapat dienkode ke objek EncodedVideoChunk dengan VideoEncoder.

Sebelum encoding, VideoEncoder harus diberikan dua objek JavaScript:

  • Kamus init dengan dua fungsi untuk menangani potongan dan error yang dienkode. Fungsi ini ditentukan oleh developer dan tidak dapat diubah setelah diteruskan ke konstruktor VideoEncoder.
  • Objek konfigurasi encoder, yang berisi parameter untuk streaming video output. Anda dapat mengubah parameter ini nanti dengan memanggil configure().

Metode configure() akan menampilkan NotSupportedError jika konfigurasi tidak didukung oleh browser. Sebaiknya panggil metode statis VideoEncoder.isConfigSupported() dengan konfigurasi untuk memeriksa terlebih dahulu apakah konfigurasi didukung dan menunggu promisenya.

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

Setelah disiapkan, encoder siap menerima frame melalui metode encode(). configure() dan encode() akan segera ditampilkan tanpa menunggu pekerjaan yang sebenarnya selesai. Metode ini memungkinkan beberapa frame mengantrekan encoding secara bersamaan, sementara encodeQueueSize menampilkan jumlah permintaan yang menunggu dalam antrean hingga encoding sebelumnya selesai. Error dilaporkan dengan segera menampilkan pengecualian, jika argumen atau urutan panggilan metode melanggar kontrak API, atau dengan memanggil callback error() untuk masalah yang dihadapi dalam implementasi codec. Jika encoding berhasil, callback output() akan dipanggil dengan potongan berenkode baru sebagai argumen. Detail penting lainnya di sini adalah bahwa frame perlu diberi tahu saat tidak lagi diperlukan dengan memanggil 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();
  }
}

Terakhir, saatnya menyelesaikan encoding kode dengan menulis fungsi yang menangani bagian video yang dienkode saat keluar dari encoder. Biasanya, fungsi ini akan mengirim potongan data melalui jaringan atau menggabungkan potongan data tersebut ke dalam penampung media untuk disimpan.

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

Jika suatu saat Anda perlu memastikan bahwa semua permintaan encoding yang tertunda telah selesai, Anda dapat memanggil flush() dan menunggu janjinya.

await encoder.flush();

Decoding

Jalur dari jaringan atau penyimpanan ke Canvas atau ImageBitmap.
Jalur dari jaringan atau penyimpanan ke Canvas atau ImageBitmap.

Penyiapan VideoDecoder mirip dengan yang telah dilakukan untuk VideoEncoder: dua fungsi diteruskan saat decoder dibuat, dan parameter codec diberikan ke configure().

Kumpulan parameter codec bervariasi dari satu codec ke codec lainnya. Misalnya codec H.264 mungkin memerlukan blob biner dari AVCC, kecuali jika dienkode dalam format Lampiran 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.
}

Setelah decoder diinisialisasi, Anda dapat mulai memasukkannya dengan objek EncodedVideoChunk. Untuk membuat potongan, Anda perlu:

  • BufferSource data video yang dienkode
  • stempel waktu mulai potongan dalam mikrodetik (waktu media {i>frame<i} pertama yang dienkode dalam potongan tersebut)
  • jenis potongan, salah satu dari:
    • key jika potongan dapat didekode secara independen dari potongan sebelumnya
    • delta jika potongan hanya dapat didekode setelah satu atau beberapa potongan sebelumnya didekode

Selain itu, potongan yang dimunculkan oleh encoder siap untuk decoder sebagaimana adanya. Semua hal yang disebutkan di atas tentang pelaporan error dan sifat asinkron metode encoder juga sama berlaku untuk decoder.

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

Sekarang saatnya menunjukkan bagaimana frame yang baru didekode dapat ditampilkan di halaman. Sebaiknya pastikan bahwa callback output decoder (handleFrame()) ditampilkan dengan cepat. Dalam contoh di bawah ini, kode hanya menambahkan frame ke antrean frame yang siap untuk rendering. Rendering terjadi secara terpisah, dan terdiri dari dua langkah:

  1. Menunggu waktu yang tepat untuk menampilkan frame.
  2. Menggambar bingkai di kanvas.

Setelah frame tidak lagi diperlukan, panggil close() untuk melepaskan memori yang mendasari sebelum pembersih sampah memori mencapainya, hal ini akan mengurangi jumlah rata-rata memori yang digunakan oleh aplikasi web.

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

Tips Developer

Gunakan Media Panel di Chrome DevTools untuk melihat log media dan men-debug WebCodecs.

Screenshot Panel Media untuk men-debug WebCodecs
Panel Media di Chrome DevTools untuk men-debug WebCodecs.

Demo

Demo di bawah ini menunjukkan bagaimana frame animasi dari kanvas:

  • diambil pada 25 fps menjadi ReadableStream x MediaStreamTrackProcessor
  • ditransfer ke pekerja web
  • dienkode ke format video H.264
  • didekodekan lagi menjadi urutan frame video
  • dan dirender di kanvas kedua menggunakan transferControlToOffscreen()

Demo lainnya

Lihat juga demo kami yang lain:

Menggunakan WebCodecs API

Deteksi fitur

Untuk memeriksa dukungan WebCodecs:

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

Perlu diingat bahwa WebCodecs API hanya tersedia dalam konteks aman, sehingga deteksi akan gagal jika self.isSecureContext salah.

Masukan

Tim Chrome ingin mengetahui pengalaman Anda saat menggunakan WebCodecs API.

Beri tahu kami tentang desain API

Apakah ada sesuatu tentang API yang tidak berfungsi seperti yang Anda harapkan? Atau apakah ada metode atau properti yang hilang yang Anda butuhkan untuk menerapkan ide Anda? Ada pertanyaan atau komentar tentang model keamanan? Laporkan masalah spesifikasi di repo GitHub yang sesuai, atau tambahkan pendapat Anda ke masalah yang sudah ada.

Melaporkan masalah terkait penerapan

Apakah Anda menemukan bug pada implementasi Chrome? Atau apakah implementasinya berbeda dengan spesifikasi? Laporkan bug di new.crbug.com. Pastikan Anda menyertakan detail sebanyak mungkin, petunjuk sederhana untuk melakukan reproduksi, dan masukkan Blink>Media>WebCodecs di kotak Components. Glitch berfungsi dengan baik untuk berbagi repro yang cepat dan mudah.

Menunjukkan dukungan untuk API

Apakah Anda berencana menggunakan WebCodecs API? Dukungan publik Anda membantu tim Chrome untuk memprioritaskan fitur dan menunjukkan kepada vendor browser lain betapa pentingnya mendukung mereka.

Kirim email ke media-dev@chromium.org atau kirim tweet ke @ChromiumDev menggunakan hashtag #WebCodecs dan beri tahu kami tempat dan cara Anda menggunakannya.

Banner besar oleh Denise Jans di Unsplash.