أخبار المنتجات

الارتقاء بتشغيل الوسائط: نظرة متعمّقة على PreloadManager في Media3 - الجزء 2

مدّة القراءة: 9 دقائق
عرض الملف الشخصي لـ Mayuri Khinvasara Khabya
Mayuri Khinvasara Khabya مهندسة علاقات المطوّرين

مرحبًا بك في الجزء الثاني من سلسلة المكوّنة من ثلاثة أجزاء حول التحميل المسبق للوسائط باستخدام Media3. تم تصميم هذه السلسلة لإرشادك خلال عملية إنشاء تجارب وسائط سريعة الاستجابة ومنخفضة وقت الاستجابة في تطبيقات Android.

  • الجزء 1: تقديم ميزة "التحميل المسبق" باستخدام Media3، وقد غطّى هذا الجزء الأساسيات. استكشفنا الفرق بين PreloadConfiguration لقوائم التشغيل البسيطة وDefaultPreloadManager الأكثر فعالية لواجهات المستخدم الديناميكية. تعرّفت على كيفية تنفيذ دورة حياة واجهة برمجة التطبيقات الأساسية: إضافة الوسائط باستخدام add()، واسترداد MediaSource مُعدّة باستخدام getMediaSource()، وإدارة الأولويات باستخدام setCurrentPlayingIndex() وinvalidate()، وإزالة الموارد باستخدام remove() وrelease().
  • الجزء 2 (هذه المشاركة): في هذه المدونة، نستكشف الإمكانات المتقدّمة في DefaultPreloadManager. نتناول كيفية الحصول على إحصاءات باستخدام PreloadManagerListener، وتنفيذ أفضل الممارسات الجاهزة للاستخدام في الإنتاج، مثل مشاركة المكوّنات الأساسية مع ExoPlayer، وإتقان نمط النافذة المنزلقة لإدارة الذاكرة بفعالية.
  • الجزء 3: سيتناول الجزء الأخير من هذه السلسلة عملية دمج PreloadManager مع ذاكرة التخزين المؤقت للقرص المستمرة، ما يتيح لك تقليل استهلاك البيانات باستخدام إدارة الموارد وتقديم تجربة سلسة.

إذا كنت جديدًا على ميزة "التحميل المسبق" في Media3، ننصحك بشدة بقراءة الجزء 1 قبل المتابعة. بالنسبة إلى المستخدمين المستعدين للانتقال إلى ما هو أبعد من الأساسيات، لنستكشف كيفية الارتقاء بتنفيذ تشغيل الوسائط.

الاستماع: استرداد الإحصاءات باستخدام PreloadManagerListener

عندما تريد إطلاق ميزة في مرحلة الإنتاج، بصفتك مطوّر تطبيقات، تريد أيضًا فهم الإحصاءات التي تقف وراءها وتسجيلها. كيف يمكنك التأكّد من أنّ استراتيجية التحميل المسبق فعّالة في بيئة العالم الحقيقي؟ تتطلب الإجابة عن هذا السؤال بيانات عن معدّلات النجاح والإخفاق والأداء. تُعد واجهة PreloadManagerListener الآلية الأساسية لجمع هذه البيانات.

توفّر واجهة PreloadManagerListener ردّي استدعاء أساسيين يقدّمان إحصاءات مهمة عن عملية التحميل المسبق وحالتها.

  • onCompleted(MediaItem mediaItem): يتم استدعاء ردّ الاستدعاء هذا عند إكمال طلب التحميل المسبق بنجاح، كما هو محدّد في TargetPreloadStatusControl.
  • onError(PreloadException error): قد يكون ردّ الاستدعاء هذا مفيدًا لتصحيح الأخطاء والمراقبة. يتم استدعاؤه عند فشل عملية التحميل المسبق، ما يوفّر الاستثناء المرتبط بها.

يمكنك تسجيل متتبِّع من خلال طلب إجراء واحد كما هو موضّح في نموذج الرمز البرمجي التالي:

val preloadManagerListener = object : PreloadManagerListener {
    override fun onCompleted(mediaItem: MediaItem) {
        // Log success for analytics. 
        Log.d("PreloadAnalytics", "Preload completed for $mediaItem")
    }

    override fun onError( preloadError: PreloadException) {
        // Log the specific error for debugging and monitoring.
        Log.e("PreloadAnalytics", "Preload error ", preloadError)
    }
}

preloadManager.addListener(preloadManagerListener)

استخراج الإحصاءات من المستمع

