Jitsi
معمارية تطبيقات المصادر المفتوحة (المجلد الأول)Jitsi
معمارية تطبيقات المصادر المفتوحة (المجلد الأول) Jitsi
Emil Ivov
إذا استمتعت بهذه الكتب، فقد يعجبك أيضًا التصميم البرمجي بالأمثلة في بايثون (Software Design by Example in Python) والتصميم البرمجي بالأمثلة في جافاسكربت (Software Design by Example in JavaScript).
Jitsi هو تطبيق يتيح للمستخدمين إجراء مكالمات الفيديو والصوت، ومشاركة سطح المكتب، وتبادل الملفات والرسائل. والأهم من ذلك أنه يتيح لهم ذلك عبر عدد من البروتوكولات المختلفة، بدءًا من البروتوكولات المعيارية XMPP (بروتوكول الرسائل والحضور القابل للتوسّع) وSIP (بروتوكول بدء الجلسة)، وصولًا إلى بروتوكولات خاصة مثل Yahoo! وWindows Live Messenger (MSN). وهو يعمل على Microsoft Windows وApple Mac OS X وLinux وFreeBSD. وهو مكتوب في معظمه بلغة Java، لكنه يتضمّن أيضًا أجزاء مكتوبة بشيفرة أصلية (native). وفي هذا الفصل، سننظر في معمارية Jitsi المبنية على OSGi، وسنرى كيف ينفّذ البروتوكولات ويديرها، ثم سنعود إلى ما تعلّمناه من بنائه.[1]
10.1. تصميم Jitsi
كانت القيود الثلاثة الأهم التي اضطررنا إلى مراعاتها عند تصميم Jitsi (واسمه آنذاك SIP Communicator) هي: دعم متعدد البروتوكولات، والتشغيل عبر المنصات المختلفة (cross-platform)، وسهولة الاستخدام من قِبل المطوّرين.
من منظور المطوّر، يتحوّل تعدّد البروتوكولات إلى الحاجة إلى واجهة موحَّدة لجميع البروتوكولات. وبعبارة أخرى، عندما يرسل المستخدم رسالة، يجب أن تستدعي واجهة المستخدم الرسومية لدينا (GUI) دائمًا الأسلوب sendMessage نفسه، سواء كان البروتوكول المحدَّد حاليًا يستخدم فعلًا أسلوبًا اسمه sendXmppMessage أو sendSipMsg.
وتُسهم كتابة معظم شيفرتنا بلغة Java إلى حدٍّ كبير في تلبية قييدنا الثاني: التشغيل عبر المنصات المختلفة. ومع ذلك، هناك أمور لا تدعمها بيئة تشغيل Java (JRE) أو لا تنفّذها بالطريقة التي نودّها، مثل التقاط الفيديو من كاميرا الويب. لذلك نحتاج إلى استخدام DirectShow على Windows وQTKit على Mac OS X وVideo for Linux 2 على Linux. ومكما في حال البروتوكولات، لا يجوز أن تشغل هذه التفاصيل أذهان أجزاء الشيفرة التي تتحكّم في مكالمات الفيديو (فهي معقّدة بما يكفي أصلًا).
أخيرًا، تعني سهولة الاستخدام من قِبل المطوّرين أنه ينبغي أن يكون من السهل على المطوّرين إضافة مزايا جديدة. فهناك ملايين الأشخاص يستخدمون VoIP اليوم بآلاف الطرق المختلفة، ويأتي مزوّدو الخدمات وبائعو الخوادم بشِرِك استخدام متنوّعة وأفكار حول المزايا الجديدة. وعلينا أن نحرص على أن يكون من السهل عليهم استخدام Jitsi بالطريقة التي يريدونها. وينبغي ألّا يضطر من يحتاج إلى إضافة شيء جديد إلى قراءة الأجزاء التي يعدّلها أو يوسّعها من المشروع وفهمها فحسب. وعلى نحو مماثل، ينبغي ألّا يكون لتعديلات شخص واحد أقل أثر ممكن على أعمال الآخرين.
ولاختصار الأمر، احتجنا إلى بيئة تكون فيها أجزاء الشيفرة المختلفة مستقلة نسبيًا عن بعضها. ويجب أن يكون من الممكن استبدال بعض الأجزاء بسهولة تبعًا لنظام التشغيل، وأن تعمل أجزاء أخرى مثل البروتوكولات على التوازي ومع ذلك تتصرّف بالطريقة نفسها؛ كما يجب أن يكون من الممكن إعادة كتابة أي جزء منها بالكامل بحيث تعمل بقية الشيفرة دون أي تغيير. وأخيرًا، أردنا القدرة على تشغيل هذه الأجزاء وإيقافها بسهولة، وكذلك القدرة على تنزيل إضافات (plugins) من الإنترنت وإضافتها إلى قائمتنا.
وقد فكّرنا لفترة وجيزة في كتابة إطار عمل خاص بنا، لكننا سرعان ما تخلّينا عن هذه الفكرة. كنا متلهّفين لبدء كتابة شيفرة VoIP وIM في أقرب وقت ممكن، وإنفاق شهرين أو ثلاثة على إطار عمل للإضافات لم يكن يبدو مثيرًا للاهتمام. واقترح أحدهم OSGi، وبدا أنه الملاءمة المثالية.
10.2. Jitsi وإطار عمل OSGi
كُتبت كتب كاملة عن OSGi، لذا لن نشرح كل ما يعنيه هذا الإطار. وإنما سنشرح فقط ما يقدّمه لنا وكيف نستخدمه في Jitsi.
وكل ما سبق أي شيء آخر، فإن OSGi يتعلق بالوحدات (modules). إذ تُفصَل مزايا تطبيقات OSGi إلى حزم (bundles). وحزمة OSGi ليست سوى ملف JAR اعتيادي مثل تلك المستخدمة لتوزيع مكتبات Java وتطبيقاتها. وJitsi هو مجموعة من هذه الحزم. فهناك حزمة مسؤولة عن الاتصال بـ Windows Live Messenger، وأخرى تتولّى XMPP، وأخرى أخرى تتعامل مع واجهة المستخدم الرسومية، وهكذا. وتعمل كل هذه الحزم معًا في بيئة يوفّرها — في حالتنا — برنامج Apache Felix، وهو تطبيق مفتوح المصدر لإطار OSGi.
تحتاج كل هذه الوحدات إلى العمل معًا. فحزمة واجهة المستخدم الرسومية تحتاج إلى إرسال الرسائل عبر حزم البروتوكولات، التي تحتاج بدورها إلى تخزينها عبر الحزم التي تتعامل مع سجلّ الرسائل. وهنا تحديدًا يأتي دور خدمات OSGi: فهي تمثّل الجزء من الحزمة الذي يكون مرئيًا لكل الآخرين. وخدمة OSGi هي في الغالب مجموعة من واجهات Java التي تتيح استخدام وظيفة معيّنة مثل التسجيل، أو إرسال الرسائل عبر الشبكة، أو استرجاع قائمة المكالمات الأخيرة. أما الأصناف التي تنفّذ تلك الوظيفة فعليًا فتُعرَف باسم تنفيذ الخدمة (service implementation). ومعظمها يحمل اسم واجهة الخدمة التي ينفّذها، مع اللاحقة "Impl" في نهايته (مثل ConfigurationServiceImpl). ويتيح إطار عمل OSGi للمطوّرين إخفاء تنفيذات الخدمات والتأكد من أنها لا تظهر أبدًا خارج الحزمة التي تقع فيها. وبهذه الطريقة، لا تستطيع الحزم الأخرى استخدامها إلا عبر واجهات الخدمة.
ولمعظم الحزم كذلك منفِّذات (activators). والمنفِّذات واجهات بسيطة تعرِّف أسلوب start وأسلوب stop. وكلما حمَّل Felix حزمة في Jitsi أو أزالها، يستدعي هذين الأسلوبين كي تستعد الحزمة للتشغيل أو للإيقاف. وحين يستدعي Felix هذين الأسلوبين، يمرِّر إليهما مُعاملًا يُسمّى BundleContext. ويمنح BundleContext الحزم وسيلةً للانصال ببيئة OSGi. وبهذه الطريقة تستطيع الحزم اكتشاف أي خدمة OSGi تحتاج إلى استخدامها، أو تسجيل خدمة خاصة بها (الشكل 10.1).

