جارٍ تنزيل الوسائط

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

فئات لتنزيل الوسائط تشير اتجاهات الأسهم إلى تدفّق البيانات.

  • DownloadService: يغلّف DownloadManager ويعيد توجيه الأوامر إليه. تتيح الخدمة استمرار تشغيل DownloadManager حتى عندما يكون التطبيق يعمل في الخلفية.
  • DownloadManager: تدير عمليات تنزيل متعددة، وتحمّل حالاتها (وتخزّنها) من DownloadIndex وإليه، وتبدأ عمليات التنزيل وتوقفها استنادًا إلى متطلبات مثل الاتصال بالشبكة وما إلى ذلك. لتنزيل المحتوى، سيقرأ المدير عادةً البيانات التي يتم تنزيلها من HttpDataSource، ويكتبها في Cache.
  • DownloadIndex: يحتفظ بحالات عمليات التنزيل.

إنشاء DownloadService

لإنشاء DownloadService، عليك إنشاء فئة فرعية وتنفيذ الطرق المجردة الخاصة بها:

  • getDownloadManager(): تعرض هذه السمة DownloadManager المطلوب استخدامه.
  • getScheduler(): تعرض هذه السمة Scheduler اختيارية، ويمكنها إعادة تشغيل الخدمة عند استيفاء المتطلبات اللازمة لإكمال عمليات التنزيل المعلقة. يوفر ExoPlayer عمليات التنفيذ التالية:
    • PlatformScheduler، التي تستخدم JobScheduler (الحد الأدنى لمستوى واجهة برمجة التطبيقات هو 21). راجِع مستندات PlatformScheduler Javadoc لمعرفة متطلبات أذونات التطبيق.
    • WorkManagerScheduler، الذي يستخدم WorkManager.
  • getForegroundNotification(): تعرض إشعارًا سيتم عرضه عندما تكون الخدمة قيد التشغيل في المقدّمة. يمكنك استخدام DownloadNotificationHelper.buildProgressNotification لإنشاء إشعار بالنمط التلقائي.

أخيرًا، حدِّد الخدمة في ملف AndroidManifest.xml:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<application>
  <service android:name="com.myapp.MyDownloadService"
      android:exported="false"
      android:foregroundServiceType="dataSync">
    <!-- This is needed for Scheduler -->
    <intent-filter>
      <action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
      <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
  </service>
</application>

يمكنك الاطّلاع على DemoDownloadService وAndroidManifest.xml في تطبيق ExoPlayer التجريبي للحصول على مثال ملموس.

إنشاء DownloadManager

يوضّح مقتطف الرمز التالي كيفية إنشاء مثيل من DownloadManager، والذي يمكن أن تعرضه الدالة getDownloadManager() في DownloadService:

Kotlin

// Note: This should be a singleton in your app.
val databaseProvider = StandaloneDatabaseProvider(context)

// A download cache should not evict media, so should use a NoopCacheEvictor.
val downloadCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), databaseProvider)

// Create a factory for reading the data from the network.
val dataSourceFactory = DefaultHttpDataSource.Factory()

// Choose an executor for downloading data. Using Runnable::run will cause each download task to
// download data on its own thread. Passing an executor that uses multiple threads will speed up
// download tasks that can be split into smaller parts for parallel execution. Applications that
// already have an executor for background downloads may wish to reuse their existing executor.
val downloadExecutor = Executor(Runnable::run)

// Create the download manager.
val downloadManager =
  DownloadManager(context, databaseProvider, downloadCache, dataSourceFactory, downloadExecutor)

// Optionally, properties can be assigned to configure the download manager.
downloadManager.requirements = requirements
downloadManager.maxParallelDownloads = 3

Java

// Note: This should be a singleton in your app.
databaseProvider = new StandaloneDatabaseProvider(context);

// A download cache should not evict media, so should use a NoopCacheEvictor.
downloadCache = new SimpleCache(downloadDirectory, new NoOpCacheEvictor(), databaseProvider);

