WebAssembly memungkinkan kami memperluas {i>browser<i} dengan fitur-fitur baru. Artikel ini menunjukkan cara melakukan port decoder video AV1 dan memutar video AV1 di browser modern apa pun.
Salah satu hal terbaik tentang WebAssembly adalah kemampuan untuk bereksperimen dengan kemampuan baru dan menerapkan ide baru sebelum browser mengirimkan fitur tersebut secara native (jika ada). Anda dapat menggunakan WebAssembly dengan cara ini sebagai mekanisme polyfill berperforma tinggi, dengan menulis fitur dalam C/C++ atau Rust, bukan JavaScript.
Dengan banyaknya kode yang tersedia untuk porting, mungkin saja ada hal-hal yang tidak dapat dilakukan di browser sebelum WebAssembly hadir.
Artikel ini akan membahas contoh cara mengambil kode sumber codec video AV1 yang ada, mem-build wrapper untuknya, dan mencobanya di dalam browser Anda serta tips untuk membantu mem-build harness pengujian guna men-debug wrapper. Kode sumber lengkap untuk contoh di sini tersedia di github.com/GoogleChromeLabs/wasm-av1 sebagai referensi.
Download salah satu dari dua file video pengujian 24 fps ini dan coba di demo bawaan kami.
Memilih code-base yang menarik
Selama beberapa tahun terakhir, kami melihat bahwa sebagian besar traffic di web terdiri dari data video. Cisco memperkirakannya sebesar 80%. Tentu saja, vendor browser dan situs video sangat menyadari keinginan untuk mengurangi data yang digunakan oleh semua konten video ini. Kuncinya, tentu saja, adalah kompresi yang lebih baik, dan seperti yang Anda harapkan, ada banyak riset tentang kompresi video generasi berikutnya yang bertujuan untuk mengurangi beban data pengiriman video di internet.
Kebetulan, Alliance for Open Media telah mengerjakan skema kompresi video generasi berikutnya yang disebut AV1 yang menjanjikan untuk memperkecil ukuran data video secara signifikan. Di masa mendatang, kami berharap browser akan mengirimkan dukungan native untuk AV1, tetapi untungnya kode sumber untuk kompresor dan dekompresor adalah open source, yang menjadikannya kandidat ideal untuk mencoba mengompilasi ke dalam WebAssembly sehingga kita dapat bereksperimen dengannya di browser.
Beradaptasi untuk digunakan di browser
Salah satu hal pertama yang perlu kita lakukan untuk memasukkan kode ini ke browser adalah mengenali kode yang ada untuk memahami seperti apa API tersebut. Saat pertama kali melihat kode ini, ada dua hal yang menarik:
- Hierarki sumber dibuat menggunakan alat yang disebut
cmake
; dan - Ada sejumlah contoh yang semuanya mengasumsikan semacam antarmuka berbasis file.
Semua contoh yang dibuat secara default dapat dijalankan di command line, dan hal ini mungkin berlaku di banyak basis kode lain yang tersedia di komunitas. Jadi, antarmuka yang akan kita buat agar dapat berjalan di browser dapat berguna untuk banyak alat command line lainnya.
Menggunakan cmake
untuk membangun kode sumber
Untungnya, penulis AV1 telah bereksperimen dengan
Emscripten, SDK yang akan kita
gunakan untuk mem-build versi WebAssembly. Di root
repositori AV1, file
CMakeLists.txt
berisi aturan build berikut:
if(EMSCRIPTEN)
add_preproc_definition(_POSIX_SOURCE)
append_link_flag_to_target("inspect" "-s TOTAL_MEMORY=402653184")
append_link_flag_to_target("inspect" "-s MODULARIZE=1")
append_link_flag_to_target("inspect"
"-s EXPORT_NAME=\"\'DecoderModule\'\"")
append_link_flag_to_target("inspect" "--memory-init-file 0")
if("${CMAKE_BUILD_TYPE}" STREQUAL "")
# Default to -O3 when no build type is specified.
append_compiler_flag("-O3")
endif()
em_link_post_js(inspect "${AOM_ROOT}/tools/inspect-post.js")
endif()
Toolchain Emscripten dapat menghasilkan output dalam dua format, salah satunya disebut
asm.js
dan yang lainnya adalah WebAssembly.
Kita akan menargetkan WebAssembly karena menghasilkan output yang lebih kecil dan dapat berjalan
lebih cepat. Aturan build yang ada ini dimaksudkan untuk mengompilasi
library versi asm.js
untuk digunakan dalam
aplikasi pemeriksa yang dimanfaatkan untuk melihat konten file
video. Untuk penggunaan kita, kita memerlukan output WebAssembly sehingga kita menambahkan baris ini tepat
sebelum pernyataan endif()
penutup dalam
aturan di atas.
# Force generation of Wasm instead of asm.js
append_link_flag_to_target("inspect" "-s WASM=1")
append_compiler_flag("-s WASM=1")
Melakukan build dengan cmake
berarti terlebih dahulu membuat beberapa
Makefiles
dengan menjalankan
cmake
itu sendiri, diikuti dengan menjalankan perintah
make
yang akan melakukan langkah kompilasi.
Perhatikan bahwa karena kita menggunakan Emscripten, kita perlu menggunakan
toolchain compiler Emscripten, bukan compiler host default.
Hal ini dicapai dengan menggunakan Emscripten.cmake
yang
merupakan bagian dari Emscripten SDK dan
meneruskan jalurnya sebagai parameter ke cmake
itu sendiri.
Baris perintah di bawah ini digunakan untuk membuat Makefile:
cmake path/to/aom \
-DENABLE_CCACHE=1 -DAOM_TARGET_CPU=generic -DENABLE_DOCS=0 \
-DCONFIG_ACCOUNTING=1 -DCONFIG_INSPECTION=1 -DCONFIG_MULTITHREAD=0 \
-DCONFIG_RUNTIME_CPU_DETECT=0 -DCONFIG_UNIT_TESTS=0
-DCONFIG_WEBM_IO=0 \
-DCMAKE_TOOLCHAIN_FILE=path/to/emsdk-portable/.../Emscripten.cmake
Parameter path/to/aom
harus ditetapkan ke jalur lengkap
lokasi file sumber library AV1. Parameter
path/to/emsdk-portable/…/Emscripten.cmake
harus
ditetapkan ke jalur untuk file deskripsi toolchain Emscripten.cmake.
Untuk memudahkan, kita menggunakan skrip shell untuk menemukan file tersebut:
#!/bin/sh
EMCC_LOC=`which emcc`
EMSDK_LOC=`echo $EMCC_LOC | sed 's?/emscripten/[0-9.]*/emcc??'`
EMCMAKE_LOC=`find $EMSDK_LOC -name Emscripten.cmake -print`
echo $EMCMAKE_LOC
Jika melihat Makefile
level atas untuk project ini, Anda
dapat melihat cara skrip tersebut digunakan untuk mengonfigurasi build.
Setelah semua penyiapan selesai, kita cukup memanggil make
yang akan mem-build seluruh hierarki sumber, termasuk sampel, tetapi yang paling penting
membuat libaom.a
yang berisi
decoder video yang dikompilasi dan siap untuk kita gabungkan ke dalam project.
Mendesain API untuk berinteraksi dengan library
Setelah mem-build library, kita perlu mencari tahu cara berinteraksi dengan library tersebut untuk mengirim data video yang dikompresi ke library, lalu membaca kembali frame video yang dapat ditampilkan di browser.
Melihat di dalam hierarki kode AV1, titik awal yang baik adalah contoh
dekoder video yang dapat ditemukan dalam file
[simple_decoder.c](https://aomedia.googlesource.com/aom/+/master/examples/simple_decoder.c)
.
Decoder tersebut membaca file IVF
dan mendekodenya menjadi serangkaian gambar yang mewakili frame dalam video.
Kita menerapkan antarmuka dalam file sumber
[decode-av1.c](https://github.com/GoogleChromeLabs/wasm-av1/blob/master/decode-av1.c)
.
Karena browser tidak dapat membaca file dari sistem file, kita perlu mendesain beberapa bentuk antarmuka yang memungkinkan kita mengabstraksi I/O sehingga kita dapat mem-build sesuatu yang mirip dengan contoh dekoder untuk memasukkan data ke library AV1.
Pada command line, I/O file disebut sebagai antarmuka stream, sehingga kita dapat menentukan antarmuka sendiri yang terlihat seperti stream I/O dan membangun apa pun yang kita inginkan dalam implementasi yang mendasarinya.
Kita menentukan antarmuka sebagai ini:
DATA_Source *DS_open(const char *what);
size_t DS_read(DATA_Source *ds,
unsigned char *buf, size_t bytes);
int DS_empty(DATA_Source *ds);
void DS_close(DATA_Source *ds);
// Helper function for blob support
void DS_set_blob(DATA_Source *ds, void *buf, size_t len);
Fungsi open/read/empty/close
terlihat sangat mirip dengan operasi I/O file normal yang memungkinkan kita memetakan dengan mudah ke I/O file untuk aplikasi command line, atau menerapkannya dengan cara lain saat dijalankan di dalam browser. Jenis DATA_Source
buram dari
sisi JavaScript, dan hanya berfungsi untuk mengenkapsulasi antarmuka. Perlu diperhatikan bahwa
membangun API yang mengikuti semantik file dengan mudah akan mempermudah penggunaan kembali dalam
banyak code base lain yang dimaksudkan untuk digunakan dari command line
(mis. diff, sed, dll.).
Kita juga perlu menentukan fungsi bantuan yang disebut DS_set_blob
yang mengikat data biner mentah ke fungsi I/O streaming. Hal ini memungkinkan blob
'dibaca' seolah-olah merupakan streaming (yaitu terlihat seperti file yang dibaca secara berurutan).
Contoh implementasi kami memungkinkan pembacaan blob yang diteruskan seolah-olah
sumber data dibaca secara berurutan. Kode referensi dapat ditemukan dalam file
blob-api.c
,
dan seluruh implementasinya adalah ini:
struct DATA_Source {
void *ds_Buf;
size_t ds_Len;
size_t ds_Pos;
};
DATA_Source *
DS_open(const char *what) {
DATA_Source *ds;
ds = malloc(sizeof *ds);
if (ds != NULL) {
memset(ds, 0, sizeof *ds);
}
return ds;
}
size_t
DS_read(DATA_Source *ds, unsigned char *buf, size_t bytes) {
if (DS_empty(ds) || buf == NULL) {
return 0;
}
if (bytes > (ds->ds_Len - ds->ds_Pos)) {
bytes = ds->ds_Len - ds->ds_Pos;
}
memcpy(buf, &ds->ds_Buf[ds->ds_Pos], bytes);
ds->ds_Pos += bytes;
return bytes;
}
int
DS_empty(DATA_Source *ds) {
return ds->ds_Pos >= ds->ds_Len;
}
void
DS_close(DATA_Source *ds) {
free(ds);
}
void
DS_set_blob(DATA_Source *ds, void *buf, size_t len) {
ds->ds_Buf = buf;
ds->ds_Len = len;
ds->ds_Pos = 0;
}
Mem-build harness pengujian untuk menguji di luar browser
Salah satu praktik terbaik dalam rekayasa software adalah membuat pengujian unit untuk kode bersama dengan pengujian integrasi.
Saat mem-build dengan WebAssembly di browser, sebaiknya buat beberapa bentuk pengujian unit untuk antarmuka ke kode yang sedang kita kerjakan sehingga kita dapat melakukan debug di luar browser dan juga dapat menguji antarmuka yang telah kita build.
Dalam contoh ini, kita telah mengemulasi API berbasis streaming sebagai antarmuka untuk
library AV1. Jadi, secara logis, sebaiknya buat harness pengujian yang
dapat kita gunakan untuk mem-build versi API yang berjalan di command line dan melakukan
I/O file yang sebenarnya di balik layar dengan menerapkan I/O file itu sendiri di bawah
DATA_Source
API.
Kode I/O streaming untuk harness pengujian kami sederhana, dan terlihat seperti ini:
DATA_Source *
DS_open(const char *what) {
return (DATA_Source *)fopen(what, "rb");
}
size_t
DS_read(DATA_Source *ds, unsigned char *buf, size_t bytes) {
return fread(buf, 1, bytes, (FILE *)ds);
}
int
DS_empty(DATA_Source *ds) {
return feof((FILE *)ds);
}
void
DS_close(DATA_Source *ds) {
fclose((FILE *)ds);
}
Dengan memisahkan antarmuka streaming, kita dapat membangun modul WebAssembly untuk menggunakan blob data biner saat berada di browser, dan berinteraksi dengan file sungguhan saat membuat kode yang akan diuji dari command line. Kode harness pengujian kami dapat
ditemukan dalam contoh file sumber
test.c
.
Mengimplementasikan mekanisme buffering untuk beberapa frame video
Saat memutar video, praktik umum adalah melakukan buffering pada beberapa frame untuk membantu pemutaran yang lebih lancar. Untuk tujuan kita, kita hanya akan menerapkan buffering 10 frame video, sehingga kita akan melakukan buffering 10 frame sebelum memulai pemutaran. Kemudian, setiap kali frame ditampilkan, kita akan mencoba mendekode frame lain agar buffer tetap penuh. Pendekatan ini memastikan frame tersedia terlebih dahulu untuk membantu menghentikan video yang tersendat.
Dengan contoh sederhana kami, seluruh video yang dikompresi dapat dibaca, sehingga buffering tidak terlalu diperlukan. Namun, jika kita ingin memperluas antarmuka data sumber untuk mendukung input streaming dari server, kita harus memiliki mekanisme buffering.
Kode dalam
decode-av1.c
untuk membaca frame data video dari library AV1 dan menyimpannya di buffer
seperti ini:
void
AVX_Decoder_run(AVX_Decoder *ad) {
...
// Try to decode an image from the compressed stream, and buffer
while (ad->ad_NumBuffered < NUM_FRAMES_BUFFERED) {
ad->ad_Image = aom_codec_get_frame(&ad->ad_Codec,
&ad->ad_Iterator);
if (ad->ad_Image == NULL) {
break;
}
else {
buffer_frame(ad);
}
}
Kami telah memilih untuk membuat buffering berisi 10 frame video, yang merupakan pilihan arbitrer. Buffering lebih banyak frame berarti lebih banyak waktu tunggu untuk video untuk memulai pemutaran, sedangkan buffering terlalu sedikit frame dapat menyebabkan berhentinya pemutaran. Dalam implementasi browser native, buffering frame jauh lebih kompleks daripada implementasi ini.
Mendapatkan frame video ke halaman dengan WebGL
Frame video yang telah di-buffer harus ditampilkan di halaman. Karena ini adalah konten video dinamis, kita ingin dapat melakukannya secepat mungkin. Untuk itu, kita beralih ke WebGL.
WebGL memungkinkan kita mengambil gambar, seperti frame video, dan menggunakannya sebagai tekstur yang dicat ke beberapa geometri. Di dunia WebGL, semuanya terdiri dari segitiga. Jadi, untuk kasus ini, kita dapat menggunakan fitur bawaan WebGL yang praktis, yang disebut gl.TRIANGLE_FAN.
Namun, ada masalah kecil. Tekstur WebGL seharusnya berupa gambar RGB, satu byte per saluran warna. Output dari decoder AV1 kami adalah gambar dalam format yang disebut YUV, dengan output default memiliki 16 bit per saluran, dan juga setiap nilai U atau V sesuai dengan 4 piksel dalam gambar output yang sebenarnya. Artinya, kita perlu mengonversi warna gambar sebelum dapat meneruskannya ke WebGL untuk ditampilkan.
Untuk melakukannya, kami mengimplementasikan fungsi AVX_YUV_to_RGB()
yang dapat Anda temukan di file sumber yuv-to-rgb.c
.
Fungsi tersebut mengonversi output dari decoder AV1 menjadi sesuatu yang dapat kita
teruskan ke WebGL. Perhatikan bahwa saat memanggil fungsi ini dari JavaScript, kita perlu
memastikan bahwa memori tempat kita menulis gambar yang dikonversi telah
dialokasikan di dalam memori modul WebAssembly. Jika tidak, memori tersebut tidak dapat
mendapatkan akses ke memori tersebut. Fungsi untuk mengeluarkan gambar dari modul WebAssembly dan
melukisnya ke layar adalah sebagai berikut:
function show_frame(af) {
if (rgb_image != 0) {
// Convert The 16-bit YUV to 8-bit RGB
let buf = Module._AVX_Video_Frame_get_buffer(af);
Module._AVX_YUV_to_RGB(rgb_image, buf, WIDTH, HEIGHT);
// Paint the image onto the canvas
drawImageToCanvas(new Uint8Array(Module.HEAPU8.buffer,
rgb_image, 3 * WIDTH * HEIGHT), WIDTH, HEIGHT);
}
}
Fungsi drawImageToCanvas()
yang mengimplementasikan gambar WebGL dapat
ditemukan di file sumber
draw-image.js
untuk referensi.
Pekerjaan mendatang dan poin penting
Mencoba demo pada dua file video pengujian (direkam sebagai video 24 fps) memberi kita beberapa pelajaran:
- Anda dapat membuat code-base yang kompleks untuk berjalan dengan performa tinggi di browser menggunakan WebAssembly; dan
- Proses yang memerlukan banyak CPU seperti decoding video lanjutan dapat dilakukan melalui WebAssembly.
Namun, ada beberapa batasan: penerapan semuanya berjalan di thread utama dan kita menyelang-seling proses menggambar dan mendekode video di satu thread tersebut. Dengan memindahkan decoding ke pekerja web, pemutaran akan menjadi lebih lancar, karena waktu untuk mendekode frame sangat bergantung pada konten frame tersebut dan terkadang dapat memerlukan waktu lebih lama dari yang kita anggarkan.
Kompilasi ke dalam WebAssembly menggunakan konfigurasi AV1 untuk jenis CPU generik. Jika kita mengompilasi secara native di command line untuk CPU generik, kita akan melihat beban CPU yang serupa untuk mendekode video seperti pada versi WebAssembly, tetapi library decoder AV1 juga menyertakan implementasi SIMD yang berjalan hingga 5 kali lebih cepat. WebAssembly Community Group saat ini sedang berupaya memperluas standar untuk menyertakan primitive SIMD, dan jika sudah tersedia, hal ini akan sangat mempercepat proses decoding. Jika hal itu terjadi, Anda dapat mendekode video HD 4K secara real-time dari decoder video WebAssembly.
Apa pun kasusnya, kode contoh berguna sebagai panduan untuk membantu mem-port utilitas command line yang ada untuk dijalankan sebagai modul WebAssembly dan menunjukkan hal yang mungkin dilakukan di web saat ini.
Kredit
Terima kasih kepada Jeff Posnick, Eric Bidelman, dan Thomas Steiner atas ulasan dan masukan yang berharga.