הגשת תוכן באמצעות MediaLibraryService

אפליקציות מדיה מכילות לעיתים קרובות אוספים של פריטי מדיה, שמסודרים בהיררכיה. לדוגמה, שירים באלבום או פרקים של תוכנית טלוויזיה בפלייליסט. ההיררכיה הזו של קובצי מדיה נקראת ספריית מדיה.

דוגמאות לתוכן מדיה שמסודר בהיררכיה
איור 1: דוגמאות להיררכיות של קובצי מדיה שיוצרות ספריית מדיה.

‫MediaLibraryService מספק ממשק API סטנדרטי להצגה של ספריית המדיה ולגישה אליה. זה יכול להיות שימושי, למשל, כשמוסיפים תמיכה ב-Android Auto לאפליקציית המדיה, שמספקת ממשק משתמש בטוח לנהיגה לספריית המדיה.

הרכבת MediaLibraryService

ההטמעה של MediaLibraryService דומה להטמעה של MediaSessionService, אבל ב-method‏ onGetSession() צריך להחזיר MediaLibrarySession במקום MediaSession.

Kotlin

class PlaybackService : MediaLibraryService() {
  private var mediaLibrarySession: MediaLibrarySession? = null
  private val callback: MediaLibrarySession.Callback =
    object : MediaLibrarySession.Callback {
      /* ... */
    }

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? {
    // If desired, validate the controller before returning the media library session
    return mediaLibrarySession
  }

  // Create your player and media library session in the onCreate lifecycle event
  override fun onCreate() {
    super.onCreate()
    val player = ExoPlayer.Builder(this).build()
    mediaLibrarySession = MediaLibrarySession.Builder(this, player, callback).build()
  }

  // Remember to release the player and media library session in onDestroy
  override fun onDestroy() {
    mediaLibrarySession?.run {
      player.release()
      release()
      mediaLibrarySession = null
    }
    super.onDestroy()
  }
}

Java

class PlaybackService extends MediaLibraryService {
  MediaLibrarySession mediaLibrarySession = null;
  MediaLibrarySession.Callback callback = new MediaLibrarySession.Callback() {
        /* ... */
      };

  @Override
  public MediaLibrarySession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    // If desired, validate the controller before returning the media library session
    return mediaLibrarySession;
  }

  // Create your player and media library session in the onCreate lifecycle event
  @Override
  public void onCreate() {
    super.onCreate();
    ExoPlayer player = new ExoPlayer.Builder(this).build();
    mediaLibrarySession = new MediaLibrarySession.Builder(this, player, callback).build();
  }

  // Remember to release the player and media library session in onDestroy
  @Override
  public void onDestroy() {
    if (mediaLibrarySession != null) {
      mediaLibrarySession.getPlayer().release();
      mediaLibrarySession.release();
      mediaLibrarySession = null;
    }
    super.onDestroy();
  }
}
חשוב לזכור להצהיר על Service ועל ההרשאות הנדרשות גם בקובץ המניפסט:

<service
    android:name=".PlaybackService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaLibraryService"/>
        <action android:name="android.media.browse.MediaBrowserService"/>
    </intent-filter>
</service>

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

מומלץ לרשום את השירות גם כפלטפורמה וגם כממשק השירות של Media3:

  • כדי לשמור על תאימות עם לקוחות שמשתמשים בממשקי ה-API של סשן המדיה בפלטפורמה, מומלץ לכלול את <action android:name="android.media.browse.MediaBrowserService"/> ברכיב intent-filter. ‫Media3 מספקת באופן אוטומטי תאימות לדורות קודמים לממשק השירות הזה.

  • כדי להבטיח שההגדרה תתאים גם בעתיד, וכדי לוודא שאפליקציות שמשתמשות ב-Media3 יוכלו לתקשר עם השירות שלכם באמצעות Media3 API, צריך לספק את <action android:name="androidx.media3.session.MediaLibraryService"/> לצד האפשרות של הפלטפורמה.

כך אפליקציות יכולות למצוא את השירות שלכם דרך PackageManager ולהתחבר אל MediaBrowser דרך אחד מהממשקים האלה.

שימוש בMediaLibrarySession