يمكن ربط ردود الاستدعاء الخاصة بالمستمع بخط أنابيب الإحصاءات. من خلال إعادة توجيه هذه الأحداث إلى محرّك الإحصاءات، يمكنك الإجابة عن أسئلة رئيسية مثل:

  • ما هو معدّل نجاح التحميل المسبق؟ (نسبة أحداث onCompleted إلى إجمالي محاولات التحميل المسبق)
  • ما هي شبكات توصيل المحتوى أو تنسيقات الفيديو التي تسجّل أعلى معدّلات الخطأ؟ (من خلال تحليل الاستثناءات من onError)
  • ما هو معدّل الخطأ في التحميل المسبق؟ (نسبة أحداث onError إلى إجمالي محاولات التحميل المسبق)

يمكن أن تمنحك هذه البيانات ملاحظات كمية عن استراتيجية التحميل المسبق، ما يتيح لك إجراء الاختبارات الثنائية وتحسين تجربة المستخدم استنادًا إلى البيانات. يمكن أن تساعدك هذه البيانات أيضًا في تعديل مدة التحميل المسبق وعدد الفيديوهات التي تريد تحميلها مسبقًا والمخازن المؤقتة التي تخصّصها بذكاء.

ما وراء تصحيح الأخطاء: استخدام onError لتوفير حل احتياطي سلس لواجهة المستخدم

يشير فشل عملية التحميل المسبق بقوة إلى حدث تخزين مؤقت قادم للمستخدم. يسمح لك ردّ الاستدعاء onError بالرد بشكل تفاعلي. بدلاً من مجرد تسجيل الخطأ، يمكنك تعديل واجهة المستخدم. على سبيل المثال، إذا تعذّر تحميل الفيديو القادم مسبقًا، يمكن لتطبيقك إيقاف التشغيل التلقائي للتمريرة التالية، ما يتطلب من المستخدم النقر لبدء التشغيل.

بالإضافة إلى ذلك، من خلال فحص نوع PreloadException، يمكنك تحديد استراتيجية إعادة محاولة أكثر ذكاءً. يمكن لتطبيق اختيار إزالة مصدر فاشل على الفور من المدير استنادًا إلى رسالة الخطأ أو رمز حالة HTTP. يجب إزالة العنصر من واجهة المستخدم وفقًا لذلك حتى لا تتسرّب مشاكل التحميل إلى تجربة المستخدم. يمكنك أيضًا الحصول على بيانات أكثر تفصيلاً من PreloadException، مثل HttpDataSourceException، للبحث أكثر في الأخطاء. مزيد من المعلومات عن تحديد المشاكل في ExoPlayer وحلّها

نظام الرفيق: لماذا من الضروري مشاركة المكوّنات مع ExoPlayer؟

تم تصميم DefaultPreloadManager وExoPlayer للعمل معًا. لضمان الاستقرار والكفاءة، يجب أن يشاركا عدة مكوّنات أساسية components. إذا كانا يعملان باستخدام مكوّنات منفصلة وغير منسّقة، فقد يؤثر ذلك في أمان الخيط وقابلية استخدام المقاطع الصوتية المحمّلة مسبقًا على المشغّل، لأننا بحاجة إلى التأكّد من تشغيل المقاطع الصوتية المحمّلة مسبقًا على المشغّل الصحيح. يمكن أن تتنافس المكوّنات المنفصلة أيضًا على موارد محدودة، مثل معدّل نقل البيانات على الشبكة والذاكرة، ما قد يؤدي إلى تدهور الأداء. يُعد التعامل مع الإزالة المناسبة جزءًا مهمًا من دورة الحياة، والترتيب المقترَح للإزالة هو إزالة PreloadManager أولاً، ثم ExoPlayer.

تم تصميم DefaultPreloadManager.Builder لتسهيل هذه المشاركة ويتضمّن واجهات برمجة تطبيقات لـ إنشاء كل من PreloadManager ومثيل مشغّل مرتبط. لنطّلع على سبب ضرورة مشاركة مكوّنات مثل BandwidthMeter وLoadControl وTrackSelector ومشغّل الرسائل. يمكنك الاطّلاع على التمثيل المرئي لكيفية تفاعل هذه المكوّنات مع تشغيل ExoPlayer.

preloadManager2.png

منع تعارضات معدّل نقل البيانات باستخدام BandwidthMeter مشترك