الشكل 10.1: تنشيط حزم OSGi
لنرَ كيف يعمل هذا عمليًا. تخيَّل خدمة تخزّن الخصائص وتسترجعها بصورة دائمة. في Jitsi نسمّي هذا ConfigurationService، وهو يبدو كالتالي:
package net.java.sip.communicator.service.configuration;
public interface ConfigurationService
{
public void setProperty(String propertyName, Object property);
public Object getProperty(String propertyName);
}
وتبدو أبسطُ تنفيذات ConfigurationService كالتالي:
package net.java.sip.communicator.impl.configuration;
import java.util.*;
import net.java.sip.communicator.service.configuration.*;
public class ConfigurationServiceImpl implements ConfigurationService
{
private final Properties properties = new Properties();
public Object getProperty(String name)
{
return properties.get(name);
}
public void setProperty(String name, Object value)
{
properties.setProperty(name, value.toString());
}
}
لاحظ كيف تُعرَّف الخدمة في حزمة net.java.sip.communicator.service، بينما يقع التنفيذ في net.java.sip.communicator.impl. وجميع الخدمات والتنفيذات في Jitsi مفصولة تحت هاتين الحزمتين. ويتيح OSGi للحزم جعل بعض الحزم مرئية خارج ملف JAR الخاص بها فحسب، لذا فإن هذا الفصل يجعل من الأسهل على الحزم أن تقتصر على تصدير حزم خدماتها وإبقاء تنفيذاتها مخفية.
وأخيرًا، الأمر الأخير الذي يلزم كي يبدأ الناس في استخدام تنفيذي هو تسجيله في BundleContext والإشارة إلى أنه يوفّر تنفيذًا للخدمة ConfigurationService. وإليك كيف يحدث ذلك:
package net.java.sip.communicator.impl.configuration;
import org.osgi.framework.*;
import net.java.sip.communicator.service.configuration;
public class ConfigActivator implements BundleActivator
{
public void start(BundleContext bc) throws Exception
{
bc.registerService(ConfigurationService.class.getName(), // service name
new ConfigurationServiceImpl(), // service implementation
null);
}
}
وبمجرد تسجيل الصنف ConfigurationServiceImpl في BundleContext، تستطيع الحزم الأخرى استخدامه. وإليك مثالًا يوضّح كيف تستطيع حزمة عشوائية استخدام خدمة الإعدادات لدينا:
package net.java.sip.communicator.plugin.randombundle;
import org.osgi.framework.*;
import net.java.sip.communicator.service.configuration.*;
public class RandomBundleActivator implements BundleActivator
{
public void start(BundleContext bc) throws Exception
{
ServiceReference cRef = bc.getServiceReference(
ConfigurationService.class.getName());
configService = (ConfigurationService) bc.getService(cRef);
// And that's all! We have a reference to the service implementation
// and we are ready to start saving properties:
configService.setProperty("propertyName", "propertyValue");
}
}
ولاحظ مرة أخرى الحزمة. فنحن نضع في net.java.sip.communicator.plugin الحزم التي تستخدم خدمات عرّفها آخرون دون أن تصدّر أي خدمة خاصة بها أو تنفّذ أيًّا منها. وتُعدّ نماذج الإعداد (configuration forms) مثالًا جيدًا على هذه الإضافات: فهي إضافات إلى واجهة المستخدم في Jitsi تتيح للمستخدمين ضبط جوانب معيّنة من التطبيق. وعندما يغيّر المستخدمون تفضيلاتهم، تتفاعل نماذج الإعداد مع ConfigurationService أو مع الحزم المسؤولة عن ميزة ما مباشرة. غير أن أيًّا من الحزم الأخرى لا يحتاج أبدًا إلى التفاعل معها بأي شكل من الأشكال (الشكل 10.2).

