گسترش مرورگر با WebAssembly

WebAssembly به ما امکان می دهد مرورگر را با ویژگی های جدید گسترش دهیم. این مقاله نحوه پورت رسیور ویدیوی AV1 و پخش ویدیوی AV1 را در هر مرورگر مدرن نشان می دهد.

Alex Danilo

یکی از بهترین چیزها در مورد WebAssembly ، آزمایش توانایی با قابلیت‌های جدید و پیاده‌سازی ایده‌های جدید قبل از اینکه مرورگر آن ویژگی‌ها را به صورت بومی (اگر وجود داشته باشد) ارسال کند، است. می توانید به استفاده از WebAssembly به این روش به عنوان مکانیزم پلی پری با کارایی بالا فکر کنید، جایی که ویژگی خود را به جای جاوا اسکریپت به زبان C/C++ یا Rust می نویسید.

با انبوهی از کدهای موجود برای انتقال، می توان کارهایی را در مرورگر انجام داد که تا قبل از ظهور WebAssembly قابل اجرا نبودند.

این مقاله نمونه‌ای از نحوه گرفتن کد منبع کدک ویدیویی AV1 موجود، ساختن یک پوشش برای آن، و امتحان آن در داخل مرورگر و نکاتی برای کمک به ساخت یک مهار آزمایشی برای اشکال‌زدایی wrapper را توضیح می‌دهد. کد منبع کامل برای مثال در اینجا برای مرجع در github.com/GoogleChromeLabs/wasm-av1 موجود است.

یکی از این دو فایل ویدئویی آزمایشی 24 فریم بر ثانیه را دانلود کنید و آنها را در نسخه نمایشی ساخته شده ما امتحان کنید.

انتخاب یک کد پایه جالب

چندین سال است که می بینیم که درصد زیادی از ترافیک وب را داده های ویدیویی تشکیل می دهد، سیسکو آن را در واقع تا 80 درصد تخمین زده است ! البته، فروشندگان مرورگرها و سایت های ویدیویی به شدت از تمایل به کاهش داده های مصرف شده توسط این همه محتوای ویدیویی آگاه هستند. البته نکته کلیدی آن فشرده سازی بهتر است و همانطور که انتظار دارید تحقیقات زیادی در مورد فشرده سازی ویدیوی نسل بعدی با هدف کاهش بار داده ارسال ویدیو به سراسر اینترنت انجام شده است.

همانطور که اتفاق می افتد، اتحاد برای رسانه های باز روی یک طرح فشرده سازی ویدیوی نسل بعدی به نام AV1 کار می کند که وعده می دهد اندازه داده های ویدیو را به میزان قابل توجهی کاهش دهد. در آینده، ما انتظار داریم مرورگرها پشتیبانی بومی برای AV1 ارائه دهند، اما خوشبختانه کد منبع برای کمپرسور و کمپرسور منبع باز هستند، که آن را به یک کاندید ایده آل برای تلاش برای کامپایل آن در WebAssembly تبدیل می کند تا بتوانیم با آن آزمایش کنیم. مرورگر

تصویر فیلم خرگوش.

تطبیق برای استفاده در مرورگر

یکی از اولین کارهایی که برای وارد کردن این کد به مرورگر باید انجام دهیم این است که با کد موجود آشنا شویم تا بفهمیم API چگونه است. وقتی برای اولین بار به این کد نگاه می کنیم، دو چیز مشخص می شود:

  1. درخت منبع با استفاده از ابزاری به نام cmake ساخته شده است. و
  2. تعدادی مثال وجود دارد که همه آنها نوعی رابط مبتنی بر فایل را فرض می کنند.

تمام نمونه‌هایی که به‌طور پیش‌فرض ساخته می‌شوند را می‌توان در خط فرمان اجرا کرد، و این احتمالاً در بسیاری از پایه‌های کد دیگر موجود در جامعه صادق است. بنابراین، رابطی که ما قصد داریم برای اجرای آن در مرورگر بسازیم، می تواند برای بسیاری از ابزارهای خط فرمان دیگر مفید باشد.

استفاده از cmake برای ساخت کد منبع

