Memanipulasi komponen streaming video.
Teknologi web modern menyediakan banyak cara untuk menangani video. Media Stream API, Media Recording API, Media Source API, dan WebRTC API digabungkan menjadi serangkaian alat yang lengkap untuk merekam, mentransfer, dan memutar streaming video. Saat menyelesaikan tugas tingkat tinggi tertentu, API ini tidak mengizinkan programmer web menangani setiap komponen streaming video seperti frame dan potongan video atau audio yang dienkode yang tidak dimux. Untuk mendapatkan akses tingkat rendah ke komponen dasar ini, developer telah menggunakan WebAssembly untuk memasukkan codec video dan audio ke dalam browser. Namun, mengingat browser modern sudah dilengkapi dengan berbagai codec (yang sering dipercepat oleh hardware), memaketkan ulang codec sebagai WebAssembly tampaknya merupakan pemborosan resource manusia dan komputer.
WebCodecs API menghilangkan inefisiensi ini dengan memberi programmer cara untuk menggunakan komponen media yang sudah ada di browser. Khususnya:
- Dekoder video dan audio
- Encoder video dan audio
- Frame video mentah
- Decoder gambar
WebCodecs API berguna untuk aplikasi web yang memerlukan kontrol penuh atas cara konten media diproses, seperti editor video, konferensi video, streaming video, dll.
Alur kerja pemrosesan video
Frame adalah inti dalam pemrosesan video. Jadi, di WebCodecs, sebagian besar class menggunakan atau menghasilkan frame. Encoder video mengonversi frame menjadi potongan yang dienkode. Dekoder video melakukan hal sebaliknya.
Selain itu, VideoFrame
berfungsi dengan baik dengan Web API lainnya dengan menjadi CanvasImageSource
dan memiliki konstruktor yang menerima CanvasImageSource
.
Jadi, fungsi ini dapat digunakan dalam fungsi seperti drawImage()
dantexImage2D()
. Selain itu, video dapat dibuat dari kanvas, bitmap, elemen video, dan frame video lainnya.
WebCodecs API berfungsi dengan baik bersama dengan class dari Insertable Streams API yang menghubungkan WebCodecs ke jalur streaming media.
MediaStreamTrackProcessor
membagi trek media menjadi beberapa frame.MediaStreamTrackGenerator
membuat trek media dari aliran frame.
WebCodecs dan pekerja web
Secara desain, WebCodecs API melakukan semua tugas berat secara asinkron dan di luar thread utama. Namun, karena callback frame dan chunk sering kali dapat dipanggil beberapa kali per detik, callback tersebut dapat mengacaukan thread utama sehingga membuat situs menjadi kurang responsif. Oleh karena itu, sebaiknya pindahkan penanganan setiap frame dan potongan yang dienkode ke dalam pekerja web.
Untuk membantu hal tersebut, ReadableStream
menyediakan cara yang mudah untuk mentransfer semua frame yang berasal dari trek
media secara otomatis ke pekerja. Misalnya, MediaStreamTrackProcessor
dapat digunakan untuk mendapatkan
ReadableStream
untuk jalur streaming media yang berasal dari kamera web. Setelah itu,
streaming ditransfer ke pekerja web tempat frame dibaca satu per satu dan dimasukkan ke dalam antrean
VideoEncoder
.
Dengan HTMLCanvasElement.transferControlToOffscreen
, rendering bahkan dapat dilakukan di luar thread utama. Namun, jika semua alat tingkat tinggi ternyata
tidak praktis, VideoFrame
itu sendiri dapat ditransfer dan dapat
dipindahkan antar-pekerja.
Cara kerja WebCodecs
Encoding
Semuanya dimulai dengan VideoFrame
.
Ada tiga cara untuk membuat frame 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 dariMediaStreamTrack
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; }
Membuat 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);
Apa pun asalnya, frame dapat dienkode ke dalam
objek EncodedVideoChunk
dengan VideoEncoder
.
Sebelum encoding, VideoEncoder
harus diberi dua objek JavaScript:
- Menginisialisasi kamus dengan dua fungsi untuk menangani error dan
potongan 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 tunggu janjinya.
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()
segera ditampilkan tanpa menunggu
tugas sebenarnya selesai. Hal ini memungkinkan beberapa frame mengantre untuk dienkode secara
bersamaan, sedangkan encodeQueueSize
menunjukkan 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 ditemukan dalam implementasi codec.
Jika encoding berhasil diselesaikan, callback output()
akan dipanggil dengan potongan yang dienkode baru sebagai argumen.
Detail penting lainnya di sini adalah 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 kode encoding 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 penyimpanan.
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 pada suatu saat Anda perlu memastikan bahwa semua permintaan encoding yang tertunda telah selesai, Anda dapat memanggil flush()
dan menunggu janjinya.
await encoder.flush();
Dekode
Menyiapkan VideoDecoder
mirip dengan yang telah dilakukan untuk
VideoEncoder
: dua fungsi diteruskan saat dekoder dibuat, dan parameter
codec diberikan ke configure()
.
Kumpulan parameter codec bervariasi dari codec ke codec. Misalnya, codec H.264
mungkin memerlukan blob biner
AVCC, kecuali jika dienkode dalam format yang disebut 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.
}
Setelah dekoder diinisialisasi, Anda dapat mulai memberinya objek EncodedVideoChunk
.
Untuk membuat bagian, Anda memerlukan:
BufferSource
data video yang dienkode- stempel waktu awal bagian dalam mikrodetik (waktu media frame pertama yang dienkode dalam bagian)
- jenis bagian, salah satu dari:
key
jika potongan dapat didekode secara independen dari potongan sebelumnyadelta
jika potongan hanya dapat didekode setelah satu atau beberapa potongan sebelumnya didekode
Selain itu, setiap bagian yang dikeluarkan oleh encoder siap untuk decoder apa adanya. Semua hal yang disebutkan di atas tentang pelaporan error dan sifat asinkron metode encoder juga 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 cara frame yang baru didekode dapat ditampilkan di halaman. Sebaiknya
pastikan callback output decoder (handleFrame()
)
ditampilkan dengan cepat. Pada contoh di bawah, kode ini hanya menambahkan frame ke antrean
frame yang siap dirender.
Rendering terjadi secara terpisah, dan terdiri dari dua langkah:
- Menunggu waktu yang tepat untuk menampilkan frame.
- Menggambar bingkai di kanvas.
Setelah frame tidak lagi diperlukan, panggil close()
untuk melepaskan memori yang mendasarinya
sebelum pembersih sampah memori mendapatkannya, 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 Panel Media di Chrome DevTools untuk melihat log media dan men-debug WebCodecs.
Demo
Demo di bawah menunjukkan bagaimana frame animasi dari kanvas:
- direkam pada 25 fps ke dalam
ReadableStream
olehMediaStreamTrackProcessor
- ditransfer ke pekerja web
- dienkode ke dalam format video H.264
- didekode 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.
}
Perhatikan bahwa WebCodecs API hanya tersedia dalam konteks aman,
sehingga deteksi akan gagal jika self.isSecureContext
bernilai salah.
Masukan
Tim Chrome ingin mengetahui pengalaman Anda saat menggunakan WebCodecs API.
Ceritakan kepada kami tentang desain API
Apakah ada sesuatu tentang API yang tidak berfungsi seperti yang Anda harapkan? Atau, adakah metode atau properti yang hilang yang Anda perlukan untuk menerapkan ide Anda? Punya pertanyaan atau komentar tentang model keamanan? Ajukan masalah spesifikasi di repo GitHub yang sesuai, atau tambahkan pendapat Anda ke masalah yang ada.
Melaporkan masalah terkait penerapan
Apakah Anda menemukan bug pada penerapan Chrome? Atau apakah penerapannya
berbeda dengan spesifikasinya? Ajukan bug di new.crbug.com.
Pastikan untuk menyertakan detail sebanyak mungkin, petunjuk sederhana untuk
mereproduksi, dan masukkan Blink>Media>WebCodecs
di kotak Components.
Glitch sangat cocok untuk membagikan rekaman ulang yang cepat dan mudah.
Menampilkan dukungan untuk API
Apakah Anda berencana menggunakan WebCodecs API? Dukungan publik Anda membantu tim Chrome memprioritaskan fitur dan menunjukkan kepada vendor browser lain betapa penting dukungan untuk fitur tersebut.
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.