Memperluas browser dengan WebAssembly

WebAssembly memungkinkan kita memperluas browser dengan fitur baru. Artikel ini menunjukkan cara mentransfer dekoder video AV1 dan memutar video AV1 di browser modern apa pun.

Alex Danilo

Salah satu hal terbaik tentang WebAssembly adalah bereksperimen kemampuan dengan kemampuan baru dan menerapkan ide baru sebelum browser mengirimkan fitur tersebut secara native (jika memang ada). Anda dapat menganggap WebAssembly dengan cara ini sebagai mekanisme polyfill berperforma tinggi, tempat Anda menulis fitur di C/C++ atau Rust, bukan JavaScript.

Dengan banyaknya kode yang tersedia untuk transfer, Anda dapat melakukan hal-hal di browser yang tidak berfungsi hingga WebAssembly diluncurkan.

Artikel ini akan memberikan contoh cara mengambil kode sumber codec video AV1 yang ada, membuat wrapper untuk kode tersebut, dan mencobanya di dalam browser serta tips untuk membantu membuat harness pengujian untuk 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 basis kode yang menarik

Selama beberapa tahun, kami mendapati bahwa sebagian besar traffic di web terdiri dari data video, Cisco memperkirakannya hingga 80%. Tentu saja, vendor browser dan situs video sangat menyadari keinginan untuk mengurangi data yang dipakai 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.

Saat ini, Alliance for Open Media telah mengerjakan skema kompresi video generasi berikutnya yang disebut AV1 yang berpotensi mengecilkan ukuran data video secara signifikan. Di masa mendatang, kami memperkirakan browser akan mengirimkan dukungan native untuk AV1, tetapi untungnya, kode sumber untuk kompresor dan dekompresi adalah open source, yang menjadinya kandidat ideal untuk mencoba mengompilasinya ke dalam WebAssembly sehingga kami dapat bereksperimen dengannya di browser.

Gambar film Kelinci.

Beradaptasi untuk digunakan di browser

Salah satu hal pertama yang perlu kita lakukan untuk memasukkan kode ini ke browser adalah mempelajari kode yang ada untuk memahami bentuk API tersebut. Saat pertama melihat kode ini, ada dua hal yang terlihat jelas:

  1. Hierarki sumber dibuat menggunakan alat yang disebut cmake; dan
  2. Ada sejumlah contoh yang semuanya mengasumsikan jenis antarmuka berbasis file.

Semua contoh yang dibuat secara default dapat dijalankan pada command line, dan ini mungkin juga berlaku di banyak code base lain yang tersedia dalam komunitas. Jadi, antarmuka yang akan kita bangun untuk menjalankannya di browser bisa berguna untuk banyak alat baris perintah lainnya.

Menggunakan cmake untuk mem-build 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.txtberisi 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 format 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 inspector yang dimanfaatkan untuk melihat konten file video. Untuk penggunaan, kita memerlukan output WebAssembly sehingga 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")

Mem-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 menggunakan Emscripten, kita perlu menggunakan toolchain compiler Empscripten, bukan compiler host default. Hal itu 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 adalah yang kita gunakan 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 disetel ke jalur untuk file deskripsi toolchain Emscripten.cmake.

Untuk memudahkan, kami 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 tingkat atas untuk project ini, Anda dapat melihat cara skrip 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: menghasilkan libaom.a yang berisi dekoder video yang dikompilasi dan siap untuk kita sertakan ke dalam project.

Mendesain API untuk berinteraksi dengan library

Setelah membuat library, kita perlu mencari tahu cara berinteraksi dengan library tersebut untuk mengirim data video terkompresi ke library, lalu membaca kembali frame video yang dapat ditampilkan di browser.

Amati hierarki kode AV1, titik awal yang baik adalah contoh dekoder video yang dapat ditemukan di 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 mengimplementasikan 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 merancang beberapa bentuk antarmuka yang memungkinkan kita memisahkan I/O secara abstrak sehingga dapat membangun sesuatu yang mirip dengan dekoder contoh untuk memasukkan data ke dalam library AV1.