خوشبختانه، نویسندگان AV1 با Emscripten ، SDK که ما از آن برای ساختن نسخه WebAssembly خود استفاده می کنیم، آزمایش کرده اند. در ریشه مخزن AV1 ، فایل CMakeLists.txt حاوی این قوانین ساخت است:

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 می تواند خروجی را در دو فرمت تولید کند، یکی asm.js نامیده می شود و دیگری WebAssembly. ما WebAssembly را هدف قرار خواهیم داد زیرا خروجی کمتری تولید می کند و می تواند سریعتر اجرا شود. این قوانین ساخت موجود برای کامپایل یک نسخه asm.js از کتابخانه برای استفاده در یک برنامه بازرسی است که برای مشاهده محتوای یک فایل ویدیویی استفاده می شود. برای استفاده ما، به خروجی WebAssembly نیاز داریم، بنابراین این خطوط را درست قبل از پایان دستور endif() در قوانین بالا اضافه می کنیم.

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

ساختن با cmake به این معنی است که ابتدا با اجرای خود cmake مقداری Makefiles تولید کنید و سپس دستور make اجرا کنید که مرحله کامپایل را انجام می دهد. توجه داشته باشید که از آنجایی که ما از Emscripten استفاده می کنیم، باید از زنجیره ابزار کامپایلر Emscripten به جای کامپایلر میزبان پیش فرض استفاده کنیم. این امر با استفاده از Emscripten.cmake که بخشی از Emscripten SDK است و ارسال مسیر آن به عنوان پارامتر به خود cmake به دست می آید. خط فرمان زیر همان چیزی است که برای تولید Makefiles استفاده می کنیم:

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 باید روی مسیر کامل محل فایل‌های منبع کتابخانه AV1 تنظیم شود. پارامتر path/to/emsdk-portable/…/Emscripten.cmake باید روی مسیر فایل توضیحات زنجیره ابزار Emscripten.cmake تنظیم شود.

برای راحتی، از یک اسکریپت پوسته برای یافتن آن فایل استفاده می کنیم:

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

اگر به Makefile سطح بالای این پروژه نگاه کنید، می توانید ببینید که چگونه از آن اسکریپت برای پیکربندی بیلد استفاده می شود.

اکنون که تمام تنظیمات انجام شده است، ما به سادگی make فراخوانی می‌کنیم که کل درخت منبع، از جمله نمونه‌ها را می‌سازد، اما مهم‌تر از همه، libaom.a را ایجاد می‌کند که حاوی رمزگشای ویدیویی است که کامپایل شده و آماده است تا در پروژه خود گنجانده شود.

طراحی یک API برای رابط با کتابخانه

هنگامی که کتابخانه خود را ساختیم، باید چگونگی ارتباط با آن را برای ارسال داده‌های ویدئوی فشرده به آن و سپس خواندن فریم‌های پشتی ویدیویی که می‌توانیم در مرورگر نمایش دهیم، بررسی کنیم.