تقدّم BandwidthMeter تقديرًا لمعدّل نقل البيانات المتاح على الشبكة استنادًا إلى معدّلات النقل السابقة. إذا كان كل من PreloadManager والمشغّل يستخدمان مثيلَين منفصلَين، لن يكونا على علم بنشاط الشبكة الخاص بكل منهما، ما قد يؤدي إلى سيناريوهات الفشل. على سبيل المثال، لنفترض أنّ مستخدمًا يشاهد فيديو، ويتدهور اتصال الشبكة، ويبدأ MediaSource للتحميل المسبق في الوقت نفسه عملية تنزيل مكثّفة لفيديو مستقبلي. سيستهلك نشاط MediaSource للتحميل المسبق معدّل نقل البيانات الذي يحتاجه المشغّل النشط، ما يؤدي إلى توقف الفيديو الحالي. يُعد التوقف أثناء التشغيل فشلاً كبيرًا في تجربة المستخدم.

من خلال مشاركة BandwidthMeter واحد، يمكن لـ TrackSelector اختيار المقاطع الصوتية بأعلى جودة في ظل ظروف الشبكة الحالية وحالة المخزن المؤقت، أثناء التحميل المسبق أو التشغيل. يمكنه بعد ذلك اتخاذ قرارات ذكية لحماية جلسة التشغيل النشطة وضمان تجربة سلسة.

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

ضمان الاتساق باستخدام مكوّنات LoadControl وTrackSelector وRenderer المشتركة في ExoPlayer

  • LoadControl: يحدّد هذا المكوّن سياسة التخزين المؤقت، مثل مقدار البيانات التي يجب تخزينها مؤقتًا قبل بدء التشغيل ومتى يجب بدء تحميل المزيد من البيانات أو إيقافه. تضمن مشاركة LoadControl توجيه استهلاك الذاكرة للمشغّل وPreloadManager من خلال استراتيجية تخزين مؤقت منسّقة واحدة على كل من الوسائط المحمّلة مسبقًا والوسائط التي يتم تشغيلها حاليًا، ما يمنع التنافس على الموارد. عليك تخصيص حجم المخزن المؤقت بذكاء بالتنسيق مع عدد العناصر التي تحمّلها مسبقًا والمدة التي تحمّلها بها، لضمان الاتساق. في أوقات التنافس، سيمنح المشغّل الأولوية لتشغيل العنصر الحالي المعروض على الشاشة. باستخدام LoadControl مشترك، سيواصل مدير التحميل المسبق التحميل المسبق طالما لم يصل عدد وحدات البايت في المخزن المؤقت المستهدَف المخصّص للتحميل المسبق إلى الحدّ الأعلى، ولا ينتظر حتى يتم تحميل المحتوى للتشغيل.

ملاحظة: تضمن مشاركة LoadControl في أحدث إصدار من Media3 (1.8) إمكانية مشاركة Allocator بشكل صحيح مع PreloadManager والمشغّل. إنّ استخدام LoadControl للتحكّم بفعالية في التحميل المسبق هو ميزة ستتوفّر في إصدار Media3 1.9 القادم.

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector: هذا المكوّن مسؤول عن اختيار المقاطع الصوتية (على سبيل المثال، فيديو بدقة معيّنة، أو صوت بلغة معيّنة) التي يجب تحميلها وتشغيلها. تضمن المشاركة أن تكون المقاطع الصوتية التي يتم اختيارها أثناء التحميل المسبق هي نفسها التي سيستخدمها المشغّل. يؤدي ذلك إلى تجنُّب سيناريو غير مجدٍ يتم فيه تحميل مقطع فيديو بدقة 480p مسبقًا، ثم يتجاهله المشغّل على الفور ويسترد مقطعًا بدقة 720p عند التشغيل.< br /> يجب ألا يشارك مدير التحميل المسبق المثيل نفسه من TrackSelector مع المشغّل. بدلاً من ذلك، يجب أن يستخدم كل منهما مثيلاً مختلفًا من TrackSelector ولكن من التنفيذ نفسه. لهذا السبب، نضبط TrackSelectorFactory بدلاً من TrackSelector في DefaultPreloadManager.Builder.

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • Renderer: هذا المكوّن مسؤول عن فهم إمكانات المشغّل بدون إنشاء أدوات العرض الكاملة. يفحص هذا المخطط لمعرفة تنسيقات الفيديو والصوت والنص التي سيدعمها المشغّل النهائي. يسمح له ذلك باختيار وتنزيل مقطع الوسائط المتوافق فقط بذكاء ويمنع إضاعة معدّل نقل البيانات على المحتوى الذي لا يمكن للمشغّل تشغيله فعليًا.

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

مزيد من المعلومات عن مكوّنات Exoplayer

القاعدة الذهبية: استخدام Playback Looper مشترك للتحكّم في كل شيء