Di command line, I/O file disebut antarmuka streaming. Jadi, kita cukup menentukan antarmuka sendiri yang terlihat seperti I/O streaming dan mem-build sesuai keinginan dalam implementasi yang mendasarinya.

Kita mendefinisikan antarmuka sebagai berikut:

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 sangat mirip dengan operasi I/O file biasa yang memungkinkan kita memetakannya dengan mudah ke I/O file untuk aplikasi command line, atau mengimplementasikannya dengan cara lain saat dijalankan di dalam browser. Jenis DATA_Source buram dari sisi JavaScript, dan hanya berfungsi untuk mengenkapsulasi antarmuka. Perhatikan bahwa membangun API yang mengikuti semantik file akan memudahkan penggunaan kembali di banyak code base lain yang dimaksudkan untuk digunakan dari command line (misalnya, diff, sed, dll.).

Kita juga perlu menentukan fungsi bantuan yang disebut DS_set_blob yang mengikat data biner mentah ke fungsi I/O aliran kita. Hal ini memungkinkan blob 'dibaca' seolah-olah itu adalah stream (yaitu terlihat seperti file yang dibaca secara berurutan).

Contoh implementasi kami memungkinkan pembacaan blob yang diteruskan seolah-olah sumber data tersebut dibaca secara berurutan. Kode referensi dapat ditemukan dalam file blob-api.c, dan seluruh implementasinya adalah sebagai berikut:

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

Membuat harness pengujian untuk diuji di luar browser

Salah satu praktik terbaik dalam software engineering adalah mem-build pengujian unit untuk kode bersama dengan pengujian integrasi.

Saat membangun dengan WebAssembly di browser, sebaiknya buat beberapa bentuk pengujian unit untuk antarmuka ke kode yang sedang kita gunakan agar dapat men-debug di luar browser dan juga dapat menguji antarmuka yang telah kita buat.

Dalam contoh ini, kami telah mengemulasi API berbasis stream sebagai antarmuka ke library AV1. Jadi, secara logis, masuk akal untuk membuat harness pengujian yang dapat kita gunakan untuk mem-build versi API yang berjalan di command line dan melakukan file I/O sebenarnya di balik layar dengan menerapkan file I/O itu sendiri di bawah API DATA_Source kami.

Kode I/O streaming untuk harness pengujian sangat mudah, 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 mengabstraksi antarmuka streaming, kita dapat membuat modul WebAssembly agar menggunakan blob data biner saat berada di browser, dan antarmuka ke file sebenarnya saat membuat kode yang akan diuji dari command line. Kode harness pengujian kita dapat ditemukan dalam contoh file sumber test.c.

Mengimplementasikan mekanisme buffering untuk beberapa frame video

Saat memutar video, biasanya melakukan buffering beberapa frame untuk membantu pemutaran yang lebih lancar. Untuk tujuan ini, kita hanya akan mengimplementasikan buffer 10 frame video, jadi 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 di awal untuk membantu menghentikan video yang tersendat.

Dengan contoh sederhana kami, seluruh video terkompresi dapat dibaca, sehingga buffering tidak terlalu diperlukan. Namun, jika ingin memperluas antarmuka data sumber untuk mendukung input streaming dari server, kita harus menerapkan mekanisme buffering.

Kode dalam decode-av1.c untuk membaca frame data video dari library AV1 dan menyimpan di buffer sebagai berikut:

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


Kita telah memilih untuk membuat buffer berisi 10 frame video, yang hanya merupakan pilihan arbitrer. Buffering lebih banyak frame berarti waktu tunggu yang lebih lama bagi video untuk memulai pemutaran, sedangkan buffering yang terlalu sedikit dapat menyebabkan macet selama pemutaran. Dalam implementasi browser native, buffering frame jauh lebih kompleks daripada implementasi ini.

