WebAssembly ile tarayıcıyı genişletme

WebAssembly, tarayıcıyı yeni özelliklerle genişletmemize olanak tanıyor. Bu makalede, AV1 video kod çözücünün nasıl bağlanacağı ve AV1 videonun tüm modern tarayıcılarda nasıl oynatılacağı gösterilmektedir.

Alex Danilo

WebAssembly ile ilgili en iyi şeylerden biri, yeni özelliklerle beceri denemesi ve tarayıcı bu özellikleri yerel olarak (varsa) göndermeden önce yeni fikirleri uygulamaktır. WebAssembly'yi bu şekilde kullanmayı, özelliğinizi JavaScript yerine C/C++ veya Rust'ta yazdığınız yüksek performanslı bir çoklu dolgu mekanizması gibi düşünebilirsiniz.

Taşıma için çok sayıda mevcut kod bulunduğundan, WebAssembly gelene kadar geçerli olmayan şeyleri tarayıcıda yapmak mümkündür.

Bu makalede, mevcut AV1 video codec kaynak kodunu alma, bunun için sarmalayıcı oluşturma ve bunu tarayıcınızda denemeyle ilgili bir örnek açıklanacaktır. Ayrıca, sarmalayıcının hatalarını ayıklamak için test ekipmanı oluşturmaya yardımcı olacak ipuçlarını da bulabilirsiniz. Buradaki örnekle ilgili kaynak kodun tamamını referans olması için github.com/GoogleChromeLabs/wasm-av1 adresinde bulabilirsiniz.

Bu iki 24 fps test video dosyasından birini indirip yerleşik demomuzda deneyin.

İlginç bir kod tabanı seçme

Birkaç yıldır, web'deki trafiğin büyük bir yüzdesinin video verilerinden oluştuğunu gördük. Cisco, bu verinin% 80 kadar olduğunu tahmin ediyor! Tarayıcı tedarikçileri ve video siteleri, tüm bu video içeriği tarafından tüketilen verileri azaltma arzusunun kesinlikle farkındadır. Bunun anahtarı elbette daha iyi bir sıkıştırmadır. Tahmin edeceğiniz gibi, internet üzerinden video göndermenin veri yükünü azaltmayı amaçlayan yeni nesil video sıkıştırma ile ilgili birçok araştırma yapılmıştır.

Bu süreçte Alliance for Open Media, video veri boyutunu önemli ölçüde küçültmeyi vaat eden AV1 adlı yeni nesil video sıkıştırma şeması üzerinde çalışıyor. Gelecekte tarayıcıların AV1 için yerel destek sunmasını bekleriz, ancak kompresör ve decompresörün kaynak kodu açık kaynaktır. Bu da bu kodu tarayıcıda deneyebilmemiz için WebAssembly'de derlemeye çalışmak için ideal bir aday haline getirir.

Tavşan filmi resmi.

Tarayıcıda kullanım için uyarlama

Bu kodu tarayıcıya almak için yapmamız gereken ilk şeylerden biri, mevcut kodu öğrenerek API'nin neye benzediğini anlamaktır. Bu koda ilk baktığımızda, iki nokta göze çarpmaktadır:

  1. Kaynak ağaç, cmake adlı bir araç kullanılarak oluşturulmuştur.
  2. Tüm örneklerde dosya tabanlı bir arayüz olduğu varsayılır.

Varsayılan olarak derlenen tüm örnekler komut satırında çalıştırılabilir. Bu durum, topluluktaki diğer kod tabanlarında da geçerli olacaktır. Bu nedenle, tarayıcıda çalışması için oluşturacağımız arayüz, diğer birçok komut satırı aracı için yararlı olabilir.

Kaynak kodu derlemek için cmake kullanma

Neyse ki AV1 yazarları WebAssembly sürümümüzü oluşturmak için kullanacağımız SDK olan Emscripten ile deneyler yapıyorlar. AV1 deposunun kök kısmındaki dosya CMakeLists.txtşu derleme kurallarını içerir:

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

