המדיה בהורדה

ExoPlayer מספק פונקציונליות להורדת מדיה להפעלה במצב אופליין. ברוב התרחישים לדוגמה, רצוי שההורדות ימשיכו גם כשהאפליקציה נמצאת ברקע. בתרחישי השימוש האלה, האפליקציה צריכה ליצור תת-סוג של DownloadService ולשלוח פקודות לשירות כדי להוסיף, להסיר ולשלוט בהורדות. בתרשים הבא מוצגים הסוגים העיקריים של הגורמים המעורבים.

כיתות להורדת מדיה. כיוון החץ מציין את זרימת הנתונים.

  • DownloadService: עוטף DownloadManager ומעביר אליו פקודות. השירות מאפשר ל-DownloadManager להמשיך לפעול גם כשהאפליקציה פועלת ברקע.
  • DownloadManager: ניהול מספר הורדות, טעינת המצבים שלהן (ושמירתם) מ-DownloadIndex (ול-DownloadIndex), הפעלה ועצירה של הורדות על סמך דרישות כמו קישוריות לרשת וכו'. כדי להוריד את התוכן, בדרך כלל מנהל הקבצים יקריא את הנתונים שהורדתם מ-HttpDataSource ויכתוב אותם ב-Cache.
  • DownloadIndex: שמירת המצבים של ההורדות.

יצירת DownloadService

כדי ליצור DownloadService, יוצרים אותה כסוג משנה ומטמיעים את השיטות המופשטות שלה:

  • getDownloadManager(): הפונקציה מחזירה את הערך של DownloadManager שישמש.
  • getScheduler(): הפונקציה מחזירה את הערך האופציונלי Scheduler, שיכול להפעיל מחדש את השירות כשהדרישות להמשך ההורדות בהמתנה מתקיימות. ExoPlayer מספק את ההטמעות הבאות:
    • PlatformScheduler, שמשתמש ב-JobScheduler (גרסת API מינימלית 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, שאפשר להחזיר על ידי 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 כדי להגדיר DRM ונתונים מותאמים אישית שהאפליקציה רוצה לשייך להורדה, בהתאמה. אפשר לציין את סוג ה-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, SmoothStream ו-HLS) מכילים בדרך כלל מספר טראקים של מדיה. בדרך כלל יש כמה טראקים שמכילים את אותו תוכן באיכות שונה (למשל, טראקים של וידאו באיכות SD, HD ו-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.