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

توفّر 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. جهِّز الدالة المساعدة وانتظِر معاودة الاتصال.
  2. يمكنك اختياريًا فحص المقاطع الصوتية المحدّدة تلقائيًا باستخدام getMappedTrackInfo وgetTrackSelections، وإجراء تعديلات باستخدام clearTrackSelections وreplaceTrackSelections وaddTrackSelection.
  3. أنشئ DownloadRequest للمقاطع الصوتية المحدّدة من خلال استدعاء getDownloadRequest. يمكن تمرير الطلب إلى DownloadService لإضافة التنزيل، كما هو موضّح أعلاه.
  4. حرِّر المساعد باستخدام release().

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);

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

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