الشكل 10.2: بنية الخدمات
10.3. بناء الحزمة وتشغيلها
الآن وقد رأينا كيفية كتابة الشيفرة داخل حزمة، حان وقت الحديث عن التغليف (packaging). وعند التشغيل، تحتاج جميع الحزم إلى الإشارة إلى ثلاثة أمور مختلفة لبيئة OSGi: حزم Java التي تتيحها للآخرين (أي الحزم المصدَّرة export)، وتلك التي تودّ استخدامها من الآخرين (أي الحزم المستوردة import)، واسم صنف BundleActivator الخاص بها. وتقوم الحزم بذلك عبر بيان (manifest) ملف JAR الذي ستُنشر داخله.
وبالنسبة إلى الخدمة ConfigurationService التي عرّفناها أعلاه، فيمكن أن يبدو ملف البيان كالتالي:
Bundle-Activator: net.java.sip.communicator.impl.configuration.ConfigActivator
Bundle-Name: Configuration Service Implementation
Bundle-Description: A bundle that offers configuration utilities
Bundle-Vendor: jitsi.org
Bundle-Version: 0.0.1
System-Bundle: yes
Import-Package: org.osgi.framework,
Export-Package: net.java.sip.communicator.service.configuration
وبعد إنشاء بيان JAR، أصبحنا جاهزين لإنشاء الحزمة نفسها. وفي Jitsi نستخدم Apache Ant للتعامل مع جميع مهام البناء. ولإضافة حزمة إلى عملية بناء Jitsi، عليك تعديل ملف build.xml الموجود في الجذر الرئيسي للمشروع. وتُنشأ ملفات JAR الخاصة بالحزم في آخر ملف build.xml، ضمن أهداف تُسمّى bundle-xxx. ولبناء خدمة الإعدادات لدينا نحتاج إلى الآتي:
<target name="bundle-configuration">
<jar destfile="${bundles.dest}/configuration.jar" manifest=
"${src}/net/java/sip/communicator/impl/configuration/conf.manifest.mf" >
<zipfileset dir="${dest}/net/java/sip/communicator/service/configuration"
prefix="net/java/sip/communicator/service/configuration"/>
<zipfileset dir="${dest}/net/java/sip/communicator/impl/configuration"
prefix="net/java/sip/communicator/impl/configuration" />
</jar>
</target>
وكما ترى، فإن هدف Ant ينشئ ملف JAR باستخدام بيان الإعدادات لدينا، ويضيف إليه حزم الإعدادات من تسلسلَي service وimpl. ولم يبقَ سوى أن نجعل Felix يحمّله.
لقد ذكرنا بالفعل أن Jitsi ليس سوى مجموعة من حزم OSGi. وعندما ينفّذ المستخدم التطبيق، فإنه في الواقع يشغّل Felix مع قائمة بالحزم التي يحتاج إلى تحميلها. ويمكنك العثور على تلك القائمة في مجلد lib لدينا، داخل ملف اسمه felix.client.run.properties. ويبدأ Felix الحزم بالترتيب الذي تحدده مستويات البدء (start levels): فجميع الحزم المستوية في مستوى معيّن مضمون أن تكتمل قبل أن تبدأ الحزم في المستويات التالية بالتحميل. ورغم أنه لا يمكنك رؤية ذلك في الشيفرة التجريبية أعلاه، فإن خدمة الإعدادات لدينا تخزّن الخصائص في ملفات، لذا تحتاج إلى استخدام FileAccessService لدينا، الموجودة داخل ملف fileaccess.jar. ولذلك سنحرص على أن يبدأ ConfigurationService بعد FileAccessService:
⋮ ⋮ ⋮
felix.auto.start.30= \
reference:file:sc-bundles/fileaccess.jar
felix.auto.start.40= \
reference:file:sc-bundles/configuration.jar \
reference:file:sc-bundles/jmdnslib.jar \
reference:file:sc-bundles/provdisc.jar \
⋮ ⋮ ⋮
وإن نظرت إلى ملف felix.client.run.properties فستجد في بدايته قائمة بالحزم:
org.osgi.framework.system.packages.extra= \
apple.awt; \
com.apple.cocoa.application; \
com.apple.cocoa.foundation; \
com.apple.eawt; \
⋮ ⋮ ⋮
وتخبر هذه القائمة Felix بالحزم التي يحتاج إلى إتاحتها للحزم من مسار الصنف الظاهر في النظام (classpath). وهذا يعني أن الحزم المدرجة في هذه القائمة يمكن استيرادها بواسطة الحزم (أي إضافتها إلى ترويسة Import-Package في بيانها) دون أن تصدّرها أي حزمة أخرى. وتحتوي القائمة غالبًا على حزم قادمة من أجزاء خاصة بنظام التشغيل ضمن JRE، ونادرًا ما يحتاج مطوّرو Jitsi إلى إضافة حزم جديدة إليها؛ ففي معظم الحالات توفّرها الحزم.
10.4. خدمة مزوّد البروتوكول
تعرّف ProtocolProviderService في Jitsi الطريقة التي تتصرَّف بها جميع تطبيقات البروتوكولات. وهي الواجهة التي تستخدمها الحزم الأخرى (مثل واجهة المستخدم) عندما تحتاج إلى إرسال الرسائل واستقبالها، وإجراء المكالمات، ومشاركة الملفات عبر الشبكات التي يتصل بها Jitsi.
ويمكن العثور على جميع واجهات خدمة البروتوكول تحت الحزمة net.java.sip.communicator.service.protocol. وهناك تطبيقات متعددة لهذه الخدمة، تطبيق واحد لكل بروتوكول مدعوم، وجميعها مخزَّنة في net.java.sip.communicator.impl.protocol.protocol_name.
لنبدأ بمجلد service.protocol. وأبرز ما فيه هو واجهة ProtocolProviderService. فكلما احتج أحدهم إلى إنجاز مهمة متعلقة ببروتوكول ما، وجب عليه أن يبحث عن تنفيذ لتلك الخدمة في BundleContext. وتتيح الخدمة وتنفيذاتها لـ Jitsi الاتصال بأيٍّ من الشبكات المدعومة، واسترجاع حالة الاتصال وتفاصيله، والأهم من ذلك الحصول على مراجع إلى الأصناف التي تنفّذ مهام الاتصال الفعلية مثل المحادثة وإجراء المكالمات.
10.4.1. مجموعات العمليات
كما ذكرنا سابقًا، تحتاج ProtocolProviderService إلى استثمار البروتوكولات الاتصالاتية المختلفة واختلافاتها. وفي حين يكون هذا الأمر بسيطًا تمامًا للميزات التي تتشارك فيها جميع البروتوكولات، مثل إرسال رسالة، إلا أن الأمور تصبح أعقد في المهام التي لا تدعمها سوى بعض البروتوكولات. وأحيانًا تأتي هذه الاختلافات من الخدمة نفسها: فمعظم خدمات SIP الموجودة لا تدعم قوائم جهات الاتصال المخزَّنة على الخادم، في حين أن هذه ميزة مدعومة إلى حدٍّ معقول في جميع البروتوكولات الأخرى. وتُعدّ MSN وAIM مثالًا آخر جيدًا: فقد كان في وقت ما لاتقريب أيٌّ منهما إمكانية إرسال رسائل إلى المستخدمين غير المتصلين، بينما كان ذلك متاحًا لدى الجميع الآخر. (وقد تغيّر ذلك منذ ذلك الحين.)
والخلاصة أن ProtocolProviderService لدينا يحتاج إلى وسيلة للتعامل مع هذه الاختلافات بحيث تتصرّف الحزم الأخرى، مثل واجهة المستخدم، تبعًا لها؛ فلا فائدة من إضافة زر مكالمة إلى جهة اتصال على AIM إذا لم تكن هناك وسيلة لإجراء المكالمة فعلًا.
وهنا تأتي مجموعات العمليات (OperationSets) لإنقاذنا (الشكل 10.3). ومن غير المستغرب أنها مجموعات عمليات، وتوفّر الواجهة التي تستخدمها حزم Jitsi للتحكّم في تطبيقات البروتوكولات. فجميع الأساليب التي تجدها في واجهة مجموعة عمليات مرتبطة بميزة معيّنة. فـ OperationSetBasicInstantMessaging مثلًا تضم أساليب لإنشاء الرسائل الفورية وإرسالها، ولتسجيل المستمعين الذين يتيحون لـ Jitsi استرجاع الرسائل التي يستقبلها. ومثال آخر هو OperationSetPresence الذي يضم أساليب للاستعلام عن حالة جهات الاتصال في قائمتك وضبط حالة لنفسك. وعندما تحدّث واجهة المستخدم الحالة التي تعرضها لجهة اتصال، أو ترسل رسالة إلى جهة اتصال، فإنها تستطيع أولًا أن تسأل المزوّد المقابل عمّا إذا كان يدعم الحضور والمراسلة. والأساليب التي تعرّفها ProtocolProviderService لهذا الغرض هي:
public Map<String, OperationSet> getSupportedOperationSets();
public <T extends OperationSet> T getOperationSet(Class<T> opsetClass);
وينبغي أن تُصمَّم مجموعات العمليات بحيث يكون من غير المرجّح أن يدعم بروتوكول جديد نضيفه سوى بعض العمليات المعرَّفة في مجموعة العمليات فحسب. فبعض البروتوكولات، مثلًا، لا تدعم قوائم جهات الاتصال المخزَّنة على الخادم رغم أنها تسمح للمستخدمين بالاستعلام عن حالة بعضهم البعض. وعليه، بدلًا من دمج إدارة الحضور واسترجاع قائمة الأصدقاء في OperationSetPresence، عرّفنا أيضًا OperationSetPersistentPresence المستخدمة فقط مع البروتوكولات التي تستطيع تخزين جهات الاتصال عبر الإنترنت. ومن جهة أخرى، لم نلتقَ حتى الآن بأي بروتوكول يسمح فقط بإرسال الرسائل دون استقبال أي منها، ولهذا فإن أمورًا مثل إرسال الرسائل واستقبالها يمكن دمجها بأمان.

