WebAssembly ile tarayıcıyı genişletme

WebAssembly, tarayıcıyı yeni özelliklerle genişletmemize olanak tanır. Bu makalede, AV1 video kod çözücünün nasıl taşınacağı ve AV1 videonun herhangi bir modern tarayıcıda nasıl oynatılacağı gösterilmektedir.

Alex Danilo

WebAssembly'nin en iyi özelliklerinden biri, tarayıcı bu özellikleri yerel olarak kullanıma sunmadan önce (sunuyorsa) yeni özelliklerle deneme yapma ve yeni fikirleri uygulama olanağıdır. WebAssembly'yi bu şekilde kullanmayı, özelliğinizi JavaScript yerine C/C++ veya Rust kullanarak yazdığınız yüksek performanslı bir çoklu dolgu mekanizması olarak düşünebilirsiniz.

Taşıma işlemi için kullanılabilecek çok sayıda mevcut kod sayesinde, WebAssembly ortaya çıkana kadar tarayıcıda mümkün olmayan şeyler yapılabilir.

Bu makalede, mevcut AV1 video codec'i kaynak kodunu alma ve bu kod için sarmalayıcı derleme, tarayıcınızda bunu deneme ve sarmalayıcıda hata ayıklamak için bir test paketi oluşturmaya yardımcı olacak ipuçları verme konusunda bir örnek verilmektedir. Bu örnekteki tam kaynak kodunu referans olarak kullanmak için github.com/GoogleChromeLabs/wasm-av1 adresinden indirebilirsiniz.

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

İlginç bir kod tabanı seçme

Web'deki trafiğin büyük bir kısmının video verilerinden oluştuğunu yıllardır görüyoruz. Cisco, bu oranın %80'e kadar çıktığını tahmin ediyor. Elbette tarayıcı tedarikçileri ve video siteleri, tüm bu video içeriklerinin tükettiği verileri azaltma arzusunun farkındadır. Bunun anahtarı elbette daha iyi sıkıştırmadır. Tahmin edebileceğiniz gibi, internet üzerinden video göndermenin veri yükünü azaltmayı amaçlayan yeni nesil video sıkıştırma konusunda çok sayıda araştırma yapılmaktadır.

Alliance for Open Media, video verilerinin boyutunu önemli ölçüde küçültmeyi vaat eden AV1 adlı yeni nesil bir video sıkıştırma şeması üzerinde çalışıyor. Gelecekte tarayıcıların AV1 için yerel destek sunmasını bekleriz, ancak neyse ki kompresör ve dekompresörün kaynak kodu açık kaynaklıdır. Bu, onu tarayıcıda denemeler yapabilmemiz için WebAssembly'de derlemeye çalışmak için ideal bir adaydır.

Bunny film resmi.

Tarayıcıda kullanıma uyarlama

Bu kodu tarayıcıya almak için ilk yapmamız gerekenlerden biri, API'nin nasıl olduğunu anlamak için mevcut kodu tanımak. Bu koda ilk bakıldığında iki şey dikkati çeker:

  1. Kaynak ağacı, cmake adlı bir araç kullanılarak oluşturulur ve
  2. Hepsi bir tür dosya tabanlı arayüz kullanan çeşitli örnekler vardır.

Varsayılan olarak derlenen tüm örnekler komut satırında çalıştırılabilir ve bu, büyük olasılıkla topluluktaki diğer birçok kod tabanında da geçerlidir. Bu nedenle, tarayıcıda çalıştırabilmek için oluşturacağımız arayüz, diğer birçok komut satırı aracı için yararlı olabilir.

Kaynak kodunu derlemek için cmake kullanma

Neyse ki AV1 yazarları, WebAssembly sürümümüzü oluşturmak için kullanacağımız SDK Emscripten ile denemeler yapıyorlar. AV1 deposunun kök dizininde, 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, birine asm.js ve diğeri WebAssembly olmak üzere iki biçimde çıktı oluşturabilir. Daha küçük çıktılar ürettiği ve daha hızlı çalışabildiği için WebAssembly'i hedefliyoruz. Bu mevcut derleme kurallarının amacı, video dosyasının içeriklerini incelemek için yararlanılan bir denetleyici uygulamasında kitaplığın asm.js sürümünü derlemektir. 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 derleme yapmak, önce cmake'ü çalıştırarak bir miktar Makefiles oluşturmak ve ardından derleme adımını gerçekleştirecek make komutunu çalıştırmak anlamına gelir. Emscripten kullandığımızdan varsayılan ana makine derleyicisi yerine Emscripten derleyicisi araç setini kullanmamız gerektiğini unutmayın. Bu, Emscripten SDK'sının bir parçası olan Emscripten.cmake kullanılarak ve Emscripten.cmake'nin yolunun cmake'a parametre olarak iletilmesi ile yapılır. Makefile'leri oluşturmak için aşağıdaki komut satırını kullanırız:

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ığı kaynak dosyalarının konumunun 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ğlamak için bu dosyayı bulmak üzere 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 dosyasına bakarsanız bu komut dosyasının derlemeyi yapılandırmak için nasıl kullanıldığını görebilirsiniz.