با نگاهی به داخل درخت کد AV1، یک نقطه شروع خوب یک نمونه رمزگشای ویدیویی است که می‌توانید در فایل [simple_decoder.c](https://aomedia.googlesource.com/aom/+/master/examples/simple_decoder.c) پیدا کنید. [simple_decoder.c](https://aomedia.googlesource.com/aom/+/master/examples/simple_decoder.c) . آن رمزگشا در یک فایل IVF می خواند و آن را به مجموعه ای از تصاویر رمزگشایی می کند که فریم های موجود در ویدئو را نشان می دهد.

ما رابط خود را در فایل منبع [decode-av1.c](https://github.com/GoogleChromeLabs/wasm-av1/blob/master/decode-av1.c) پیاده سازی می کنیم.

از آنجایی که مرورگر ما نمی‌تواند فایل‌ها را از سیستم فایل بخواند، باید نوعی رابط طراحی کنیم که به ما امکان می‌دهد ورودی/خروجی خود را انتزاع کنیم تا بتوانیم چیزی شبیه به رمزگشای نمونه بسازیم تا داده‌ها را به کتابخانه AV1 خود وارد کنیم.

در خط فرمان، فایل ورودی/خروجی چیزی است که به عنوان رابط جریان شناخته می‌شود، بنابراین ما می‌توانیم رابط کاربری خود را که شبیه ورودی/خروجی جریان است تعریف کنیم و هر آنچه را که دوست داریم در پیاده‌سازی اصلی بسازیم.

ما رابط کاربری خود را اینگونه تعریف می کنیم:

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 بسیار شبیه عملیات ورودی/خروجی فایل معمولی هستند که به ما اجازه می‌دهد به راحتی آن‌ها را روی فایل ورودی/خروجی برای یک برنامه خط فرمان نگاشت کنیم، یا زمانی که در مرورگر اجرا می‌شوند، آن‌ها را به روش دیگری پیاده‌سازی کنیم. نوع DATA_Source از سمت جاوا اسکریپت مات است و فقط برای کپسوله کردن رابط عمل می کند. توجه داشته باشید که ساختن یک API که دقیقاً از معنای فایل پیروی می کند، استفاده مجدد را در بسیاری از پایه های کد دیگر که برای استفاده از خط فرمان در نظر گرفته شده اند آسان می کند (مانند diff، sed و غیره).

ما همچنین باید یک تابع کمکی به نام DS_set_blob تعریف کنیم که داده های باینری خام را به توابع ورودی/خروجی جریانی ما متصل می کند. این به لکه اجازه می‌دهد به‌گونه‌ای «خوانده» شود که انگار یک جریان است (یعنی شبیه یک فایل خوانده شده متوالی است).

پیاده سازی مثال ما خواندن اطلاعات ارسال شده در blob را به گونه ای امکان پذیر می کند که گویی یک منبع داده به طور متوالی خوانده شده است. کد مرجع را می توان در فایل blob-api.c پیدا کرد و کل پیاده سازی فقط به این صورت است:

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

ساخت یک مهار تست برای آزمایش خارج از مرورگر

یکی از بهترین روش‌ها در مهندسی نرم‌افزار، ساختن تست‌های واحد برای کد در ارتباط با تست‌های یکپارچه‌سازی است.

هنگام ساخت با WebAssembly در مرورگر، منطقی است که نوعی تست واحد برای رابط با کدی که با آن کار می کنیم ایجاد کنیم تا بتوانیم خارج از مرورگر را اشکال زدایی کنیم و همچنین بتوانیم رابطی را که ساخته ایم آزمایش کنیم. .

در این مثال ما یک API مبتنی بر جریان را به عنوان رابط کتابخانه AV1 شبیه سازی کرده ایم. بنابراین، منطقاً منطقی است که یک مهار تست بسازیم که بتوانیم از آن برای ساختن نسخه‌ای از API خود استفاده کنیم که در خط فرمان اجرا می‌شود و با پیاده‌سازی فایل I/O در زیر DATA_Source API ما، I/O واقعی فایل را در زیر هود انجام می‌دهد. .

کد ورودی/خروجی جریان برای مهار تست ما ساده است و به شکل زیر است:

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

با انتزاع کردن رابط استریم، می‌توانیم ماژول WebAssembly خود را بسازیم تا از حباب‌های داده باینری در مرورگر استفاده کند، و هنگامی که کد را برای آزمایش از خط فرمان می‌سازیم، با فایل‌های واقعی رابط ایجاد کنیم. کد مهار تست ما را می توان در نمونه فایل منبع test.c یافت.

پیاده سازی مکانیزم بافر برای چندین فریم ویدیو

هنگام پخش ویدیو، معمول است که چند فریم را بافر کنید تا به پخش روان‌تر کمک کنید. برای اهداف خود ما فقط یک بافر از 10 فریم ویدیو را پیاده سازی می کنیم، بنابراین قبل از شروع پخش، 10 فریم را بافر می کنیم. سپس هر بار که یک فریم نمایش داده می شود، سعی می کنیم فریم دیگری را رمزگشایی کنیم تا بافر را پر نگه داریم. این رویکرد اطمینان حاصل می کند که فریم هایی از قبل برای کمک به توقف لکنت ویدیو در دسترس هستند.

با مثال ساده ما، کل ویدیوی فشرده برای خواندن در دسترس است، بنابراین بافر واقعاً مورد نیاز نیست. با این حال، اگر بخواهیم رابط داده منبع را برای پشتیبانی از ورودی جریان از یک سرور گسترش دهیم، باید مکانیسم بافر را در جای خود داشته باشیم.

کد موجود در decode-av1.c برای خواندن فریم های داده های ویدئویی از کتابخانه AV1 و ذخیره در بافر به شرح زیر است:

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


ما انتخاب کرده‌ایم که بافر حاوی 10 فریم ویدیو باشد، که فقط یک انتخاب دلخواه است. بافر کردن فریم‌های بیشتر به معنای زمان انتظار بیشتر برای شروع پخش ویدیو است، در حالی که بافر کردن فریم‌های بسیار کم می‌تواند باعث توقف در حین پخش شود. در پیاده سازی مرورگر بومی، بافر کردن فریم ها بسیار پیچیده تر از این پیاده سازی است.

با WebGL فریم های ویدیویی را به صفحه وارد کنید

فریم های ویدیویی که بافر کرده ایم باید در صفحه ما نمایش داده شوند. از آنجایی که این محتوای ویدیویی پویا است، می‌خواهیم بتوانیم آن را در سریع‌ترین زمان ممکن انجام دهیم. برای آن، ما به WebGL مراجعه می کنیم.

WebGL به ما امکان می‌دهد یک تصویر، مانند یک فریم ویدیو، بگیریم و از آن به عنوان بافتی استفاده کنیم که هندسه‌ای را به تصویر می‌کشد. در دنیای WebGL همه چیز از مثلث تشکیل شده است. بنابراین، برای مورد ما می‌توانیم از یک ویژگی داخلی راحت WebGL به نام gl.TRIANGLE_FAN استفاده کنیم.

با این حال، یک مشکل جزئی وجود دارد. بافت های WebGL قرار است تصاویر RGB، یک بایت در هر کانال رنگی باشند. خروجی رسیور AV1 ما تصاویری با فرمت به اصطلاح YUV است، که در آن خروجی پیش فرض دارای 16 بیت در هر کانال است و همچنین هر مقدار U یا V مربوط به 4 پیکسل در تصویر خروجی واقعی است. همه این بدان معنی است که ما باید قبل از اینکه بتوانیم تصویر را برای نمایش به WebGL ارسال کنیم، آن را رنگی تبدیل کنیم.

برای انجام این کار، ما یک تابع AVX_YUV_to_RGB() را پیاده سازی می کنیم که می توانید آن را در فایل منبع yuv-to-rgb.c بیابید. این تابع خروجی رمزگشای AV1 را به چیزی تبدیل می کند که می توانیم به WebGL ارسال کنیم. توجه داشته باشید که وقتی این تابع را از جاوا اسکریپت فرا می‌خوانیم، باید مطمئن شویم که حافظه‌ای که تصویر تبدیل‌شده را به آن می‌نویسیم، در حافظه ماژول WebAssembly تخصیص داده شده است - در غیر این صورت نمی‌تواند به آن دسترسی پیدا کند. عملکرد بیرون آوردن یک تصویر از ماژول WebAssembly و رنگ آمیزی آن بر روی صفحه به این صورت است:

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

تابع drawImageToCanvas() که نقاشی WebGL را پیاده سازی می کند را می توان برای مرجع در فایل منبع draw-image.js یافت.

کارهای آینده و چیزهای آماده

آزمایش نسخه ی نمایشی ما روی دو فایل ویدیوی آزمایشی (ضبط شده به صورت ویدیوی 24 فریم در ثانیه) چند چیز را به ما می آموزد:

  1. ساخت یک کد-پایه پیچیده برای اجرا در مرورگر با استفاده از WebAssembly کاملاً امکان پذیر است. و
  2. چیزی به اندازه CPU فشرده به عنوان رمزگشایی ویدیوی پیشرفته از طریق WebAssembly امکان پذیر است.

با این حال، محدودیت‌هایی وجود دارد: پیاده‌سازی همه روی رشته اصلی اجرا می‌شود و ما نقاشی و رمزگشایی ویدیو را در آن رشته به هم می‌زنیم. بارگذاری رمزگشایی در وب‌کارگر می‌تواند پخش نرم‌تری را برای ما فراهم کند، زیرا زمان رمزگشایی فریم‌ها به شدت به محتوای آن فریم بستگی دارد و گاهی اوقات می‌تواند بیشتر از زمانی که بودجه در نظر گرفته‌ایم طول بکشد.

کامپایل در WebAssembly از پیکربندی AV1 برای یک نوع CPU عمومی استفاده می کند. اگر به صورت بومی در خط فرمان برای یک CPU عمومی کامپایل کنیم، بار CPU مشابهی را برای رمزگشایی ویدیو مانند نسخه WebAssembly مشاهده می کنیم، با این حال کتابخانه رمزگشا AV1 شامل پیاده سازی های SIMD است که تا 5 برابر سریعتر اجرا می شود. گروه جامعه WebAssembly در حال حاضر در حال کار بر روی گسترش استاندارد برای گنجاندن SIMD اولیه است، و زمانی که این اتفاق بیفتد، قول می دهد که رمزگشایی را به میزان قابل توجهی سرعت بخشد. وقتی این اتفاق بیفتد، رمزگشایی ویدیوی 4k HD در زمان واقعی از یک رمزگشای ویدیویی WebAssembly کاملاً امکان پذیر خواهد بود.

در هر صورت، کد مثال به عنوان یک راهنما برای کمک به انتقال هر ابزار خط فرمان موجود برای اجرا به عنوان یک ماژول WebAssembly مفید است و نشان می دهد که امروز چه چیزی در وب امکان پذیر است.

وام

با تشکر از جف پوسنیک، اریک بیدلمن و توماس اشتاینر برای ارائه نقد و بازخورد ارزشمند.