// Create a factory for reading the data from the network.
dataSourceFactory = new DefaultHttpDataSource.Factory();

// Choose an executor for downloading data. Using Runnable::run will cause each download task to
// download data on its own thread. Passing an executor that uses multiple threads will speed up
// download tasks that can be split into smaller parts for parallel execution. Applications that
// already have an executor for background downloads may wish to reuse their existing executor.
Executor downloadExecutor = Runnable::run;

// Create the download manager.
downloadManager =
    new DownloadManager(
        context, databaseProvider, downloadCache, dataSourceFactory, downloadExecutor);

// Optionally, setters can be called to configure the download manager.
downloadManager.setRequirements(requirements);
downloadManager.setMaxParallelDownloads(3);

يمكنك الاطّلاع على DemoUtil في التطبيق التجريبي للحصول على مثال ملموس.

إضافة عملية تنزيل

لإضافة تنزيل، أنشئ DownloadRequest وأرسِله إلى DownloadService. بالنسبة إلى ساحات المشاركة التكيُّفية، استخدِم DownloadHelper للمساعدة في إنشاء DownloadRequest. يوضّح المثال التالي كيفية إنشاء طلب تنزيل:

Kotlin

val downloadRequest = DownloadRequest.Builder(contentId, contentUri).build()

Java

DownloadRequest downloadRequest = new DownloadRequest.Builder(contentId, contentUri).build();

في هذا المثال، contentId هو معرّف فريد للمحتوى. في الحالات البسيطة، يمكن غالبًا استخدام contentUri كـ contentId، ولكن يمكن للتطبيقات استخدام أي نظام تعريف يناسب حالة الاستخدام. يحتوي DownloadRequest.Builder أيضًا على بعض أدوات الضبط الاختيارية. على سبيل المثال، يمكن استخدام setKeySetId وsetData لضبط إدارة الحقوق الرقمية والبيانات المخصّصة التي يريد التطبيق ربطها بعملية التنزيل، على التوالي. يمكن أيضًا تحديد نوع MIME للمحتوى باستخدام setMimeType، كإشارة للحالات التي لا يمكن فيها استنتاج نوع المحتوى من contentUri.

بعد إنشاء الطلب، يمكن إرساله إلى DownloadService لإضافة عملية التنزيل باتّباع الخطوات التالية:

Kotlin

DownloadService.sendAddDownload(
  context,
  MyDownloadService::class.java,
  downloadRequest,
  /* foreground= */ false
)

Java

DownloadService.sendAddDownload(
    context, MyDownloadService.class, downloadRequest, /* foreground= */ false);

في هذا المثال، MyDownloadService هو فئة فرعية من DownloadService في التطبيق، وتتحكّم المَعلمة foreground في ما إذا كان سيتم بدء الخدمة في المقدّمة. إذا كان تطبيقك يعمل في المقدّمة، يجب عادةً ضبط المَعلمة foreground على false لأنّ DownloadService سيضع نفسه في المقدّمة إذا تبيّن له أنّ لديه عملًا يجب تنفيذه.

تجري إزالة المحتوى الذي تم تنزيله

يمكن إزالة عملية تنزيل من خلال إرسال أمر إزالة إلى DownloadService، حيث يحدّد contentId عملية التنزيل المطلوب إزالتها:

Kotlin

DownloadService.sendRemoveDownload(
  context,
  MyDownloadService::class.java,
  contentId,
  /* foreground= */ false
)

Java

DownloadService.sendRemoveDownload(
    context, MyDownloadService.class, contentId, /* foreground= */ false);

يمكنك أيضًا إزالة جميع البيانات التي تم تنزيلها باستخدام DownloadService.sendRemoveAllDownloads.

بدء عمليات التنزيل وإيقافها

لن تتقدّم عملية التنزيل إلا في حال استيفاء أربعة شروط:

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

يمكن التحكّم في كل هذه الشروط من خلال إرسال أوامر إلى DownloadService.