Tüm kurulum tamamlandığında, make adlı işlevi çağırmamız yeterlidir. Bu işlev, örnekler dahil olmak üzere kaynak ağacının tamamını oluşturur ancak en önemlisi, derlenmiş ve projemize dahil etmeye hazır video kod çözücüyü içeren libaom.a dosyasını oluşturur.

Kitaplıkla arayüz oluşturmak için API tasarlama

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

AV1 kod ağacına göz atarken iyi bir başlangıç noktası, [simple_decoder.c](https://aomedia.googlesource.com/aom/+/master/examples/simple_decoder.c) dosyasında bulunan örnek video kod çözücüdür. Bu kod çözücü, bir IVF dosyasını okur ve videodaki kareleri temsil eden bir dizi resme dönüştürür.

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

Tarayıcımız dosya sisteminden dosya okuyamadığından, AV1 kitaplığımıza veri almak için örnek kod çözücüye benzer bir şey oluşturabilmek amacıyla G/Ç'mizi soyutlamamıza olanak tanıyan bir tür arayüz tasarlamamız gerekir.

Komut satırında dosya G/Ç'si, akış arayüzü olarak bilinir. Bu nedenle, akış G/Ç'sine benzeyen kendi arayüzümüze tanımlayabilir ve temel uygulamada istediğimiz her şeyi oluşturabiliriz.

Arayüzümüz şu şekilde tanımlanır:

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 sayede, bunları bir komut satırı uygulaması için dosya G/Ç ile kolayca eşleyebilir veya tarayıcıda çalıştırıldığında başka bir şekilde uygulayabiliriz. DATA_Source türü, JavaScript tarafında opaktır ve yalnızca arayüzü kapsüllemeyi sağlar. Dosya anlamlarını yakından takip eden bir API oluşturmanın, komut satırından (ör. diff, sed vb.) kullanılacak diğer birçok kod tabanında yeniden kullanılmasını kolaylaştırdığını unutmayın.

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

Örnek uygulamamız, iletilen blob'un sıralı olarak okunan bir veri kaynağıymış gibi okunmasını sağlar. Referans kodu blob-api.c dosyasında bulunabilir. Uygulamanın tamamı şu şekildedir:

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 etmek için test aparatı 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 çalışırken, tarayıcı dışında hata ayıklama yapabilmemiz ve oluşturduğumuz arayüzü test edebilmemiz için üzerinde çalıştığımız kodun arayüzü için bir tür birim testi oluşturmak mantıklıdır.

Bu örnekte AV1 kitaplığının arayüzü olarak akış tabanlı bir API emüle ederiz. Bu nedenle, API'mizin komut satırında çalışan ve DATA_Source API'mizin altında dosya I/O'sunu uygulayarak arka planda gerçek dosya I/O'su yapan bir sürümü oluşturmak için kullanabileceğimiz bir test donanımı oluşturmak mantıklı olacaktır.

Test aracımızın akış I/O kodu basittir ve şu şekilde 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ü, tarayıcıda ikili veri bloklarını kullanacak ve komut satırından test edilecek kodu oluştururken gerçek dosyalarla arayüz oluşturacak şekilde derleyebiliriz. Test aracı kodumuzu test.c örnek kaynak dosyasında bulabilirsiniz.

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

Video oynatırken daha sorunsuz bir oynatma deneyimi için birkaç karenin arabelleğe alınması yaygın bir uygulamadır. Amaçlarımız doğrultusunda yalnızca 10 karelik bir tampon uygulayacağız. Böylece, oynatmaya başlamadan önce 10 kareyi arabelleğe alacağız. Ardından, arabelleği dolu tutmak için her kare gösterildiğinde başka bir karenin kodunu çözmeye çalışırız. Bu yaklaşım, videonun takılmasını önlemek için karelerin önceden hazır olmasını sağlar.

Basit örneğimizde, sıkıştırılmış videonun tamamı okunabilir durumda olduğundan arabelleğe alma işlemine gerek yoktur. Ancak kaynak veri arayüzünü bir sunucudan gelen akış girişini destekleyecek şekilde genişletmek istiyorsak arabelleğe alma mekanizmasını kullanmamız gerekir.

AV1 kitaplığından video verisi karelerini okumak ve arabelleğe depolamak için decode-av1.c bölümündeki kod şu şekildedir:

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


Arabelleğin 10 kare video içermesini seçtik. Bu, tamamen keyfi bir seçimdir. Daha fazla karenin arabelleğe alınması, videonun oynatılmaya 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 durmaya neden olabilir. Yerel tarayıcı uygulamasında karelerin arabelleğe alınması bu uygulamadan çok daha karmaşıktır.

WebGL ile video karelerini sayfaya ekleme

Önbelleğe aldığımız video karelerinin sayfamızda gösterilmesi gerekir. Bu dinamik bir video içeriği olduğundan bunu mümkün olduğunca hızlı bir şekilde yapmak istiyoruz. Bunun için WebGL'den yararlanırız.

WebGL, video çerçevesi gibi bir görüntü alıp bunu belirli bir geometriye göre boyanan bir doku olarak kullanmamıza olanak tanır. WebGL dünyasında her şey üçgenlerden oluşur. Bizim örneğimizde, WebGL'nin gl.TRIANGLE_FAN adlı yerleşik bir özelliğini kullanabiliriz.

Ancak küçük bir sorun var. WebGL dokularının, renk kanalı başına bir bayt olan RGB resimler olması gerekir. AV1 kod çözücümüzden elde ettiğimiz çıkış, YUV biçiminde görüntülerdir. Varsayılan çıkış, kanal başına 16 bite sahiptir. Ayrıca her U veya V değeri, gerçek çıkış görüntüsünde 4 piksele karşılık gelir. Tüm bunlar, görüntüyü görüntüleme için WebGL'ye iletmeden önce renk dönüştürmemiz gerektiği anlamına gelir.

Bunun için bir AVX_YUV_to_RGB() işlevi uygularız. Bu işlevi yuv-to-rgb.c kaynak dosyasında bulabilirsiniz. Bu işlev, AV1 kod çözücüsünün çıkışını WebGL'ye iletebileceğimiz bir biçime dönüştürür. Bu işlevi JavaScript'ten çağırdığımızda, dönüştürülen resmi yazdığımız belleğin WebAssembly modülünün belleğinde ayrılmış olduğundan emin olmamız gerektiğini, aksi takdirde bu belleğe erişemeyeceğini unutmayın. WebAssembly modülünden bir resim alıp ekrana çizen işlev şudur:

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 boyamayı uygulayan drawImageToCanvas() işlevini referans olarak kullanmak için kaynak dosyadaki draw-image.js bölümünde bulabilirsiniz.

Gelecekteki çalışmalar ve çıkarımlar

Demomuzu iki test video dosyasında (24 fps video olarak kaydedilmiş) denediğimizde birkaç şey öğrendik:

  1. WebAssembly'i kullanarak tarayıcıda yüksek performansla çalışacak karmaşık bir kod tabanı oluşturmak tamamen mümkündür.
  2. Gelişmiş video kod çözme işlemi kadar yoğun CPU kullanımı gerektiren işlemler WebAssembly ile yapılabilir.

Bununla birlikte bazı sınırlamalar vardır: Uygulamanın tamamı ana iş parçacığında çalışır ve boyama ile video kod çözme işlemlerini bu tek iş parçacığında birleştiririz. Kod çözmeyi bir web işçisine boşaltmak bize daha sorunsuz bir oynatma sağlayabilir. Çünkü karelerin kodunu çözme süresi büyük ölçüde ilgili karenin içeriğine bağlıdır ve bazen bütçede belirtilenden daha fazla zaman alabilir.

WebAssembly'e derleme işleminde, genel bir CPU türü için AV1 yapılandırması kullanılır. Genel bir CPU için komut satırında yerel olarak derleyelim. Videonun kodunu çözmek için WebAssembly sürümünde olduğu gibi 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ı da içerir. WebAssembly Community Group şu anda standardı SIMD primitifleri içerecek şekilde genişletmek için çalışıyor. Bu özellik kullanıma sunulduğunda kod çözme işleminin önemli ölçüde hızlanması bekleniyor. Bu durumda, 4K HD videonun kodunu WebAssembly video kod çözücüsünden gerçek zamanlı olarak çözmek tamamen mümkün olacaktır.

Her durumda, örnek kod, mevcut komut satırı yardımcı programlarını WebAssembly modülü olarak çalıştırmaya yardımcı olacak bir kılavuz olarak kullanışlıdır ve web'de bugün neler yapılabileceğini gösterir.

Kredi

Değerli yorum ve geri bildirimler için Jeff Posnick, Eric Bidelman ve Thomas Steiner'a teşekkür ederiz.