Emscripten araç zinciri, biri asm.js, diğeri WebAssembly adlı iki biçimde çıkış oluşturabilir. Daha küçük çıktı üretip daha hızlı çalışabildiği için WebAssembly'yi hedefleyeceğiz. Bu mevcut derleme kurallarının, video dosyasının içeriğine bakmak için kullanılan bir denetleyici uygulamada kullanılmak üzere kitaplığın asm.js sürümünü derlemesi amaçlanmıştır. Kullanımımız için WebAssembly çıkışına ihtiyacımız var. Bu nedenle, bu satırları yukarıdaki kurallarda yer alan endif() kapanış ifadesinden hemen önce ekliyoruz.

# Force generation of Wasm instead of asm.js
append_link_flag_to_target("inspect" "-s WASM=1")
append_compiler_flag("-s WASM=1")

cmake ile derlemek, önce cmake kendisini çalıştırarak ve ardından derleme adımını gerçekleştirecek make komutunu çalıştırarak bir miktar Makefiles oluşturmak anlamına gelir. Emscripten kullandığımız için varsayılan ana makine derleyicisi yerine Emscripten derleyici araç zincirini kullanmamız gerektiğini unutmayın. Bu, Emscripten SDK'sının bir parçası olan Emscripten.cmake kullanılarak ve yolunu cmake parametresi olarak geçirilerek gerçekleştirilir. Makefiles oluşturmak için kullandığımız komut satırı aşağıdaki gibidir:

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

path/to/aom parametresi, AV1 kitaplık kaynak dosyalarının bulunduğu konumun tam yoluna ayarlanmalıdır. path/to/emsdk-portable/…/Emscripten.cmake parametresinin, Emscripten.cmake araç zinciri açıklama dosyasının yoluna ayarlanması gerekir.

Kolaylık sağlaması açısından, bu dosyayı bulmak için bir kabuk komut dosyası kullanırız:

#!/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

Bu projenin üst düzey Makefile öğesine bakarsanız bu komut dosyasının derlemeyi yapılandırmak için nasıl kullanıldığını görebilirsiniz.

Tüm kurulum tamamlandı. Bu işlem, örnekler de dahil olmak üzere kaynak ağacının tamamını oluşturacak make adını verir. Ancak en önemlisi, derlenen ve projemize dahil edilmeye hazır video kod çözücüyü içeren libaom.a öğesi oluşturur.

Kitaplık arayüzü için API tasarlama

Kitaplığımızı oluşturduktan sonra, sıkıştırılmış video verilerini göndermek için bu kitaplıkla nasıl arayüz kuracağımızı öğrenmemiz ve ardından tarayıcıda görüntüleyeceğimiz video karelerini okumamız gerekir.