ضبط أسباب إيقاف التنزيل وإزالتها

يمكنك تحديد سبب لإيقاف عملية تنزيل واحدة أو جميع عمليات التنزيل:

Kotlin

// Set the stop reason for a single download.
DownloadService.sendSetStopReason(
  context,
  MyDownloadService::class.java,
  contentId,
  stopReason,
  /* foreground= */ false
)

// Clear the stop reason for a single download.
DownloadService.sendSetStopReason(
  context,
  MyDownloadService::class.java,
  contentId,
  Download.STOP_REASON_NONE,
  /* foreground= */ false
)

Java

// Set the stop reason for a single download.
DownloadService.sendSetStopReason(
    context, MyDownloadService.class, contentId, stopReason, /* foreground= */ false);

// Clear the stop reason for a single download.
DownloadService.sendSetStopReason(
    context,
    MyDownloadService.class,
    contentId,
    Download.STOP_REASON_NONE,
    /* foreground= */ false);

يمكن أن تكون قيمة stopReason أي قيمة غير صفرية (Download.STOP_REASON_NONE = 0 هي قيمة خاصة تعني أنّه لم يتم إيقاف التنزيل). يمكن للتطبيقات التي تتضمّن أسبابًا متعدّدة لإيقاف عمليات التنزيل استخدام قيم مختلفة لتتبُّع سبب إيقاف كل عملية تنزيل. يتم ضبط سبب الإيقاف وإزالته لجميع عمليات التنزيل بالطريقة نفسها التي يتم بها ضبط سبب الإيقاف وإزالته لعملية تنزيل واحدة، باستثناء أنّه يجب ضبط contentId على null.

عندما يكون لعملية التنزيل سبب إيقاف غير صفري، ستكون في الحالة Download.STATE_STOPPED. يتم الاحتفاظ بأسباب الإيقاف في DownloadIndex، وبالتالي يتم الاحتفاظ بها إذا تم إيقاف عملية التطبيق وإعادة تشغيلها لاحقًا.

إيقاف جميع عمليات التنزيل مؤقتًا واستئنافها

يمكن إيقاف جميع عمليات التنزيل مؤقتًا واستئنافها على النحو التالي:

Kotlin

// Pause all downloads.
DownloadService.sendPauseDownloads(
  context,
  MyDownloadService::class.java,
  /* foreground= */ false
)

// Resume all downloads.
DownloadService.sendResumeDownloads(
  context,
  MyDownloadService::class.java,
  /* foreground= */ false
)

Java

// Pause all downloads.
DownloadService.sendPauseDownloads(context, MyDownloadService.class, /* foreground= */ false);

// Resume all downloads.
DownloadService.sendResumeDownloads(context, MyDownloadService.class, /* foreground= */ false);

عندما يتم إيقاف عمليات التنزيل مؤقتًا، ستكون في الحالة Download.STATE_QUEUED. على عكس تحديد أسباب التوقّف، لا يحتفظ هذا الأسلوب بأي تغييرات في الحالة. ولا يؤثر ذلك سوى في حالة وقت التشغيل DownloadManager.

وضع متطلبات لإحراز تقدّم في عملية التنزيل

يمكن استخدام Requirements لتحديد القيود التي يجب استيفاؤها لمتابعة عمليات التنزيل. يمكن ضبط المتطلبات عن طريق استدعاء DownloadManager.setRequirements() عند إنشاء DownloadManager، كما هو موضّح في المثال أعلاه. يمكن أيضًا تغييرها بشكل ديناميكي من خلال إرسال أمر إلى DownloadService:

Kotlin

// Set the download requirements.
DownloadService.sendSetRequirements(
  context, MyDownloadService::class.java, requirements, /* foreground= */ false)

Java

// Set the download requirements.
DownloadService.sendSetRequirements(
  context,
  MyDownloadService.class,
  requirements,
  /* foreground= */ false);

