يتيح لنا WebAssembly توسيع نطاق المتصفّح من خلال إضافة ميزات جديدة. توضّح هذه المقالة كيفية نقل برنامج ترميز الفيديو AV1 وتشغيل فيديو AV1 في أي متصفّح حديث.
من أفضل ميزات WebAssembly هو القدرة على تجربة إمكانات جديدة وتنفيذ أفكار جديدة قبل أن يطرح المتصفّح هذه الميزات بشكل أصلي (إن أمكن). يمكنك استخدام WebAssembly بهذه الطريقة كآلية polyfill عالية الأداء، حيث يمكنك كتابة ميزتك بلغة C/C++ أو Rust بدلاً من JavaScript.
مع توفّر الكثير من الرموز البرمجية الحالية لنقلها، من الممكن تنفيذ إجراءات في المتصفّح لم تكن قابلة للتطبيق إلى أن ظهرت WebAssembly.
ستوضّح هذه المقالة مثالاً على كيفية استخدام رمز المصدر الحالي لبرنامج ترميز الفيديو بتنسيق AV1 وإنشاء حزمة له وتجريبه داخل المتصفّح، بالإضافة إلى نصائح للمساعدة في إنشاء حزمة اختبار لتصحيح أخطاء الحزمة. يتوفّر رمز المصدر الكامل للمثال هنا على الرابط github.com/GoogleChromeLabs/wasm-av1 للرجوع إليه.
نزِّل أحد هذين الفيديو تجربتَي الملف بمعدل 24 لقطة في الثانية وجرِّبهما على الإصدار التجريبي الذي أنشأناه.
اختيار قاعدة بيانات رموز برمجية مثيرة للاهتمام
على مدار عدة سنوات، لاحظنا أنّ نسبة كبيرة من الزيارات على الويب تتألف من بيانات الفيديو، وتُقدّر شركة Cisco هذه النسبة بنحو% 80. بالطبع، يدرك مورّدو المتصفّحات ومواقع الفيديوهات بشكل كبير الرغبة في تقليل البيانات التي يستهلكها كل هذا المحتوى. ويعتمد ذلك بالطبع على تحسين تقنيات الضغط، وكما هو متوقّع، هناك الكثير من الأبحاث التي تتناول تقنيات الضغط من الجيل التالي بهدف تقليل عبء البيانات الناتج عن إرسال الفيديوهات على الإنترنت.
يعمل تحالف الوسائط المفتوحة على تطوير تنسيق ضغط فيديو من الجيل التالي يُعرف باسم AV1، ويُتوقّع أن يؤدي إلى تصغير حجم بيانات الفيديو بشكل كبير. في المستقبل، نتوقّع أن توفّر المتصفّحات دعمًا أصليًا لتنسيق AV1، ولكن لحسن الحظ، الرمز المصدر للمكبِّر والمفكِّك مفتوح المصدر، ما يجعله مرشحًا مثاليًا لمحاولة تجميعه في WebAssembly لنتمكّن من تجربته في المتصفّح.
تعديل التطبيق لاستخدامه في المتصفّح
من أوّل الإجراءات التي يجب اتّخاذها لإدخال هذا الرمز إلى المتصفّح هو معرفة الرمز الحالي لفهم واجهة برمجة التطبيقات. عند النظر إلى هذا الرمز لأول مرة، يبرز شيئان:
- يتم إنشاء شجرة المصدر باستخدام أداة تُسمى
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
إلى إنشاء بعض
Makefiles
أولاً من خلال تشغيل
cmake
نفسه، ثم تنفيذ الأمر
make
الذي سينفّذ خطوة الترجمة.
يُرجى العِلم أنّه بما أنّنا نستخدم Emscripten، علينا استخدام
مجموعة أدوات مُجمِّع Emscripten بدلاً من المُجمِّع التلقائي للمضيف.
ويتم تحقيق ذلك باستخدام Emscripten.cmake
الذي
هو جزء من Emscripten SDK و
تمرير مساره كمَعلمة إلى cmake
نفسه.
سطر الأوامر أدناه هو ما نستخدمه لإنشاء ملفات Makefile:
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
الذي يحتوي على ملف libaom.a
الذي تم تجميعه وأصبح جاهزًا لإدراجه في مشروعنا.
تصميم واجهة برمجة تطبيقات للتواصل مع المكتبة
بعد إنشاء مكتبتنا، علينا معرفة كيفية التفاعل معها لإرسال بيانات الفيديو المضغوطة إليها ثم إعادة قراءة لقطات الفيديو التي يمكننا عرضها في المتصفّح.
عند إلقاء نظرة داخل شجرة رمز AV1، تكون نقطة البدء الجيدة هي مثال
على وحدة فك ترميز الفيديو التي يمكن العثور عليها في الملف
[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
غير شفاف من
جانب JavaScript، ويعمل فقط على تجميع الواجهة. يُرجى العِلم أنّ
إنشاء واجهة برمجة تطبيقات تتّبع دلالات الملفات عن كثب يسهّل إعادة استخدامها في
العديد من قواعد الرموز البرمجية الأخرى التي يُراد استخدامها من سطر أوامر
(مثل diff وsed وما إلى ذلك).
نحتاج أيضًا إلى تحديد دالة مساعدة تُسمى DS_set_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 في المتصفّح، من المنطقي إنشاء نوع من اختبار الوحدة للواجهة مع الرمز البرمجي الذي نعمل عليه حتى نتمكّن من تصحيح الأخطاء خارج المتصفّح، كما يمكننا أيضًا اختبار الواجهة التي أنشأناها.
في هذا المثال، كنا نستخدِم واجهة برمجة تطبيقات مستندة إلى البث كواجهة ل
مكتبة AV1. لذلك، من المنطقي إنشاء مجموعة اختبارات يمكننا
استخدامها لإنشاء إصدار من واجهة برمجة التطبيقات التي تعمل على سطر الأوامر وتعمل
على نقل البيانات إلى الملفات أو من الملفات بشكلٍ تلقائي من خلال تنفيذ عملية نقل البيانات إلى الملفات أو من الملفات نفسها ضمن
واجهة برمجة التطبيقات DATA_Source
.
رمز إدخال/إخراج البث لوحدة الاختبار بسيط، ويبدو على النحو التالي:
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 صور قبل بدء التشغيل. بعد ذلك، في كل مرة يتم فيها عرض إطار، سنحاول فك ترميز إطار آخر حتى نحافظ على مملأ ملف التخزين المؤقت. يضمن هذا النهج توفّر اللقطات مسبقًا للمساعدة في إيقاف التوقفات المفاجئة في الفيديو.
في المثال البسيط الذي نقدمه، يكون الفيديو المضغوط بأكمله متاحًا للقراءة، لذلك لا حاجة إلى التخزين المؤقت. ومع ذلك، إذا أردنا توسيع واجهة data source للسماح ببث الإدخال من خادم، يجب أن نضع استراتيجية التخزين المؤقت.
التعليمة البرمجية في
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. يُرجى العلم أنّه عند استدعاء هذه الوظيفة من JavaScript، يجب
التأكّد من أنّ الذاكرة التي نكتب الصورة المحوَّلة فيها قد تم تخصيصها
داخل ذاكرة وحدة 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.
- يمكن تنفيذ مهام كثيفة الاستخدام لوحدة المعالجة المركزية، مثل فك ترميز الفيديوهات المتقدّم، من خلال WebAssembly.
ومع ذلك، هناك بعض القيود: يتم تنفيذ جميع الإجراءات على سلسلت الرسائل الرئيسية، ونخلط بين عمليات الرسم وفك ترميز الفيديو على سلسلت الرسائل هذه. يمكن أن يؤدي نقل عملية فك التشفير إلى Web Worker إلى تحسين عملية التشغيل، لأنّ وقت فك تشفير اللقطات يعتمد بشكل كبير على محتوى اللقطة ويمكن أن يستغرق أحيانًا وقتًا أطول من الوقت الذي خططنا له.
يستخدم التحويل إلى WebAssembly إعدادات AV1 لنوع معالج مركزي عام. في حال إجراء عملية الترجمة والنشر مباشرةً على سطر الأوامر لوحدة معالجة مركزية عامة، نلاحظ حملًا مشابهًا لوحدة المعالجة المركزية لفك ترميز الفيديو كما هو الحال مع إصدار WebAssembly، ومع ذلك، تضم مكتبة AV1 لفك الترميز أيضًا عمليات تنفيذ SIMD التي تعمل بمعدل أسرع مما يصل إلى 5 مرات. تعمل مجموعة WebAssembly Community Group حاليًا على توسيع نطاق المعيار ليشمل التعليمات الأساسية لمعالجة البيانات المتسلسلة (SIMD)، وبعد الانتهاء من ذلك، من المفترض أن يؤدي ذلك إلى تسريع عملية فك التشفير بشكل كبير. عند حدوث ذلك، سيكون من الممكن تمامًا فك ترميز فيديو بدقة 4K HD في الوقت الفعلي من خلال برنامج فك ترميز فيديو WebAssembly.
في جميع الأحوال، يكون مثال الرمز البرمجي مفيدًا كدليل للمساعدة في نقل أي أداة حالية لسلسلة الأوامر لتشغيلها كوحدة WebAssembly، كما يعرض ما يمكن فعله على الويب اليوم.
المساهمون
نشكر "جيف بوسنيك" و"إريك بيدلمان" و"توماس شتاينر" على تقديمهم مراجعات وملاحظات قيّمة.