WebAssembly به ما امکان می دهد مرورگر را با ویژگی های جدید گسترش دهیم. این مقاله نحوه پورت رسیور ویدیوی AV1 و پخش ویدیوی AV1 را در هر مرورگر مدرن نشان می دهد.
یکی از بهترین چیزها در مورد WebAssembly ، آزمایش توانایی با قابلیتهای جدید و پیادهسازی ایدههای جدید قبل از اینکه مرورگر آن ویژگیها را به صورت بومی (اگر وجود داشته باشد) ارسال کند، است. می توانید به استفاده از WebAssembly به این روش به عنوان مکانیزم پلی پری با کارایی بالا فکر کنید، جایی که ویژگی خود را به جای جاوا اسکریپت به زبان C/C++ یا Rust می نویسید.
با انبوهی از کدهای موجود برای انتقال، می توان کارهایی را در مرورگر انجام داد که تا قبل از ظهور WebAssembly قابل اجرا نبودند.
این مقاله نمونهای از نحوه گرفتن کد منبع کدک ویدیویی AV1 موجود، ساختن یک پوشش برای آن، و امتحان آن در داخل مرورگر و نکاتی برای کمک به ساخت یک مهار آزمایشی برای اشکالزدایی wrapper را توضیح میدهد. کد منبع کامل برای مثال در اینجا برای مرجع در github.com/GoogleChromeLabs/wasm-av1 موجود است.
یکی از این دو فایل ویدئویی آزمایشی 24 فریم بر ثانیه را دانلود کنید و آنها را در نسخه نمایشی ساخته شده ما امتحان کنید.
انتخاب یک کد پایه جالب
چندین سال است که می بینیم که درصد زیادی از ترافیک وب را داده های ویدیویی تشکیل می دهد، سیسکو آن را در واقع تا 80 درصد تخمین زده است ! البته، فروشندگان مرورگرها و سایت های ویدیویی به شدت از تمایل به کاهش داده های مصرف شده توسط این همه محتوای ویدیویی آگاه هستند. البته نکته کلیدی آن فشرده سازی بهتر است و همانطور که انتظار دارید تحقیقات زیادی در مورد فشرده سازی ویدیوی نسل بعدی با هدف کاهش بار داده ارسال ویدیو به سراسر اینترنت انجام شده است.
همانطور که اتفاق می افتد، اتحاد برای رسانه های باز روی یک طرح فشرده سازی ویدیوی نسل بعدی به نام AV1 کار می کند که وعده می دهد اندازه داده های ویدیو را به میزان قابل توجهی کاهش دهد. در آینده، ما انتظار داریم مرورگرها پشتیبانی بومی برای AV1 ارائه دهند، اما خوشبختانه کد منبع برای کمپرسور و کمپرسور منبع باز هستند، که آن را به یک کاندید ایده آل برای تلاش برای کامپایل آن در WebAssembly تبدیل می کند تا بتوانیم با آن آزمایش کنیم. مرورگر
تطبیق برای استفاده در مرورگر
یکی از اولین کارهایی که برای وارد کردن این کد به مرورگر باید انجام دهیم این است که با کد موجود آشنا شویم تا بفهمیم API چگونه است. وقتی برای اولین بار به این کد نگاه می کنیم، دو چیز مشخص می شود:
- درخت منبع با استفاده از ابزاری به نام
cmake
ساخته شده است. و - تعدادی مثال وجود دارد که همه آنها نوعی رابط مبتنی بر فایل را فرض می کنند.
تمام نمونههایی که بهطور پیشفرض ساخته میشوند را میتوان در خط فرمان اجرا کرد، و این احتمالاً در بسیاری از پایههای کد دیگر موجود در جامعه صادق است. بنابراین، رابطی که ما قصد داریم برای اجرای آن در مرورگر بسازیم، می تواند برای بسیاری از ابزارهای خط فرمان دیگر مفید باشد.
استفاده از 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 فریم در ثانیه) چند چیز را به ما می آموزد:
- ساخت یک کد-پایه پیچیده برای اجرا در مرورگر با استفاده از WebAssembly کاملاً امکان پذیر است. و
- چیزی به اندازه CPU فشرده به عنوان رمزگشایی ویدیوی پیشرفته از طریق WebAssembly امکان پذیر است.
با این حال، محدودیتهایی وجود دارد: پیادهسازی همه روی رشته اصلی اجرا میشود و ما نقاشی و رمزگشایی ویدیو را در آن رشته به هم میزنیم. بارگذاری رمزگشایی در وبکارگر میتواند پخش نرمتری را برای ما فراهم کند، زیرا زمان رمزگشایی فریمها به شدت به محتوای آن فریم بستگی دارد و گاهی اوقات میتواند بیشتر از زمانی که بودجه در نظر گرفتهایم طول بکشد.
کامپایل در WebAssembly از پیکربندی AV1 برای یک نوع CPU عمومی استفاده می کند. اگر به صورت بومی در خط فرمان برای یک CPU عمومی کامپایل کنیم، بار CPU مشابهی را برای رمزگشایی ویدیو مانند نسخه WebAssembly مشاهده می کنیم، با این حال کتابخانه رمزگشا AV1 شامل پیاده سازی های SIMD است که تا 5 برابر سریعتر اجرا می شود. گروه جامعه WebAssembly در حال حاضر در حال کار بر روی گسترش استاندارد برای گنجاندن SIMD اولیه است، و زمانی که این اتفاق بیفتد، قول می دهد که رمزگشایی را به میزان قابل توجهی سرعت بخشد. وقتی این اتفاق بیفتد، رمزگشایی ویدیوی 4k HD در زمان واقعی از یک رمزگشای ویدیویی WebAssembly کاملاً امکان پذیر خواهد بود.
در هر صورت، کد مثال به عنوان یک راهنما برای کمک به انتقال هر ابزار خط فرمان موجود برای اجرا به عنوان یک ماژول WebAssembly مفید است و نشان می دهد که امروز چه چیزی در وب امکان پذیر است.
اعتبارات
با تشکر از جف پوسنیک، اریک بیدلمن و توماس اشتاینر برای ارائه نقد و بازخورد ارزشمند.