يمكن تحديد الخيط الذي يمكن الوصول إلى مثيل ExoPlayer عليه بشكل صريح من خلال تمرير Looper عند إنشاء المشغّل. يمكن طلب البحث عن Looper الخاص بالخيط الذي يجب الوصول إلى المشغّل منه باستخدام Player.getApplicationLooper. من خلال الاحتفاظ بـ Looper مشترك بين المشغّل وPreloadManager، نضمن تسلسل جميع العمليات على عناصر الوسائط المشتركة هذه في قائمة رسائل خيط واحد. يمكن أن يقلّل ذلك من أخطاء التزامن.

يجب أن تحدث جميع التفاعلات بين PreloadManager والمشغّل مع مصادر الوسائط التي سيتم تحميلها أو تحميلها مسبقًا على خيط التشغيل نفسه. تُعد مشاركة Looper أمرًا ضروريًا لضمان أمان الخيط، وبالتالي يجب مشاركة PlaybackLooper بين PreloadManager والمشغّل.

يُعدّ PreloadManager كائن MediaSource له حالة في الخلفية. عندما يستدعي رمز واجهة المستخدم player.setMediaSource(mediaSource)، فإنك تنقل هذا الكائن المعقّد الذي له حالة من MediaSource للتحميل المسبق إلى المشغّل. في هذا السيناريو، يتم نقل PreloadMediaSource بالكامل من المدير إلى المشغّل. يجب أن تحدث جميع هذه التفاعلات وعمليات النقل على PlaybackLooper نفسه.

إذا كان كل من PreloadManager وExoPlayer يعملان على خيطَين مختلفَين، فقد تحدث حالة تنافس. يمكن أن يعدّل خيط PreloadManager الحالة الداخلية لـ MediaSource (على سبيل المثال، كتابة بيانات جديدة في مخزن مؤقت) في اللحظة نفسها التي يحاول فيها خيط المشغّل القراءة منه. يؤدي ذلك إلى سلوك غير متوقّع، وهو IllegalStateException الذي يصعب تصحيح أخطائه.

preloadManagerBuilder.setPreloadLooper(playbackLooper)

لنطّلع على كيفية مشاركة جميع المكوّنات أعلاه بين ExoPlayer وDefaultPreloadManager في الإعداد نفسه.

val preloadManagerBuilder =
DefaultPreloadManager.Builder(context, targetPreloadStatusControl)

// Optional - Share components between ExoPlayer and DefaultPreloadManager
preloadManagerBuilder
     .setBandwidthMeter(customBandwidthMeter)
     .setLoadControl(customLoadControl)
     .setMediaSourceFactory(customMediaSourceFactory)
     .setTrackSelectorFactory(customTrackSelectorFactory)
     .setRenderersFactory(customRenderersFactory)
     .setPreloadLooper(playbackLooper)

val preloadManager = val preloadManagerBuilder.build()

ملاحظة: إذا كنت تستخدم المكوّنات التلقائية في ExoPlayer، مثل DefaultLoadControlوغيرها، فلست بحاجة إلى مشاركتها صراحةً مع DefaultPreloadManager. عند إنشاء مثيل ExoPlayer من خلال buildExoPlayer في DefaultPreloadManager.Builder، تتم الإشارة إلى هذه المكوّنات تلقائيًا مع بعضها البعض، إذا كنت تستخدم عمليات التنفيذ التلقائية مع الإعدادات التلقائية. ولكن إذا كنت تستخدم مكوّنات مخصّصة أو إعدادات مخصّصة، عليك إشعار DefaultPreloadManager بها صراحةً من خلال واجهات برمجة التطبيقات أعلاه.

التحميل المسبق الجاهز للاستخدام في الإنتاج: نمط النافذة المنزلقة

في خلاصة ديناميكية، يمكن للمستخدم الانتقال إلى محتوى غير محدود تقريبًا. إذا كنت تضيف الفيديوهات باستمرار إلى DefaultPreloadManager بدون استراتيجية إزالة مقابلة، فسيؤدي ذلك حتمًا إلى حدوث OutOfMemoryError. يحتفظ كل MediaSource تم تحميله مسبقًا بـ SampleQueue، الذي يخصّص مخازن مؤقتة للذاكرة. عندما تتراكم هذه المخازن المؤقتة، يمكن أن تستنفد مساحة الذاكرة المؤقتة للتطبيق. الحل هو خوارزمية قد تكون مألوفة لديك، تُعرف باسم النافذة المنزلقة. يحافظ نمط النافذة المنزلقة على مجموعة صغيرة وقابلة للإدارة من العناصر في الذاكرة تكون مجاورة منطقيًا للموضع الحالي للمستخدم في الخلاصة. أثناء انتقال المستخدم، تنزلق هذه "النافذة" من العناصر المُدارة معه، وتضيف عناصر جديدة تظهر في العرض، وتزيل أيضًا العناصر البعيدة الآن.

