يتيح لنا 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
الذي
هو جزء من حزمة تطوير البرامج (SDK) من Emscripten
وإرسال مساره كمَعلمة إلى 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
الذي يحتوي على ملف 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.
هناك بعض القيود: يتم تنفيذ جميع الإجراءات على سلسلت الرسائل الرئيسية، ونخلط بين عمليات الرسم وفك ترميز الفيديو على سلسلت الرسائل هذه. قد يساعد إلغاء فك الترميز في مشغّل الويب على توفير تشغيل أكثر سلاسةً لأنّ الوقت اللازم لفك ترميز الإطارات يعتمد بشكلٍ كبير على محتوى ذلك الإطار وقد يستغرق أحيانًا وقتًا أطول من الميزانية المحدّدة.
تستخدم طريقة التجميع في WebAssembly إعدادات AV1 لنوع عام من وحدة المعالجة المركزية. في حال إجراء عملية الترجمة والنشر مباشرةً على سطر الأوامر لوحدة معالجة مركزية عامة، نلاحظ حملًا مشابهًا لوحدة المعالجة المركزية لفك ترميز الفيديو كما هو الحال مع إصدار WebAssembly، ومع ذلك، تضم مكتبة AV1 لفك الترميز أيضًا عمليات تنفيذ SIMD التي تعمل بمعدل أسرع مما يصل إلى 5 مرات. تعمل مجموعة WebAssembly Community Group حاليًا على توسيع نطاق المعيار ليشمل التعليمات الأساسية لمعالجة البيانات المتسلسلة (SIMD)، وبعد الانتهاء من ذلك، من المُفترَض أن يؤدي ذلك إلى تسريع عملية فك التشفير بشكل كبير. عند حدوث ذلك، سيكون من الممكن تمامًا فك ترميز فيديو بدقة 4K HD في الوقت الفعلي من خلال برنامج فك ترميز فيديو WebAssembly.
في جميع الأحوال، يكون مثال الرمز البرمجي مفيدًا كدليل للمساعدة في نقل أي أداة حالية لسلسلة الأوامر لتشغيلها كوحدة WebAssembly، كما يعرض ما يمكن فعله على الويب اليوم.
المساهمون
نشكر "جيف بوسنيك" و"إريك بيدلمان" و"توماس شتاينر" على تقديمهم مراجعات وملاحظات قيّمة.