يتيح لنا 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
الذي يحتوي على رمز فك ترميز الفيديو المجمّع والجاهز لدمجنا في مشروعنا.
تصميم واجهة برمجة تطبيقات للتواصل مع المكتبة
بعد إنشاء مكتبتنا، علينا معرفة كيفية التفاعل معها لإرسال بيانات الفيديو المضغوطة إليها ثم إعادة قراءة لقطات الفيديو التي يمكننا عرضها في المتصفّح.
عند إلقاء نظرة داخل شجرة رمز 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، كما يعرض ما يمكن فعله على الويب اليوم.
المساهمون
نشكر "جيف بوسنيك" و"إريك بيدلمان" و"توماس شتاينر" على تقديمهم مراجعات وملاحظات قيّمة.