slidingwindow.png

تنفيذ نمط النافذة المنزلقة

من الضروري أن تفهم أنّ PreloadManager لا يوفّر طريقة setWindowSize() مضمّنة. النافذة المنزلقة هي نمط تصميم أنت، أي المطوّر، مسؤول عن تنفيذه باستخدام الطريقتَين الأساسيتَين add() وremove(). يجب أن تربط منطق تطبيقك أحداث واجهة المستخدم، مثل الانتقال أو تغيير الصفحة، باستدعاءات واجهة برمجة التطبيقات هذه. إذا كنت تريد مرجعًا للرمز البرمجي لهذا، فقد نفّذنا نمط النافذة المنزلقة هذا في نموذج socialite الذي يتضمّن أيضًا PreloadManagerWrapper الذي يحاكي نافذة منزلقة.

لا تنسَ إضافة preloadManager.remove(mediaItem) في عملية التنفيذ عندما يصبح من غير المرجّح أن يظهر العنصر قريبًا في عرض المستخدم. إنّ عدم إزالة العناصر التي لم تعد قريبة من المستخدم هو السبب الرئيسي لمشاكل الذاكرة في عمليات تنفيذ التحميل المسبق. يضمن استدعاء remove() إزالة الموارد التي تساعدك في الحفاظ على استخدام الذاكرة في تطبيقك ضمن الحدود وثابتًا.

تعديل استراتيجية التحميل المسبق المصنّفة باستخدام TargetPreloadStatusControl

بعد أن حدّدنا ما يجب تحميله مسبقًا (العناصر في نافذتنا)، يمكننا تطبيق استراتيجية محدّدة جيدًا لمقدار ما يجب تحميله مسبقًا لكل عنصر. لقد رأينا سابقًا كيفية تحقيق هذه الدقة باستخدام إعداد TargetPreloadStatusControl في الجزء 1.

للتذكير، قد يكون احتمال تشغيل عنصر في الموضع +/- 1 أعلى من احتمال تشغيل عنصر في الموضع +/- 4. يمكنك تخصيص المزيد من الموارد (الشبكة ووحدة المعالجة المركزية والذاكرة) للعناصر التي من المرجّح أن يشاهدها المستخدم بعد ذلك. يؤدي ذلك إلى إنشاء استراتيجية "تحميل مسبق" استنادًا إلى القرب، وهو المفتاح لتحقيق التوازن بين التشغيل الفوري والاستخدام الفعال للموارد.

يمكنك استخدام بيانات الإحصاءات من خلال PreloadManagerListener كما هو موضّح في الأقسام السابقة لتحديد استراتيجية مدة التحميل المسبق.

الخاتمة والخطوات التالية

أصبحت الآن لديك المعرفة المتقدّمة لإنشاء خلاصات وسائط سريعة ومستقرة وفعالة من حيث استخدام الموارد باستخدام DefaultPreloadManager في Media3.

لنلخّص الأفكار الرئيسية المستخلصة:

  • استخدِم PreloadManagerListener لجمع إحصاءات الإحصاءات وتنفيذ معالجة قوية للأخطاء.
  • استخدِم دائمًا DefaultPreloadManager.Builder واحدًا لإنشاء كل من مثيلَي المدير والمشغّل لضمان مشاركة المكوّنات المهمة.
  • نفِّذ نمط النافذة المنزلقة من خلال إدارة استدعاءات add() وremove() بشكل نشط لمنع OutOfMemoryError.
  • استخدِم TargetPreloadStatusControl لإنشاء استراتيجية تحميل مسبق ذكية ومقسّمة على مستويات توازن بين الأداء واستهلاك الموارد.

ما هي الخطوة التالية في الجزء 3: التخزين المؤقت باستخدام الوسائط المحمّلة مسبقًا

يوفّر تحميل البيانات مسبقًا في الذاكرة فائدة فورية من حيث الأداء، ولكن قد يكون له جوانب سلبية. بعد إغلاق التطبيق أو إزالة الوسائط المحمّلة مسبقًا من المدير، ستفقد البيانات. لتحقيق مستوى أكثر استمرارًا من التحسين، يمكننا الجمع بين التحميل المسبق والتخزين المؤقت على القرص. هذه الميزة قيد التطوير النشط وستتوفّر قريبًا خلال بضعة أشهر.

هل لديك أي ملاحظات لمشاركتها؟ نحن حريصون على معرفة رأيك.

ترقَّبوا كل ما هو جديد، وابدأوا في تسريع تشغيل الفيديوهات. 🚀

من تأليف:
متابعة القراءة