‫API של MediaLibraryService מצפה שספריית המדיה תהיה בפורמט עץ, עם צומת שורש אחד וצומתי צאצאים שיכולים להיות ניתנים להפעלה או ניתנים לעיון.

MediaLibrarySession מרחיב את ה-API של MediaSession כדי להוסיף ממשקי API לעיון בתוכן. בהשוואה לקריאה החוזרת (callback) של MediaSession, הקריאה החוזרת של MediaLibrarySession מוסיפה שיטות כמו:

  • ‫onGetLibraryRoot() כשלקוח מבקש את השורש MediaItem של עץ תוכן
  • ‫onGetChildren() כשלקוח מבקש את הצאצאים של MediaItem בעץ התוכן
  • onGetSearchResult() כשלקוח מבקש תוצאות חיפוש מעץ התוכן של שאילתה נתונה

שיטות קריאה חוזרת רלוונטיות יכללו אובייקט LibraryParams עם אותות נוספים לגבי סוג עץ התוכן שאפליקציית הלקוח מעוניינת בו.

כפתורי פקודות לקובצי מדיה

אפליקציית סשן יכולה להצהיר על כפתורי פקודות שנתמכים על ידי MediaItem ב-MediaMetadata. כך אפשר להקצות רשומה אחת או יותר של CommandButton לפריט מדיה, שבקר יכול להציג ולהשתמש בה כדי לשלוח את הפקודה המותאמת אישית של הפריט לסשן בצורה נוחה.

הגדרת כפתורי פקודות בצד של הסשן

כשיוצרים את הסשן, אפליקציית הסשן מצהירה על קבוצת כפתורי הפקודות שהסשן יכול לטפל בהם כפקודות בהתאמה אישית:

Kotlin

val allCommandButtons =
  listOf(
    CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD)
      .setDisplayName(context.getString(R.string.add_to_playlist))
      .setSessionCommand(SessionCommand(COMMAND_PLAYLIST_ADD, Bundle.EMPTY))
      .setExtras(playlistAddExtras)
      .build(),
    CommandButton.Builder(CommandButton.ICON_RADIO)
      .setDisplayName(context.getString(R.string.radio_station))
      .setSessionCommand(SessionCommand(COMMAND_RADIO, Bundle.EMPTY))
      .setExtras(radioExtras)
      .build(),
  )
// Add all command buttons for media items supported by the session.
val session =
  MediaSession.Builder(context, player)
    .setCommandButtonsForMediaItems(allCommandButtons)
    .build()

Java

ImmutableList<CommandButton> allCommandButtons =
    ImmutableList.of(
        new CommandButton.Builder(CommandButton.ICON_PLAYLIST_ADD)
            .setDisplayName(context.getString(R.string.add_to_playlist))
            .setSessionCommand(new SessionCommand(COMMAND_PLAYLIST_ADD, Bundle.EMPTY))
            .setExtras(playlistAddExtras)
            .build(),
        new CommandButton.Builder(CommandButton.ICON_RADIO)
            .setDisplayName(context.getString(R.string.radio_station))
            .setSessionCommand(new SessionCommand(COMMAND_RADIO, Bundle.EMPTY))
            .setExtras(radioExtras)
            .build());
// Add all command buttons for media items supported by the session.
MediaSession session =
    new MediaSession.Builder(context, player)
        .setCommandButtonsForMediaItems(allCommandButtons)
        .build();

כשיוצרים קובץ מדיה, אפליקציית סשן יכולה להוסיף קבוצה של מזהי פקודות נתמכות שמפנים לפקודות סשן של לחצני פקודות שהוגדרו במהלך יצירת הסשן:

Kotlin

val mediaItem =
  MediaItem.Builder()
    .setMediaMetadata(
      MediaMetadata.Builder()
        .setSupportedCommands(listOf(COMMAND_PLAYLIST_ADD, COMMAND_RADIO))
        .build()
    )
    .build()

Java

MediaItem mediaItem =
    new MediaItem.Builder()
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setSupportedCommands(ImmutableList.of(COMMAND_PLAYLIST_ADD, COMMAND_RADIO))
                .build())
        .build();