عندما يتعذّر إكمال عملية التنزيل بسبب عدم استيفاء المتطلبات، ستكون الحالة Download.STATE_QUEUED. يمكنك طلب البحث عن المتطلبات غير المستوفاة باستخدام DownloadManager.getNotMetRequirements().

ضبط الحد الأقصى لعدد عمليات التنزيل المتوازية

يمكن ضبط الحد الأقصى لعدد عمليات التنزيل المتوازية من خلال استدعاء DownloadManager.setMaxParallelDownloads(). يتم ذلك عادةً عند إنشاء DownloadManager، كما هو موضّح في المثال أعلاه.

عندما يتعذّر إكمال عملية تنزيل لأنّ الحد الأقصى لعدد عمليات التنزيل المتوازية قد تم بلوغه، ستكون الحالة Download.STATE_QUEUED.

الاستعلام عن المحتوى الذي تم تنزيله

يمكن طلب حالة جميع عمليات التنزيل، بما في ذلك العمليات التي اكتملت أو تعذّر إكمالها، من خلال DownloadIndex DownloadManager. يمكن الحصول على DownloadIndex من خلال الاتصال بالرقم DownloadManager.getDownloadIndex(). يمكن بعد ذلك الحصول على مؤشر يتكرّر على جميع عمليات التنزيل من خلال استدعاء DownloadIndex.getDownloads(). بدلاً من ذلك، يمكن الاستعلام عن حالة عملية تنزيل واحدة من خلال الاتصال بـ DownloadIndex.getDownload().

توفّر السمة DownloadManager أيضًا السمة DownloadManager.getCurrentDownloads() التي تعرض حالة عمليات التنزيل الحالية فقط (أي التي لم تكتمل أو تفشل). تكون هذه الطريقة مفيدة لتعديل الإشعارات ومكوّنات واجهة المستخدم الأخرى التي تعرض مستوى التقدّم وحالة عمليات التنزيل الحالية.

الاستماع إلى المحتوى الذي نزّلته

يمكنك إضافة مستمع إلى DownloadManager ليتم إعلامك عند تغيير حالة عمليات التنزيل الحالية:

Kotlin

downloadManager.addListener(
  object : DownloadManager.Listener { // Override methods of interest here.
  }
)

Java

downloadManager.addListener(
    new DownloadManager.Listener() {
      // Override methods of interest here.
    });

راجِع DownloadManagerListener في فئة DownloadTracker لتطبيق العرض التوضيحي للاطّلاع على مثال ملموس.

تشغيل المحتوى الذي تم تنزيله

تشبه عملية تشغيل المحتوى الذي تم تنزيله عملية تشغيل المحتوى على الإنترنت، إلا أنّ البيانات تتم قراءتها من Cache الذي تم تنزيله بدلاً من قراءتها عبر الشبكة.

لتشغيل المحتوى الذي تم تنزيله، أنشئ مثيلاً من CacheDataSource.Factory باستخدام مثيل Cache نفسه الذي تم استخدامه للتنزيل، وأدرِجه في DefaultMediaSourceFactory عند إنشاء المشغّل:

Kotlin

// Create a read-only cache data source factory using the download cache.
val cacheDataSourceFactory: DataSource.Factory =
  CacheDataSource.Factory()
    .setCache(downloadCache)
    .setUpstreamDataSourceFactory(httpDataSourceFactory)
    .setCacheWriteDataSinkFactory(null) // Disable writing.

val player =
  ExoPlayer.Builder(context)
    .setMediaSourceFactory(
      DefaultMediaSourceFactory(context).setDataSourceFactory(cacheDataSourceFactory)
    )
    .build()

Java

// Create a read-only cache data source factory using the download cache.
DataSource.Factory cacheDataSourceFactory =
    new CacheDataSource.Factory()
        .setCache(downloadCache)
        .setUpstreamDataSourceFactory(httpDataSourceFactory)
        .setCacheWriteDataSinkFactory(null); // Disable writing.

ExoPlayer player =
    new ExoPlayer.Builder(context)
        .setMediaSourceFactory(
            new DefaultMediaSourceFactory(context).setDataSourceFactory(cacheDataSourceFactory))
        .build();

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