Memindahkan frame video ke halaman dengan WebGL

Frame video yang telah di-buffer perlu ditampilkan di halaman kita. Karena ini adalah konten video dinamis, kami ingin dapat melakukannya secepat mungkin. Untuk itu, kita beralih ke WebGL.

WebGL memungkinkan kita mengambil gambar, seperti frame video, dan menggunakannya sebagai tekstur yang digambar pada beberapa geometri. Di dunia WebGL, semuanya terdiri dari segitiga. Jadi, untuk kasus ini, kita dapat menggunakan fitur bawaan WebGL yang mudah digunakan, yang disebut gl.TRIANGLE_FAN.

Namun, ada masalah kecil. Tekstur WebGL seharusnya berupa gambar RGB, satu byte per saluran warna. Output dari decoder AV1 adalah gambar yang disebut format YUV, dengan output default-nya memiliki 16 bit per saluran, dan juga setiap nilai U atau V terkait dengan 4 piksel dalam gambar output sebenarnya. Ini semua berarti kita perlu mengonversi warna pada gambar sebelum dapat meneruskannya ke WebGL untuk ditampilkan.

Untuk melakukannya, kami mengimplementasikan fungsi AVX_YUV_to_RGB() yang dapat Anda temukan dalam file sumber yuv-to-rgb.c. Fungsi tersebut mengonversi output dari decoder AV1 menjadi sesuatu yang dapat diteruskan ke WebGL. Perhatikan bahwa saat memanggil fungsi ini dari JavaScript, kita perlu memastikan bahwa memori yang digunakan untuk menulis gambar yang dikonversi telah dialokasikan di dalam memori modul WebAssembly. Jika tidak, fungsi tersebut tidak akan bisa mendapatkan akses ke sana. Fungsi untuk mengeluarkan gambar dari modul WebAssembly dan menampilkannya 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 penggambaran WebGL dapat ditemukan di file sumber draw-image.js untuk referensi.

Pekerjaan di masa mendatang dan hal penting yang bisa dipelajari

Mencoba demo kami pada dua file video pengujian (direkam sebagai video 24 f.p.s.) mengajarkan beberapa hal:

  1. Sangat memungkinkan untuk membangun code-base yang kompleks untuk dijalankan secara optimal di browser menggunakan WebAssembly; dan
  2. Penggunaan CPU intensif seperti decoding video tingkat lanjut dapat dilakukan melalui WebAssembly.

Namun ada beberapa batasan: semua implementasinya berjalan di thread utama dan kita menyisipkan proses menggambar dan decoding video di satu thread tersebut. Pemindahan decoding ke pekerja web dapat memberi kita pemutaran yang lebih lancar, karena waktu untuk mendekode frame sangat bergantung pada konten frame tersebut dan terkadang dapat memerlukan waktu lebih banyak daripada yang telah dianggarkan.

Kompilasi ke WebAssembly menggunakan konfigurasi AV1 untuk jenis CPU umum. Jika kami mengompilasi secara native pada command line untuk CPU generik, kita akan melihat beban CPU yang serupa untuk mendekode video seperti pada versi WebAssembly, tetapi library dekoder AV1 juga menyertakan implementasi SIMD yang berjalan hingga 5 kali lebih cepat. Grup Komunitas WebAssembly saat ini sedang berupaya memperluas standar untuk menyertakan primitif SIMD, dan jika hal itu terjadi, standar ini menjanjikan untuk mempercepat decoding secara signifikan. Jika hal itu terjadi, video 4K HD dapat didekode secara real-time dari dekoder video WebAssembly.

Bagaimanapun, kode contoh ini berguna sebagai panduan untuk membantu mem-port utilitas command line yang ada untuk dijalankan sebagai modul WebAssembly dan menunjukkan hal yang dapat dilakukan di web saat ini.

Kredit

Terima kasih kepada Jeff Posnick, Eric Bidelman, dan Thomas Steiner atas ulasan dan masukannya yang berharga.