כשבקר או דפדפן מתחברים או קוראים לשיטה אחרת של הסשן Callback, אפליקציית הסשן יכולה לבדוק את ה-ControllerInfo שמועבר לקריאה החוזרת (callback) כדי לקבל את המספר המקסימלי של לחצני הפקודות שבקר או דפדפן יכולים להציג. ה-ControllerInfo שמועבר לשיטת קריאה חוזרת מספק פונקציית getter כדי לגשת לערך הזה בקלות. כברירת מחדל, הערך מוגדר ל-0, מה שמציין שהדפדפן או הבקר לא תומכים בתכונה הזו:

Kotlin

override fun onGetItem(
  session: MediaLibrarySession,
  browser: MediaSession.ControllerInfo,
  mediaId: String,
): ListenableFuture<LibraryResult<MediaItem>> {
  val settableFuture = SettableFuture.create<LibraryResult<MediaItem>>()

  val maxCommandsForMediaItems = browser.maxCommandsForMediaItems
  loadMediaItemAsync(settableFuture, mediaId, maxCommandsForMediaItems)

  return settableFuture
}

Java

@Override
public ListenableFuture<LibraryResult<MediaItem>> onGetItem(
    MediaLibraryService.MediaLibrarySession session,
    ControllerInfo browser,
    String mediaId) {
  SettableFuture<LibraryResult<MediaItem>> settableFuture = SettableFuture.create();

  int maxCommandsForMediaItems = browser.getMaxCommandsForMediaItems();
  loadMediaItemAsync(settableFuture, mediaId, maxCommandsForMediaItems);

  return settableFuture;
}

כדי לטפל בפעולה מותאמת אישית שנשלחה לקובץ מדיה, אפליקציית הסשן יכולה לקבל את מזהה פריט המדיה מהארגומנטים Bundle שמועברים אל onCustomCommand:

Kotlin

override fun onCustomCommand(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
  customCommand: SessionCommand,
  args: Bundle,
): ListenableFuture<SessionResult> {
  val mediaItemId = args.getString(MediaConstants.EXTRA_KEY_MEDIA_ID)
  return if (mediaItemId != null)
    handleCustomCommandForMediaItem(controller, customCommand, mediaItemId, args)
  else handleCustomCommand(controller, customCommand, args)
}

Java

@Override
public ListenableFuture<SessionResult> onCustomCommand(
    MediaSession session,
    ControllerInfo controller,
    SessionCommand customCommand,
    Bundle args) {
  String mediaItemId = args.getString(MediaConstants.EXTRA_KEY_MEDIA_ID);
  return mediaItemId != null
      ? handleCustomCommandForMediaItem(controller, customCommand, mediaItemId, args)
      : handleCustomCommand(controller, customCommand, args);
}

שימוש בכפתורי הפקודות כדפדפן או כבקר

בצד של MediaController, אפליקציה יכולה להצהיר על המספר המקסימלי של לחצני פקודות שהיא תומכת בהם לקובץ מדיה, בזמן שהיא יוצרת את MediaController או את MediaBrowser:

Kotlin

val browserFuture =
  MediaBrowser.Builder(context, sessionToken).setMaxCommandsForMediaItems(3).buildAsync()

Java

ListenableFuture<MediaBrowser> browserFuture =
    new MediaBrowser.Builder(context, sessionToken).setMaxCommandsForMediaItems(3).buildAsync();

כשהיא מחוברת לסשן, אפליקציית הבקר יכולה לקבל את כפתורי הפקודות שנתמכים על ידי קובץ המדיה, ושלבקר יש את הפקודה הזמינה שניתנה על ידי אפליקציית הסשן:

Kotlin

val commandButtonsForMediaItem = controller.getCommandButtonsForMediaItem(mediaItem)

Java

ImmutableList<CommandButton> commandButtonsForMediaItem =
    controller.getCommandButtonsForMediaItem(mediaItem);

Kotlin

val future =
  controller.sendCustomCommand(
    requireNotNull(addToPlaylistButton.sessionCommand),
    mediaItem,
    Bundle.EMPTY,
  )

Java

ListenableFuture<SessionResult> future =
    controller.sendCustomCommand(
        checkNotNull(addToPlaylistButton.sessionCommand), mediaItem, Bundle.EMPTY);