بعد إعداد المشغّل باستخدام CacheDataSource.Factory، سيتمكّن من الوصول إلى المحتوى الذي تم تنزيله لتشغيله. بعد ذلك، يصبح تشغيل المحتوى الذي تم تنزيله بسيطًا مثل تمرير MediaItem المقابل إلى المشغّل. يمكن الحصول على MediaItem من Download باستخدام Download.request.toMediaItem، أو مباشرةً من DownloadRequest باستخدام DownloadRequest.toMediaItem.

إعداد MediaSource

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

Kotlin

val mediaSource =
  ProgressiveMediaSource.Factory(cacheDataSourceFactory)
    .createMediaSource(MediaItem.fromUri(contentUri))
player.setMediaSource(mediaSource)
player.prepare()

Java

ProgressiveMediaSource mediaSource =
    new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
        .createMediaSource(MediaItem.fromUri(contentUri));
player.setMediaSource(mediaSource);
player.prepare();

تنزيل أحداث البث التكيُّفي وتشغيلها

تحتوي مجموعات البث التكيّفي (مثل DASH وSmoothStreaming وHLS) عادةً على مسارات وسائط متعدّدة. في كثير من الأحيان، تتوفّر عدة مقاطع صوتية تتضمّن المحتوى نفسه بجودات مختلفة (مثل مقاطع الفيديو بدقة عادية ودقة عالية ودقة 4K). قد تتضمّن أيضًا عدة مقاطع من النوع نفسه تحتوي على محتوى مختلف (مثل مقاطع صوتية متعددة بلغات مختلفة).

بالنسبة لعمليات تشغيل البث، يمكن استخدام محدد المسار لاختيار المسارات التي سيتم تشغيلها. وبالمثل، بالنسبة للتنزيل، يمكن استخدام DownloadHelper لاختيار المسارات التي يتم تنزيلها. يتضمّن الاستخدام النموذجي DownloadHelper الخطوات التالية:

  1. أنشئ DownloadHelper باستخدام مثيل DownloadHelper.Factory. قم بإعداد المساعد وانتظر معاودة الاتصال.

    Kotlin

    val downloadHelper =
         DownloadHelper.Factory()
          .setRenderersFactory(DefaultRenderersFactory(context))
          .setDataSourceFactory(dataSourceFactory)
          .create(MediaItem.fromUri(contentUri))
    downloadHelper.prepare(callback)

    Java

    DownloadHelper downloadHelper =
       new DownloadHelper.Factory()
            .setRenderersFactory(new DefaultRenderersFactory(context))
            .setDataSourceFactory(dataSourceFactory)
            .create(MediaItem.fromUri(contentUri));
    downloadHelper.prepare(callback);
  2. اختياريًا، يمكنك فحص المسارات المحددة الافتراضية باستخدام getMappedTrackInfo وgetTrackSelections، وإجراء التعديلات باستخدام clearTrackSelections وreplaceTrackSelections وaddTrackSelection.
  3. قم بإنشاء DownloadRequest للمسارات المحددة عن طريق استدعاء getDownloadRequest. يمكن تمرير الطلب إلى DownloadService الخاص بك لإضافة التنزيل، كما هو موضح أعلاه.
  4. حرِّر المساعد باستخدام release().

يتطلب تشغيل المحتوى التكيفي الذي تم تنزيله تكوين المشغل وتمرير MediaItem المقابل، كما هو موضح أعلاه.

عند إنشاء MediaItem، يجب ضبط MediaItem.localConfiguration.streamKeys ليتطابق مع تلك الموجودة في DownloadRequest حتى يحاول المشغل تشغيل مجموعة فرعية من المسارات التي تم تنزيلها فقط. سيؤدي استخدام Download.request.toMediaItem وDownloadRequest.toMediaItem لبناء MediaItem إلى الاهتمام بهذا الأمر نيابةً عنك.