Memanipulasi komponen streaming video.
Teknologi web modern memberikan banyak cara untuk bekerja dengan video. Media Stream API, Media Recording API, Media Source API, dan WebRTC API akan bertambah ke set alat yang lengkap untuk merekam, mentransfer, dan memutar streaming video. Saat memecahkan tugas tingkat tinggi tertentu, API ini tidak mengizinkan programmer bekerja dengan komponen individu streaming video seperti {i>frame<i} dan potongan video atau audio yang tidak dikodekan. Untuk mendapatkan akses tingkat rendah ke komponen dasar ini, developer menggunakan WebAssembly untuk menghadirkan codec video dan audio ke browser. Namun, mengingat bahwa {i>browser<i} modern telah diluncurkan dengan berbagai codec (yang sering dipercepat oleh perangkat keras), mengemasnya ulang sebagai WebAssembly sepertinya membuang-buang sumber daya manusia dan komputer.
WebCodecs API menghilangkan inefisiensi ini dengan memberi {i>programmer<i} suatu 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, video streaming, dll.
Alur kerja pemrosesan video
Bingkai adalah pusat pemrosesan video. Sehingga dalam WebCodecs sebagian besar kelas memakai atau menghasilkan frame. Encoder video mengonversi frame menjadi potongan (chunk). Decoder video melakukan hal sebaliknya.
Selain itu, VideoFrame
berfungsi baik dengan Web API lainnya dengan menjadi CanvasImageSource
dan memiliki konstruktor yang menerima CanvasImageSource
.
Sehingga dapat digunakan dalam fungsi seperti drawImage()
dan texImage2D()
. Selain itu, gambar dapat dibuat dari kanvas, bitmap, elemen video, dan bingkai video lainnya.
WebCodecs API berfungsi baik bersama class dari Insertable Streams API yang menghubungkan WebCodecs ke trek streaming media.
MediaStreamTrackProcessor
memecah trek media menjadi beberapa frame.MediaStreamTrackGenerator
membuat trek media dari aliran frame.
WebCodecs dan pekerja web
Berdasarkan desainnya, WebCodecs API melakukan semua tugas sulit secara asinkron dan di luar thread utama. Tapi karena callback {i>frame<i} dan potongan sering kali bisa dipanggil beberapa kali dalam satu detik, dapat mengacaukan utas utama dan membuat situs web kurang responsif. Oleh karena itu, lebih baik memindahkan penanganan {i>frame<i} individu dan potongan yang dikodekan ke dalam pekerja web.
Untuk membantu Anda, ReadableStream
menyediakan cara mudah untuk secara otomatis mentransfer semua {i>frame<i} yang berasal dari suatu media
ke pekerja. Misalnya, MediaStreamTrackProcessor
dapat digunakan untuk mendapatkan
ReadableStream
untuk trek streaming media yang berasal dari kamera web. Setelah itu
aliran data ditransfer ke pekerja web tempat frame dibaca satu per satu dan diantrekan
menjadi VideoEncoder
.
Dengan HTMLCanvasElement.transferControlToOffscreen
, rendering dapat dilakukan di luar thread utama. Tetapi jika semua alat tingkat
tinggi berubah menjadi
merepotkan, VideoFrame
sendiri dapat dipindahtangankan dan mungkin
berpindah-pindah pekerja.
Cara kerja WebCodecs
Encoding
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 menarik 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);
Tidak peduli dari mana asalnya, {i>frame<i} dapat dienkode ke dalam
Objek EncodedVideoChunk
dengan VideoEncoder
.
Sebelum encoding, VideoEncoder
harus diberikan dua objek JavaScript:
- Init kamus dengan dua fungsi untuk
menangani potongan yang dienkode dan
yang sama. Fungsi ini ditentukan oleh developer dan tidak dapat diubah setelah
penerusan tersebut diteruskan ke konstruktor
VideoEncoder
. - Objek konfigurasi encoder, yang berisi parameter untuk output
streaming video. 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
konfigurasi didukung dan menunggu promise-nya.
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()
langsung ditampilkan tanpa menunggu
pekerjaan yang sebenarnya. Ini memungkinkan beberapa bingkai mengantre untuk pengkodean pada
pada waktu yang sama, sedangkan encodeQueueSize
menunjukkan jumlah permintaan yang menunggu dalam antrean
agar enkode sebelumnya selesai.
Error dilaporkan dengan segera menampilkan pengecualian, jika argumen
atau urutan panggilan metode melanggar kontrak API, atau dengan memanggil error()
untuk masalah yang dihadapi dalam implementasi codec.
Jika encoding berhasil diselesaikan output()
dipanggil dengan potongan baru yang dienkode sebagai argumen.
Detail penting lainnya di sini adalah {i>frame<i}
perlu diberi tahu jika {i>frame<i} tidak
lagi yang 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();
}
}
Akhirnya saatnya untuk menyelesaikan kode pengkodean dengan menulis fungsi yang menangani potongan video yang dienkode saat keluar dari encoder. Biasanya fungsi ini akan mengirimkan potongan data melalui jaringan atau menggabungkan data tersebut ke dalam media container 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 suatu saat Anda perlu memastikan bahwa
semua permintaan encoding yang tertunda telah
selesai, Anda dapat memanggil flush()
dan menunggu promise-nya.
await encoder.flush();
Dekode
Menyiapkan VideoDecoder
mirip dengan yang telah dilakukan untuk
VideoEncoder
: dua fungsi diteruskan saat decoder dibuat, dan codec
parameter 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 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 memberinya feed dengan objek EncodedVideoChunk
.
Untuk membuat potongan, Anda perlu:
BufferSource
data video yang dienkode- stempel waktu mulai potongan dalam mikrodetik (waktu media dari frame pertama yang dienkode dalam potongan)
- jenis potongan, salah satu dari:
key
jika potongan tersebut dapat didekode secara terpisah dari potongan sebelumnyadelta
jika potongan hanya dapat didekode setelah satu atau beberapa bagian sebelumnya didekode
Selain itu, setiap potongan yang dikeluarkan oleh encoder siap untuk decoder sebagaimana adanya. Semua hal 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 bagaimana frame yang baru didekode dapat ditampilkan di halaman. Penting
sebaiknya pastikan bahwa callback output decoder (handleFrame()
)
kembali dengan cepat. Dalam contoh di bawah ini, ia hanya menambahkan {i>frame<i} ke antrean
{i>frame<i} Anda siap
untuk rendering.
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 mencapainya, 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
Menggunakan Panel Media di Chrome DevTools untuk melihat log media dan men-debug WebCodecs.
Demo
Demo di bawah ini menunjukkan cara kerja frame animasi dari kanvas:
- yang direkam pada 25 fps menjadi
ReadableStream
olehMediaStreamTrackProcessor
- ditransfer ke pekerja web
- dienkode ke dalam format video H.264
- didekode lagi menjadi urutan frame video
- dan dirender pada 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
bernilai salah (false).
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 ada metode atau properti yang hilang yang diperlukan untuk menerapkan ide Anda? Memiliki pertanyaan atau komentar tentang model keamanan? Ajukan masalah spesifikasi di repo GitHub yang sesuai, atau tambahkan pendapat Anda terhadap masalah yang ada.
Laporkan masalah terkait penerapan
Apakah Anda menemukan bug pada implementasi Chrome? Ataukah implementasi
berbeda dengan spesifikasi? Laporkan bug di new.crbug.com.
Pastikan untuk menyertakan detail sebanyak mungkin, instruksi sederhana untuk
mereproduksi, dan masukkan
Blink>Media>WebCodecs
di kotak Components.
Glitch sangat cocok untuk membagikan repro dengan cepat dan mudah.
Menunjukkan dukungan untuk API
Anda berencana menggunakan WebCodecs API? Dukungan publik Anda membantu Tim Chrome memprioritaskan fitur dan menunjukkan kepada vendor browser lainnya betapa pentingnya adalah untuk mendukung mereka.
Kirim email ke media-dev@chromium.org atau kirim tweet
ke @ChromiumDev menggunakan hashtag
#WebCodecs
dan beri tahu kami tempat serta
cara Anda menggunakannya.
Banner besar oleh Jan Denise di Unsplash.