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

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

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

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

إنشاء خدمة DownloadService

لإنشاء DownloadService، عليك تصنيفه إلى فئة فرعية وتنفيذ أساليبه المجرّدة:

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

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

يمكن إزالة ملف تنزيل عن طريق إرسال أمر remove إلى 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

يجعل المثال السابق ذاكرة التخزين المؤقت للتنزيل متاحة لتشغيل جميع MediaItems. يمكنك أيضًا إتاحة ذاكرة التخزين المؤقت للتنزيل لنُسخ 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.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 لإجراء ذلك.