AV1 kod ağacında, [simple_decoder.c](https://aomedia.googlesource.com/aom/+/master/examples/simple_decoder.c) dosyasında bulabileceğiniz örnek bir video kod çözücü iyi bir başlangıç noktasıdır. Bu kod çözücü, IVF dosyasını okur ve bu dosyanın kodunu videodaki kareleri temsil eden bir dizi görüntü olarak çözer.

Arayüzümüzü [decode-av1.c](https://github.com/GoogleChromeLabs/wasm-av1/blob/master/decode-av1.c) adlı kaynak dosyada uyguluyoruz.

Tarayıcımız dosya sisteminden dosya okuyamadığından, I/O'muzu soyutlamamıza olanak tanıyan bir arayüz biçimi tasarlamamız gerekir. Böylece, verileri AV1 kitaplığımıza almak üzere örnek kod çözücüye benzer bir şey oluşturabiliriz.

Komut satırında dosya G/Ç, akış arayüzü olarak bilinir. Böylece akış G/Ç'ye benzeyen kendi arayüzümüzü tanımlayabilir ve temel uygulamada istediğimizi yapabiliriz.

Arayüzümüzü aşağıdaki şekilde tanımlarız:

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

open/read/empty/close işlevleri normal dosya G/Ç işlemlerine çok benzer. Bu işlem, bunları bir komut satırı uygulaması için dosya G/Ç ile kolayca eşlememize veya bir tarayıcı içinde çalıştırıldığında başka bir şekilde uygulamamıza olanak tanır. DATA_Source türü, JavaScript tarafında opaktır ve yalnızca arayüzü kapsüllemeye yarar. Dosya semantiğini yakından takip eden bir API oluşturmanın, komut satırından (ör. diff, sed vb.) kullanılması amaçlanan diğer birçok kod tabanında yeniden kullanımı kolaylaştırdığını unutmayın.

Ayrıca, ham ikili verileri akış G/Ç işlevlerimize bağlayan DS_set_blob adında bir yardımcı işlev tanımlamamız gerekir. Bu, blob'un bir akış (sırayla okunan bir dosya gibi) gibi "okunmasını" sağlar.

Örnek uygulamamız, blob'ta geçirilen verilerin, sıralı olarak okunan bir veri kaynağı gibi okunmasını sağlar. Referans kodunu blob-api.c dosyasında bulabilirsiniz. Uygulama şekli şöyledir:

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

Tarayıcı dışında test yapmak için bir test bandı oluşturma

Yazılım mühendisliğindeki en iyi uygulamalardan biri, entegrasyon testleriyle birlikte kod için birim testleri oluşturmaktır.

Tarayıcıda WebAssembly ile derleme yaparken, üzerinde çalıştığımız kodun arayüzü için bir tür birim testi oluşturmak mantıklıdır. Böylece tarayıcı dışında hata ayıklama yapabilir ve oluşturduğumuz arayüzü test edebiliriz.

Bu örnekte, AV1 kitaplığına arayüz olarak akış tabanlı bir API emüle ediyoruz. Bu nedenle, API'mizin komut satırında çalışan ve dosya G/Ç'sini arka planında uygulayan bir API sürümü oluşturmak için kullanabileceğimiz bir test grubu oluşturmak mantıklıdır. Bunun için dosya G/Ç'sini DATA_Source API'mızın altına uygularız.

Test bandımız için akış G/Ç kodu basittir ve şöyle görünür:

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

Akış arayüzünü soyutlayarak WebAssembly modülümüzü derleyerek tarayıcıdayken ikili veri blob'larını, komut satırından test etmek için kodu oluşturduğumuzda ise gerçek dosyaların arayüzünü oluşturabiliriz. Test bandı kodumuz, örnek kaynak dosyasında test.c bulunabilir.

Birden fazla video karesi için arabelleğe alma mekanizması uygulama

Video oynatırken daha düzgün bir oynatma için birkaç karenin arabelleğe alınması yaygın bir uygulamadır. Amaçlarımız doğrultusunda yalnızca 10 karelik bir video arabelleği uygulayacağız. Böylece, oynatmaya başlamadan önce 10 kareyi arabelleğe alacağız. Daha sonra, bir kare her görüntülendiğinde arabelleği dolu tutmak için başka bir karenin kodunu çözmeye çalışırız. Bu yaklaşım, videoda takılmayı durdurmaya yardımcı olması için karelerin önceden sunulmasını sağlar.

Basit örneğimizde sıkıştırılmış videonun tamamı okunabiliyor, dolayısıyla arabelleğe alma işlemi gerçekten gerekli değil. Ancak kaynak veri arayüzünü bir sunucudan gelen akışı destekleyecek şekilde genişletmek istiyorsak arabelleğe alma mekanizmasına sahip olmamız gerekir.

AV1 kitaplığından video verisi karelerini okumak ve arabelleğe şu şekilde depolamak için decode-av1.c içindeki kod:

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


Tamponun 10 video karesi içermesini seçtik. Bu, rastgele bir seçimdir. Daha fazla karenin arabelleğe alınması, videonun oynatmaya başlaması için daha uzun bekleme süresi anlamına gelir. Çok az sayıda karenin arabelleğe alınması ise oynatma sırasında duraklamaya neden olabilir. Yerel bir tarayıcı uygulamasında çerçevelerin arabelleğe alınması bu uygulamadan çok daha karmaşıktır.

WebGL ile video çerçevelerini sayfaya alma

Arabelleğe aldığımız video karelerinin sayfamızda görüntülenmesi gerekir. Bu dinamik bir video içeriği olduğundan, bunu mümkün olduğunca hızlı bir şekilde yapabilmek istiyoruz. Bunun için WebGL'yi kullanıyoruz.

WebGL, video çerçevesi gibi bir görüntü alıp bunu bir geometrik şekil üzerine boyanan bir doku olarak kullanmamızı sağlar. WebGL dünyasında her şey üçgenlerden oluşur. Bizim örneğimizde, WebGL'nin gl.TRIANGLE_FAN adlı yerleşik ve kullanışlı bir özelliğini kullanabiliriz.

Ancak küçük bir sorun var. WebGL dokularının, renk kanalı başına bir bayt olacak şekilde RGB görüntüleri olduğu kabul edilir. AV1 kod çözücümüzden elde edilen çıktı, YUV biçimindeki resimlerdir. Bu biçimde, varsayılan çıkışın kanal başına 16 biti vardır ve her bir U veya V değeri, gerçek çıkış resminde 4 piksele karşılık gelir. Bu, görüntüyü görüntülemek üzere WebGL'ye geçirebilmemiz için önce renklere dönüştürmemiz gerektiği anlamına gelir.

Bunun için, yuv-to-rgb.c kaynak dosyasında bulabileceğiniz bir AVX_YUV_to_RGB() işlevi uyguluyoruz. Bu işlev, AV1 kod çözücüden gelen çıktıyı WebGL'ye iletebileceğimiz bir şeye dönüştürür. Bu işlevi JavaScript'ten çağırdığımızda, dönüştürülen görüntüyü yazdığımız belleğin WebAssembly modülünün belleğinde ayrıldığından emin olmamız gerektiğini unutmayın. Aksi takdirde bellek bu modüle erişemez. WebAssembly modülünden bir görüntü alıp ekrana kopyalama işlevi şöyledir:

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

WebGL boyamasını uygulayan drawImageToCanvas() işlevi, referans olması için draw-image.js kaynak dosyasında bulunabilir.

Gelecekteki çalışmalar ve ana fikirler

Demomuzu iki test videosunda deneyerek (24 f.p.s videosu olarak kaydedilmiştir) bazı şeyler öğrenebilirsiniz:

  1. WebAssembly kullanarak tarayıcıda iyi performans gösterecek karmaşık bir kod tabanı oluşturmak tamamen uygundur ve
  2. WebAssembly aracılığıyla gelişmiş video kod çözme işlemi yapabildiğiniz kadar CPU yoğun bir işlem yapmak da mümkündür.

Yine de bazı sınırlamalar vardır: Uygulamanın tamamı ana iş parçacığında çalışır ve bu tek iş parçacığında boyama ve video kodu çözmeyi ekleriz. Karelerin kodunu çözme süresi büyük ölçüde söz konusu karenin içeriğine bağlı olduğundan ve bazen bütçemizde belirtilenden daha fazla zaman alabildiğinden, kod çözme işleminin bir web çalışanına boşaltılması bize daha sorunsuz oynatma sağlayabilir.

WebAssembly derlemesi, genel bir CPU türü için AV1 yapılandırmasını kullanır. Genel bir CPU için komut satırında yerel olarak derleme yaparsak videonun kodunu WebAssembly sürümünde olduğu gibi çözmek için benzer bir CPU yükü görürüz. Ancak AV1 kod çözücü kitaplığı, 5 kata kadar daha hızlı çalışan SIMD uygulamalarını da içerir. WebAssembly Topluluk Grubu şu anda standardı SIMD temel öğelerini içerecek şekilde genişletmek için çalışıyor ve bu adımla birlikte kod çözmeyi önemli ölçüde hızlandırmayı taahhüt ediyor. Bu olduğunda, 4K HD videonun kodunu WebAssembly video kod çözücüden gerçek zamanlı olarak çözmek tamamen makul olacaktır.

Her durumda örnek kod, mevcut herhangi bir komut satırı yardımcı programını WebAssembly modülü olarak çalışacak şekilde taşımanıza yardımcı olacak bir rehber olarak faydalıdır ve halihazırda web'de halihazırda neler yapılabileceğini gösterir.

Kredi

Değerli yorumlar ve geri bildirimler sağlayan Jeff Posnick, Eric Bidelman ve Thomas Steiner'a teşekkür ederiz.