الشكل 10.3: مجموعات العمليات
10.4.2. الحسابات والمصانع ونُسخ المزوّد
من الخصائص المهمة لـ ProtocolProviderService أن النسخة الواحدة تقابل حساب بروتوكول واحد. لذا، في أي لحظة لدى عدد من تطبيقات الخدمة في BundleContext مساوٍ لعدد الحسابات التي سجّلها المستخدم.
قد تتساءل في هذه المرحلة عمّن ينشئ مزوّدات البروتوكولات ويسجّلها. وهناك كيانان مختلفان مشاركان في ذلك. أولهما ProtocolProviderFactory، وهي الخدمة التي تتيح للحزم الأخرى إنشاء نسخ من المزوّدات ثم تسجيلها كخدمات. وهناك مصنع واحد لكل بروتوكول، وكل مصنع مسؤول عن إنشاء المزوّدات لذلك البروتوكول بعينه. وتُخزَّن تنفيذات المصانع مع بقية تفاصيل البروتوكولات الداخلية. فبالنسبة إلى SIP مثلًا، لدينا net.java.sip.communicator.impl.protocol.sip.ProtocolProviderFactorySipImpl.
أما الكيان الثاني الذي يشارك في إنشاء الحسابات فهو معالج البروتوكول (protocol wizard). وبخلاف المصانع، تُفصَل المعالجات (wizards) عن بقية تنفيذ البروتوكول لأنها تتضمّن واجهة المستخدم الرسومية. فالمعالج الذي يتيح للمستخدمين إنشاء حسابات SIP، مثلًا، موجود في net.java.sip.communicator.plugin.sipaccregwizz.
10.5. خدمة الوسائط
عند العمل مع الاتصالات الفورية عبر الإنترنت، هناك أمر واحد مهم ينبغي فهمه: بروتوكولات مثل SIP وXMPP، رغم أن كثيرًا ما يُعرفها بأنها أكثر بروتوكولات VoIP شيوعًا، ليست البروتوكولات التي تنقل الصوت والفيديو فعليًا عبر الإنترنت. فهذا التكليف تتولّاه بروتوكول النقل الفوري (Real-time Transport Protocol، ويُختصر RTP). وSIP وXMPP مسؤوليتان فقط عن تحضير كل ما يحتاجه RTP، مثل تحديد العنوان الذي يجب إرسال حزم RTP إليه والتفاوض على الصيغة التي يجب ترميز الصوت والفيديو بها (أي المُرمِّز-الفاكِّك codec)، وغيرها. كما تتولّيان أمورًا مثل تحديد مواقع المستخدمين، والحفاظ على حالتهم، وجعل الهواتف ترنّ، والكثير غير ذلك. ولهذا يُشار إلى بروتوكولات مثل SIP وXMPP غالبًا بأنها بروتوكولات إشارات (signalling protocols).
وماذا يعني هذا في سياق Jitsi؟ حسنًا، أولًا يعني أنك لن تجد أي شيفرة تتعامل مع تدفّقات الصوت أو الفيديو في حزمتَي jitsi sip أو jabber. فهذه النوعية من الشيفرة تعيش في MediaService لدينا. وتقع MediaService وتنفيذها في net.java.sip.communicator.service.neomedia وnet.java.sip.communicator.impl.neomedia.
ولماذا "neomedia"؟
يشير الجزء "neo" في اسم حزمة neomedia إلى أنها تحلّ محل حزمة مماثلة استخدمناها في الأصل واضطررنا إلى إعادة كتابتها بالكامل. وفي الواقع، هكذا توصّلنا إلى إحدى قواعدنا الإرشادية: من النادر جدًا أن يكون من المجدي قضاء وقت طويل في تصميم تطبيق بحيث يكون آمنًا أمام المستقبل بنسبة 100%. فهناك ببساطة لا سبيل إلى أخذ كل شيء في الحسبان، لذا ستضطر إلى إجراء تعديلات لاحقًا على أي حال. وإلى جانب ذلك، من المرجح جدًا أن تُدخل مرحلة تصميم دقيقة تعقيدات لن تحتاج إليها أبدًا لأن السيناريوهات التي جهّزتَ لها لن تحدث.
إلى جانب MediaService نفسها، هناك واجهتان أخريان مهمتان بشكل خاص: MediaDevice وMediaStream.
10.5.1. الالتقاط والبث والتشغيل
تمثّل MediaDevices أجهزة الالتقاط والتشغيل التي نستخدمها أثناء المكالمة (الشكل 10.4). فالميكروفون والسماعات، وسماعة الرأس، وكاميرا الويب، كلها أمثلة على هذه الأجهزة، لكنها ليست الأمثلة الوحيدة. فبثّ سطح المكتب ومشاركة المكالمات في Jitsi يلتقطان الفيديو من سطح مكتبك، في حين تستخدم المكالمة الجماعية جهاز AudioMixer من أجل مزج الصوت الذي نستقبله من المشاركين النشطين. وفي جميع الحالات، لا تمثّل MediaDevices سوى MediaType واحدة. أي أنها إما صوتية أو فيديو، وليست كليهما أبدًا. وهذا يعني، على سبيل المثال، إذا كانت لديك كاميرا ويب بميكروفون مدمج، فإن Jitsi يراها جهازين: أحدهما يلتقط الفيديو فقط، والآخر يلتقط الصوت فقط.
غير أن الأجهزة وحدها لا تكفي لإجراء مكالمة هاتفية أو مكالمة فيديو. فإلى جانب تشغيل الوسائط والتقاطها، يجب أيضًا القدرة على إرسالها عبر الشبكة. وهنا يأتي دور MediaStreams. وواجهة MediaStream هي ما يصل MediaDevice بمحدّثك. فهي تمثّل الحزم الواردة والصادرة التي تتبادلها معه خلال المكالمة.
ومكما في حالة الأجهزة، يمكن أن يكون التيار مسؤولًا عن MediaType واحدة فقط. وهذا يعني أنه في حالة مكالمة صوت/فيديو، يقوم Jitsi بإنشاء تدفّقَي وسائط منفصلين ثم يربط كلًا منهما بجهاز الوسائط الصوتي أو المرئي المقابل.

