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üsünün nasıl taşınacağı ve AV1 videolarının herhangi bir modern tarayıcıda nasıl oynatılacağı açıklanmaktadır.

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'i bu şekilde, özelliğinizi JavaScript yerine C/C++ veya Rust'ta yazdığınız yüksek performanslı bir polyfill mekanizması olarak kullanabilirsiniz.

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 kodunun nasıl alınacağı, bunun için bir sarmalayıcı oluşturulacağı ve tarayıcınızda nasıl deneneceğiyle ilgili bir örnek açıklanmaktadır. Ayrıca, sarmalayıcıda hata ayıklama için bir test donanımı oluşturmaya yardımcı olacak ipuçları da 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ı bekleyebiliriz. Ancak kompresör ve dekompresör kaynak kodu açık kaynak olduğundan, bu kodu tarayıcıda deneysel olarak kullanabilmek için WebAssembly'e derlemeye çalışmak ideal bir seçenektir.

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 dikkat ç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. Bu durum, toplulukta bulunan diğer birçok kod tabanı için de geçerli olabilir. 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 olan Emscripten ile denemeler yapıyordu. AV1 deposunun kökündeki CMakeLists.txt dosyası aşağıdaki 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ç seti, biri asm.js, diğeri ise WebAssembly adlı iki biçimde çıkış oluşturabilir. Daha küçük çıktılar ürettiği ve daha hızlı çalışabildiği için WebAssembly'i hedefliyoruz. Mevcut bu derleme kuralları, kitaplığın bir asm.js sürümünü derlemek için tasarlanmıştır. Bu sürüm, bir video dosyasının içeriğine bakmak için kullanılan bir denetleyici uygulamasında kullanılır. Kullanım için WebAssembly çıkışına ihtiyacımız olduğundan bu satırları yukarıdaki kurallardaki kapatıcı endif() ifadesinden hemen önce ekleriz.

# 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ğırıyoruz. 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şturacak bir 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 kodlarını videodaki kareleri temsil eden bir dizi resme dönüştürür.

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

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 sayede, blob bir akış gibi "okunur" (yani sırayla okunan bir dosya gibi görünür).

Ö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'yi taklit ettik. Bu nedenle, API'mizin komut satırında çalışan ve DATA_Source API'mizin altında dosya I/O'yu 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. Amacımız doğrultusunda, oynatmaya başlamadan önce 10 karelik bir video arabelleği uygulayacağı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şleteceksek arabelleğe alma mekanizmasının mevcut olması 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 fazla bekleme süresi anlamına gelir. Çok az karenin arabelleğe alınması ise oynatma sırasında takılmalara 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 karesi gibi bir resmi alıp bazı geometrilere boyanacak bir doku olarak kullanmamızı sağlar. WebGL dünyasında her şey üçgenlerden oluşur. Bu nedenle, kendi durumumuzda WebGL'nin gl.TRIANGLE_FAN adlı kullanışlı bir yerleşik ö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üzün çıkışı, varsayılan çıkışın kanal başına 16 bit olduğu ve her U veya V değerinin gerçek çıkış resmindeki 4 piksele karşılık geldiği YUV biçimindeki resimlerdir. Tüm bunlar, görüntüyü görüntülemek için WebGL'ye iletmeden önce renk dönüştürmemiz gerektiği anlamına gelir.

Bunun için, kaynak dosyada yuv-to-rgb.c bulabileceğiniz bir AVX_YUV_to_RGB() işlevi uygularız. Bu işlev, AV1 kod çözücüsünün çıkışını WebGL'ye iletebileceğimiz bir forma dönüştürür. Bu işlevi JavaScript'den çağırırken, dönüştürülmüş resmi yazdığımız belleğin WebAssembly modülünün belleğinde ayrıldığından emin olmamız gerektiğini unutmayın. Aksi takdirde işlev bu belleğe erişemez. 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 dosyasında 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şlemini bu tek iş parçacığında birleştiririz. Çerçevelerin kodunu çözme süresi, çerçevenin içeriğine büyük ölçüde bağlı olduğundan ve bazen bütçelendirdiğimizden daha uzun sürebileceğinden, kod çözme işlemini bir web işleyiciye aktarmak daha sorunsuz bir oynatma sağlayabilir.

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.