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

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

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

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

إنشاء DownloadService

لإنشاء DownloadService، ضع فئة فرعية ونفِّذ طرقها التجريدية:

  • getDownloadManager(): عرض DownloadManager المطلوب استخدامه
  • getScheduler(): عرض Scheduler اختياري يمكنه إعادة تشغيل الخدمة عند استيفاء المتطلبات اللازمة لمتابعة عمليات التنزيل في انتظار المراجعة يقدّم ExoPlayer آليات التنفيذ التالية:
    • PlatformScheduler، التي تستخدم Jobscheduler (الحد الأدنى لواجهة برمجة التطبيقات هو 21). اطّلِع على مستندات javadoc الخاصة بـ PlatformScheduler لمعرفة متطلبات أذونات التطبيق.
    • 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 من الأغاني. إنّ الاستخدام النموذجي لـ DownloadHelper يتّبع الخطوات التالية:

  1. أنشِئ DownloadHelper باستخدام إحدى طرق DownloadHelper.forMediaItem. جهِّز تطبيق المساعد وانتظِر حتى يتم معاودة الاتصال.

    Kotlin

    val downloadHelper =
     DownloadHelper.forMediaItem(
       context,
       MediaItem.fromUri(contentUri),
       DefaultRenderersFactory(context),
       dataSourceFactory
     )
    downloadHelper.prepare(callback)
    

    Java

    DownloadHelper downloadHelper =
       DownloadHelper.forMediaItem(
           context,
           MediaItem.fromUri(contentUri),
           new DefaultRenderersFactory(context),
           dataSourceFactory);
    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" إلى حلّ هذه المشكلة.