الشكل 10.4: تدفّقات وسائط لأجهزة مختلفة
10.5.2. المُرمِّزات-الفاكِّكات (Codecs)
مفهوم آخر مهم في بثّ الوسائط هو MediaFormats، المعروف أيضًا بالمُرمِّزات-الفاكِّكات (codecs). فبحسب الافتراض، تسمح معظم أنظمة التشغيل بالتقاط الصوت بصيغة PCM بتردد 48 كيلوهرتز أو ما شابه. وهذا ما نشار إليه غالبًا بـ«الصوت الخام»، وهو نوع الصوت الذي تحصل عليه في ملفات WAV: جودة رائعة وحجم هائل. ومن غير العملي إطلاقًا محاولة نقل الصوت عبر الإنترنت بصيغة PCM.
وهذا هو ما وُجدت المُرمِّزات-الفاكِّكات من أجله: فهي تتيح لك تمثيل ونقل الصوت أو الفيديو بطرق متنوعة. فبعض مُرمِّزات الصوت مثل iLBC أو Speex بتردد 8 كيلوهرتز أو G.729، متطلباتها من عرض النطاق منخفضة لكن صوتها مكبوح إلى حدٍّ ما. أما أُخرى مثل Speex عريض النطاق وG.722 فتمنحك جودة صوت رائعة لكنها تتطلب عرض نطاق أكبر. وهناك مُرمِّزات-فاكِّكات تحاول تسليم جودة جيدة مع إبقاء متطلبات عرض النطاق عند مستوى معقول. وH.264، وهو مُرمِّز الفيديو الرائج، مثال جيد على ذلك. والمقايضة هنا تكمن في مقدار الحساب المطلوب أثناء التحويل. فإذا استخدمت Jitsi في مكالمة فيديو بصيغة H.264، فسترى صورة ذات جودة جيدة وستكون متطلبات عرض النطاق لديك معقولة إلى حدٍّ كبير، لكن وحدة المعالجة المركزية (CPU) سيعمل بأقصى طاقتها.
وككل ما سبق تبسيط مُبالَغ فيه، لكن الفكرة هي أن اختيار المُرمِّز-الفاكِّك يدور حول المفاضلة في كل الأحوال. فإما أن تضحّى بعرض النطاق، أو بالجودة، أو بحمل المعالج، أو بأي مزيج من ذلك. ونادرًا ما يحتاج العاملون مع VoIP إلى معرفة المزيد عن المُرمِّزات-الفاكِّكات.
10.5.3. الاتصال بمزوِّدي البروتوكول
جميع البروتوكولات في Jitsi التي تدعم الصوت والفيديو حاليًا تستخدم خدمات الوسائط لدينا بالطريقة نفسها تمامًا. فأولًا تسأل MediaService عن الأجهزة المتاحة على النظام:
public List<MediaDevice> getDevices(MediaType mediaType, MediaUseCase useCase);
ويبيّن MediaType ما إذا كنا مهتمين بأجهزة الصوت أم بأجهزة الفيديو. أما مُعامل MediaUseCase فلا يُؤخذ به بالعين الاعتبار حاليًا إلا في حالة أجهزة الفيديو. فهو يخبر خدمة الوسائط ما إذا كنا نودّ الحصول على أجهزة يمكن استخدامها في مكالمة عادية (MediaUseCase.CALL) — وعندئذ تُعيد قائمةً بكاميرات الويب المتاحة — أم في جلسة مشاركة سطح المكتب (MediaUseCase.DESKTOP) — وعندئذ تُعيد مراجع إلى أسطح مكتب المستخدم.
وتتمثل الخطوة التالية في الحصول على قائمة الصيغ المتاحة لجهاز معيّن. ونفعل ذلك عبر الأسلوب MediaDevice.getSupportedFormats:
public List<MediaFormat> getSupportedFormats();
وبمجرد حصولها على هذه القائمة، يرسلها تنفيذ البروتوكول إلى الطرف البعيد، الذي يردّ عليها بمجموعة فرعية منها ليبيّن أيّها يدعم. وتُعرف هذه التبادلة أيضًا بنموذج العرض/الرد (Offer/Answer Model)، وهي غالبًا ما تستخدم بروتوكول وصف الجلسة (Session Description Protocol) أو شكلًا منه.
وبعد تبادل الصيغ وبعض أرقام المنافذ وعناوين بروتوكول الإنترنت، تُنشئ بروتوكولات VoIP تدفّقات الوسائط وتهيّئها وتبدأها. وبكلمات عامة، يتم هذا التهيئة على النحو التالي:
// first create a stream connector telling the media service what sockets
// to use when transport media with RTP and flow control and statistics
// messages with RTCP
StreamConnector connector = new DefaultStreamConnector(rtpSocket, rtcpSocket);
MediaStream stream = mediaService.createMediaStream(connector, device, control);
// A MediaStreamTarget indicates the address and ports where our
// interlocutor is expecting media. Different VoIP protocols have their
// own ways of exchanging this information
stream.setTarget(target);
// The MediaDirection parameter tells the stream whether it is going to be
// incoming, outgoing or both
stream.setDirection(direction);
// Then we set the stream format. We use the one that came
// first in the list returned in the session negotiation answer.
stream.setFormat(format);
// Finally, we are ready to actually start grabbing media from our
// media device and streaming it over the Internet
stream.start();
والآن تستطيع أن تلوّح لكاميرا الويب، وتُمسك الميكروفون، وتقول: "Hello world!"
10.6. خدمة واجهة المستخدم
حتى الآن غطَّينا أجزاء Jitsi التي تتعامل مع البروتوكولات، وإرسال الرسائل واستقبالها، وإجراء المكالمات. لكن الأمر الأهم على الإطلاق هو أن Jitsi تطبيق يستخدمه أشخاص حقيقيون، ومن ثم فإن واجهة المستخدم إحدى أهم جوانبه. وفي معظم الوقت تستخدم واجهة المستخدم الخدمات التي تكشفها جميع الحزم الأخرى في Jitsi. غير أن هناك بعض الحالات التي يجري فيها الأمر بصورة معكوسة.
الإضافات (plugins) هي أول مثال يخطر بالبال. فالإضافات في Jitsi تحتاج في كثير من الأحيان إلى التفاعل مع المستخدم. وهذا يعني أنها مطالَبة بفتح نوافذ ولوحات موجودة في واجهة المستخدم وإغلاقها أو تحريكها أو إضافة مكوّنات إليها. وهنا يأتي دور UIService لدينا. فهي تتيح تحكّمًا أساسيًا في النافذة الرئيسية في Jitsi، ومن ثم فإن أيقوناتنا في Mac OS X dock وفي منطقة إشعارات Windows هي ما يتيح للمستخدمين التحكّم في التطبيق.
وإلى جانب مجرد التعامل مع قائمة جهات الاتصال، يمكن للإضافات أيضًا توسيعها. والإضافة التي تنفّذ دعم تشفير المحادثة (OTR) في Jitsi مثال جيد على ذلك. فتحتاج حزمة OTR لدينا إلى تسجيل عدة مكوّنات لواجهة المستخدم في أجزاء مختلفة منها. فهي تضيف زر قفل (padlock) في نافذة المحادثة وقسمًا فرعيًا في قائمة النقر بالزر الأيمن لكل جهات الاتصال.
والخبر الجيد أنه يمكنها فعل كل ذلك بضع نداءات من الأساليب فقط. فمُنشِّط OSGi الخاص بحزمة OTR، وهو OtrActivator، يحتوي على الأسطر التالية:
Hashtable<String, String> filter = new Hashtable<String, String>();
// Register the right-click menu item.
filter(Container.CONTAINER_ID,
Container.CONTAINER_CONTACT_RIGHT_BUTTON_MENU.getID());
bundleContext.registerService(PluginComponent.class.getName(),
new OtrMetaContactMenu(Container.CONTAINER_CONTACT_RIGHT_BUTTON_MENU),
filter);
// Register the chat window menu bar item.
filter.put(Container.CONTAINER_ID,
Container.CONTAINER_CHAT_MENU_BAR.getID());
bundleContext.registerService(PluginComponent.class.getName(),
new OtrMetaContactMenu(Container.CONTAINER_CHAT_MENU_BAR),
filter);
وكما ترى، فإن إضافة مكوّنات إلى واجهة المستخدم الرسومية لدينا تنحصر ببساطة في تسجيل خدمات OSGi. وفي الجهة الأخرى من السياج، يبحث تنفيذ UIService لدينا عن تنفيذات لواجهة PluginComponent التي يخصها. فكلما اكتشف تسجيل تنفيذ جديد، يحصل على مرجع إليه ويضيفه إلى الحاوية المحدَّدة في مُرشِّح خدمة OSGi.
وهكذا يحدث الأمر في حالة عنصر قائمة النقر بالزر الأيمن. فداخل حزمة واجهة المستخدم، الصنف الذي يمثّل قائمة النقر بالزر الأيمن، وهو MetaContactRightButtonMenu، يحتوي على الأسطر التالية:
// Search for plugin components registered through the OSGI bundle context.
ServiceReference[] serRefs = null;
String osgiFilter = "("
+ Container.CONTAINER_ID
+ "="+Container.CONTAINER_CONTACT_RIGHT_BUTTON_MENU.getID()+")";
serRefs = GuiActivator.bundleContext.getServiceReferences(
PluginComponent.class.getName(),
osgiFilter);
// Go through all the plugins we found and add them to the menu.
for (int i = 0; i < serRefs.length; i ++)
{
PluginComponent component = (PluginComponent) GuiActivator
.bundleContext.getService(serRefs[i]);
component.setCurrentContact(metaContact);
if (component.getComponent() == null)
continue;
this.add((Component)component.getComponent());
}
وهذا كل ما في الأمر. فمعظم النوافذ التي تراها داخل Jitsi تفعل الشيء نفسه بالضبط: فهي تتصفّح سياق الحزم بحثًا عن خدمات تنفّذ واجهة PluginComponent ولها مُرشِّح يشير إلى رغبتها في إضافتها إلى الحاوية المقابلة. والإضافات أشبه بمسافرين يحملون لافتات تحمل أسماء وجهاتهم، وتجعل نوافذ Jitsi هي السائقين الذين يلتقطونهم.
10.7. الدروس المستفادة
عندما بدأنا العمل على SIP Communicator، كان أحد أكثر الانتقادات أو الأسئلة شيوعًا التي سمعناها هو: «لماذا تستخدمون Java؟ ألا تعرفون أنها بطيئة؟ لن تتمكنوا أبدًا من الحصول على جودة مقبولة لمكالمات الصوت/الفيديو!» ولم تُكرَّر خرافة «Java بطيئة» من قِبل المستخدمين المحتملين بوصفها سببًا للتمسك بـ Skype بدلًا من تجربة Jitsi. لكن أول درس تعلّمناه من عملنا على المشروع هو أن الكفاءة لم تكن أقل مصدر للقلق مع Java مما كانت ستكون عليه مع C++ أو البدائل الأصلية الأخرى.
ولن ندّعي أن قرار اختيار Java جاء نتيجة تحليل صارم لجميع الخيارات الممكنة. كنا نريد ببساطة وسيلة سهلة لبناء شيء يعمل على Windows وLinux، وبدا لنا أن Java وJava Media Framework يوفّران وسيلة واحدة سهلة نسبيًا للقيام بذلك.
وعلى مدى السنوات، لم لدينا أسباب كثيرة للندم على هذا القرار. بل العكس تمامًا: فعلى الرغم من أنها لا تجعله شفافًا تمامًا، تساعد Java على قابلية النقل، و90% من شيفرة SIP Communicator لا تتغيّر من نظام تشغيل إلى آخر. وهذا يشمل جميع تطبيقات مكدّس البروتوكولات (مثل SIP وXMPP وRTP وغيرها) التي هي معقّدة بما يكفي في ذاتها. وقد ثبت أن عدم الحاجة إلى القلق بشأن خصوصيات أنظمة التشغيل في هذه الأجزاء من الشيفرة مفيد إلى حدٍّ كبير.
وفضلًا عن ذلك، تبيّن أن شعبية Java كانت ذات أهمية بالغة عند بناء مجتمعنا. فالمساهمون مورد نادر في حد ذاته. ويحتاج الناس إلى أن تعجبهم طبيعة التطبيق، وأن يجدوا وقتًا ودافعًا — وكل ذلك صعب الجمع. ولذلك فإن عدم إلزامهم بتعلّم لغة جديدة يمثّل ميزة.
وبخلاف معظم التوقّعات، نادرًا ما كان افتراض بطء Java سببًا للاتجاه إلى الحل الأصلي (native). فمعظم الوقت كانت القرارات باستخدام اللغات الأصلية مدفوعة بالتكامل مع نظام التشغيل وبمدى ما يتيحه Java من وصول إلى أدوات خاصة بنظام التشغيل. وفيما يلي ناقشنا الأهم ثلاث مناطق قلَّ فيها أداء Java عن المستويى المقبول.
10.7.1. Java Sound مقابل PortAudio
تُعدّ Java Sound واجهة برمجة Java الافتراضية لالتقاط الصوت وتشغيله. وهي جزء من بيئة التشغيل، ولهذا فهي تعمل على جميع المنصات التي تأتي بها الآلة الافتراضية لجافا. وخلال سنواتها الأولى تحت اسم SIP Communicator، كان Jitsi يستخدم JavaSound حصريًا، وهذا قدّم لنا عددًا لا بأس به من المضايقات.
أولًا وقبل كل شيء، لم تكن الواجهة البرمجية تتيح لنا خيار اختيار أي جهاز صوتي نريد استخدامه. وهذه مشكلة كبيرة. فحين يستخدم المستخدمون حواسيبهم لإجراء مكالمات الصوت والفيديو، فإنهم يستخدمون غالبًا سماعات رأس USB متقدّمة أو أجهزة صوتية أخرى للحصول على أفضل جودة ممكنة. وعندما تتوفّر أجهزة متعددة على الحاسوب، يوجّه JavaSound كل الصوت عبر الجهاز الذي يعتبره نظام التشغيل هو الجهاز الافتراضي، وهذا ليس كافيًا في حالات كثيرة. فكثير من المستخدمين يفضّلون إبقاء بقية التطبيقات تعمل على بطاقتهم الصوتية الافتراضية كي يظل بإمكانهم مثلًا الاستماع إلى الموسيقى عبر مكبّرات الصوت. والأهم من ذلك هو أنه في كثير من الحالات يكون من الأفضل أن يرسل SIP Communicator إشعارات صوتية إلى جهاز واحد وصوت المكالمة الفعلية إلى جهاز آخر، مما يتيح للمستخدم سماع تنبيه المكالمة الواردة عبر مكبّرات الصوت حتى لو لم يكن أمام الحاسوب، ثم بعد التقاط المكالمة يبدأ في استخدام سماعة الرأس.
ولا شيء من هذا ممكن مع Java Sound. وفوق ذلك، تستخدم النسخة التنفيذية على Linux نظام OSS المهجور في معظم توزيعات Linux اليوم.
قررنا استخدام نظام صوتي بديل. ولم نرغب في المساس بطبيعتنا متعددة المنصات، وإن أمكن، كنا نودّ تجنّب الحاجة إلى معالجته بأنفسنا. وهنا وفّرت لنا PortAudio[2] فائدة كبيرة.
وحين لا تسمح لك Java بشيء ما، فإن المشاريع مفتوحة المصدر متعددة المنصات هي الخيار التالي الأفضل. وقد أتاح لنا الانتقال إلى PortAudio تنفيذ دعم للعرض والالتقاط الصوتيين بإعداد تفصيلي دقيق، تمامًا كما وصفنا أعلاه. وهو أيضًا يعمل على Windows وLinux وMac OS X وFreeBSD وغيرها ممّا لم يتوفّر لنا الوقت لتوفير حزم له.
10.7.2. التقاط الفيديو وعرضه
الفيديو لا يقل أهميةً عن الصوت بالنسبة إلينا. لكن يبدو أن هذا لم يكن الحال بالنسبة إلى مطوّري Java، إذ لا توجد في JRE واجهة برمجية افتراضية تتيح التقاط الفيديو أو عرضه. لفترة من الوقت بدا Java Media Framework وكأنه مصمَّم ليصبح تلك الواجهة، إلى أن توقفت Sun عن صيانته.
طبيعيًا، بدأنا نبحث عن بديل للفيديو على طراز PortAudio، لكننا لم نكن محظوظين هذه المرة. في البداية قررنا الاعتماد على إطار العمل LTI-CIVIL من Ken Larson[3]. وهذا مشروع رائع واستخدمناه لفترة لا بأس بها[4]. غير أنه تبيّن أنه دون الأمثل عند استخدامه في سياق الاتصالات الفورية.
وعليه توصّلنا إلى أن الطريقة الوحيدة لتوفير اتصالات فيديو لا تشوبها شائبة في Jitsi هي أن ننفّذ بأنفسنا أدوات الالتقاط والعرض الأصلية. ولم يكن هذا قرارًا سهلًا لأنه يعني إضافة قدر كبير من التعقيد وحِمل صيانة جسيم إلى المشروع، لكننا ببساطة لم يكن لدينا خيار آخر: كنا نريد حقًا أن تكون لدينا مكالمات فيديو عالية الجودة. والآن أصبح لدينا ذلك!
وتستخدم أدواتنا الأصلية للالتقاط والعرض مباشرةً Video4Linux 2 وQTKit وDirectShow/Direct3D على Linux وMac OS X وWindows على التوالي.
10.7.3. ترميز الفيديو وفكّ ترميزه
دعم SIP Communicator — وبالتالي Jitsi — مكالمات الفيديو منذ أيامه الأولى. وذلك لأن Java Media Framework سمح بترميز الفيديو باستخدام مُرمِّز-فاكِّك H.263 وصيغة 176×144 (CIF). ومن يعرفون شكل H.263 CIF هم على الأرجح يبتسمون الآن؛ فقليلون منا سيستخدمون تطبيق دردشة فيديو اليوم لو كان كل ما لديه على هذا النحو.
ولتقديم جودة مقبولة، اضطررنا إلى استخدام مكتبات أخرى مثل FFmpeg. وترميز الفيديو في الواقع إحدى الحالات القليلة التي تُظهر فيها Java حدودها من ناحية الأداء. ويصدق الأمر نفسه على اللغات الأخرى أيضًا، كما يدل على ذلك أن مطوّري FFmpeg يستخدمون فعلًا Assembler في عدد من المواضع للتعامل مع الفيديو بأكفأ طريقة ممكنة.
10.7.4. أمور أخرى
هناك عدد من المواضع الأخرى التي قررنا فيها الحاجة إلى الانتقال إلى الحل الأصلي للحصول على نتائج أفضل. فإشعارات شريط النظام عبر Growl على Mac OS X وlibnotify على Linux مثال على ذلك. وتشمل أمورًا أخرى الاستعلام عن قواعد بيانات جهات الاتصال في Microsoft Outlook وApple Address Book، وتحديد عنوان بروتوكول الإنترنت المصدر تبعًا للوجهة، واستخدام تطبيقات مُرمِّز-فاكِّك القائمة لـ Speex وG.722، والتقاط لقطات شاشة لسطح المكتب، وتحويل المحارف إلى رموز مفاتيح.
والأمر المهم هو أنه كلما احتجنا إلى اختيار حل أصلي، كنا قادرين على ذلك، وقد فعلنا. وهنا نصل إلى نقطتنا: فمنذ بدأنا Jitsi، أصلحنا أجزاءَ منه أو أضفناها أو حتى أعدنا كتابة أجزاء كاملة منه، لأننا أردنا أن تبدو أو تُحَسّ أو تعمل على نحو أفضل. غير أننا لم نُدَمْدِم قطعًا أيًّا من الأمور التي لم ننجح في إتقانها من أول محاولة وحينما تساءلنا، ببساطة اخترنا أحد الخيارات المتاحة وسِرنا معه. كان بإمكاننا أن ننتظر حتى نعرف بصورة أفضل ما نقوم به، لكن لو فعلنا ذلك لما وُجد Jitsi اليوم.
10.8. شكر وتقدير
شكر خاص لـ Yana Stamcheva على إنشاء جميع المخططات في هذا الفصل.
الحواشي
- للإشارة إلى المصدر وأنت تقرأ، نزِّله من
http://jitsi.org/source. وإذا كنت تستخدم Eclipse أو NetBeans، فيمكنك الانتقال إلىhttp://jitsi.org/eclipseأوhttp://jitsi.org/netbeansللحصول على إرشادات كيفية ضبطهما. http://portaudio.com/http://lti-civil.org/- ما زالت لدينا في الواقع كخيار غير افتراضي.
للإشارة إلى المصدر وأنت تقرأ، نزِّله من
http://jitsi.org/source. وإذا كنت تستخدم Eclipse أو NetBeans، فيمكنك الانتقال إلىhttp://jitsi.org/eclipseأوhttp://jitsi.org/netbeansللحصول على إرشادات كيفية ضبطهما. ↩︎http://portaudio.com/↩︎http://lti-civil.org/↩︎ما زالت لدينا في الواقع كخيار غير افتراضي. ↩︎