הפעלה ברקע עם MediaSessionService

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

שימוש ב-MediaSessionService

כדי להפעיל הפעלה ברקע, צריך לכלול את Player ו-MediaSession בשירות נפרד. כך המכשיר יוכל להמשיך להציג מדיה גם כשהאפליקציה לא בחזית.

השירות MediaSessionService מאפשר להפעיל את סשן המדיה בנפרד מהפעילות של האפליקציה
איור 1: ה-MediaSessionService מאפשר לסשן המדיה לפעול בנפרד מהפעילות של האפליקציה

כשמארחים נגן בתוך שירות, צריך להשתמש ב-MediaSessionService. כדי לעשות זאת, יוצרים סוג (class) שמרחיב את MediaSessionService ויוצרים את סשן המדיה בתוכו.

השימוש ב-MediaSessionService מאפשר ללקוחות חיצוניים כמו Google Assistant, אמצעי הבקרה של המדיה במערכת, לחצני המדיה במכשירים היקפיים או מכשירים נלווים כמו Wear OS לגלות את השירות, להתחבר אליו ולשלוט בהפעלה, בלי לגשת בכלל לפעילות בממשק המשתמש של האפליקציה. למעשה, יכולות להיות כמה אפליקציות לקוח שמחוברות לאותו MediaSessionService בו-זמנית, וכל אפליקציה עם MediaController משלה.

הטמעת מחזור החיים של השירות

צריך להטמיע שתי שיטות של מחזור החיים של השירות:

  • onCreate() נקראת כשהמסוף הראשון עומד להתחבר, והשירות נוצר ומופעל. זהו המקום הטוב ביותר לפתח את Player ו-MediaSession.
  • onDestroy() נקרא כשהשירות מושבת. צריך לשחרר את כל המשאבים, כולל הנגן והסשן.

אפשר לשנות את הערך של onTaskRemoved(Intent) כדי להתאים אישית את הפעולה שתתבצע כשהמשתמש יסגור את האפליקציה מהמשימות האחרונות. כברירת מחדל, השירות ימשיך לפעול אם ההפעלה נמשכת, אחרת הוא יופסק.

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null

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

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

Java

public class PlaybackService extends MediaSessionService {
  private MediaSession mediaSession = null;

  // Create your Player and MediaSession in the onCreate lifecycle event
  @Override
  public void onCreate() {
    super.onCreate();
    ExoPlayer player = new ExoPlayer.Builder(this).build();
    mediaSession = new MediaSession.Builder(this, player).build();
  }

  // Remember to release the player and media session in onDestroy
  @Override
  public void onDestroy() {
    mediaSession.getPlayer().release();
    mediaSession.release();
    mediaSession = null;
    super.onDestroy();
  }
}

במקום להמשיך את ההפעלה ברקע, אפשר להפסיק את השירות בכל מקרה שבו המשתמש סוגר את האפליקציה:

Kotlin

override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

בכל הטמעה ידנית אחרת של onTaskRemoved, אפשר להשתמש ב-isPlaybackOngoing() כדי לבדוק אם ההפעלה נחשבת להפעלה מתמשכת והשירות בחזית מופעל.

מתן גישה לסשן המדיה

אפשר לשנות את השיטה onGetSession() כדי לתת ללקוחות אחרים גישה לסשן המדיה שנוצר כשהשירות נוצר.

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null
  // [...] lifecycle methods omitted

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

public class PlaybackService extends MediaSessionService {
  private MediaSession mediaSession = null;
  // [...] lifecycle methods omitted

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

הצהרה על השירות במניפסט

כדי להפעיל שירות שפועל בחזית להפעלת מדיה, נדרשות לאפליקציה ההרשאות FOREGROUND_SERVICE ו-FOREGROUND_SERVICE_MEDIA_PLAYBACK:

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

צריך גם להצהיר על הכיתה Service במניפסט עם מסנן Intent של MediaSessionService ועם foregroundServiceType שכולל את mediaPlayback.

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

שליטה בהפעלה באמצעות MediaController

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

טיפול בפקודות MediaController

ה-MediaSession מקבל פקודות מהבקר דרך ה-MediaSession.Callback שלו. איפוס של MediaSession יוצר הטמעה שמוגדרת כברירת מחדל של MediaSession.Callback, שמטפלת באופן אוטומטי בכל הפקודות ש-MediaController שולח לנגן.

התראה

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

לדוגמה, אפליקציית סטרימינג של מוזיקה שמשתמשת ב-MediaSessionService תיצור MediaNotification שבו יוצגו הכותרת, האומן ועטיפת האלבום של פריט המדיה הנוכחי שמופעל, לצד פקדי ההפעלה על סמך ההגדרה של MediaSession.

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

Kotlin

val mediaItem =
    MediaItem.Builder()
      .setMediaId("media-1")
      .setUri(mediaUri)
      .setMediaMetadata(
        MediaMetadata.Builder()
          .setArtist("David Bowie")
          .setTitle("Heroes")
          .setArtworkUri(artworkUri)
          .build()
      )
      .build()

mediaController.setMediaItem(mediaItem)
mediaController.prepare()
mediaController.play()

Java

MediaItem mediaItem =
    new MediaItem.Builder()
        .setMediaId("media-1")
        .setUri(mediaUri)
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setArtist("David Bowie")
                .setTitle("Heroes")
                .setArtworkUri(artworkUri)
                .build())
        .build();

mediaController.setMediaItem(mediaItem);
mediaController.prepare();
mediaController.play();

מחזור החיים של התראות

ההתראה נוצרת ברגע שלPlayer יש MediaItem מופעים בפלייליסט שלו.

כל עדכוני ההתראות מתבצעים באופן אוטומטי על סמך המצבים Player ו-MediaSession.

לא ניתן להסיר את ההתראה בזמן שהשירות בחזית פועל. כדי להסיר את ההתראה באופן מיידי, צריך להפעיל את הפקודה Player.release() או לנקות את הפלייליסט באמצעות הפקודה Player.clearMediaItems().

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

התאמה אישית של ההתראות

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

אפשר גם להתאים אישית חלק מהלחצנים שמוצגים בהתראה על ידי הגדרת העדפות מותאמות אישית של לחצני מדיה לפקדי המדיה של Android. מידע נוסף על התאמה אישית של אמצעי הבקרה על המדיה ב-Android

כדי להתאים אישית את ההתראה עצמה, אפשר ליצור MediaNotification.Provider באמצעות DefaultMediaNotificationProvider.Builder או ליצור הטמעה מותאמת אישית של ממשק הספק. מוסיפים את הספק ל-MediaSessionService באמצעות setMediaNotificationProvider.

המשך ההפעלה

אחרי סיום ה-MediaSessionService, ואפילו אחרי הפעלה מחדש של המכשיר, אפשר להציע המשך של ההפעלה כדי לאפשר למשתמשים להפעיל מחדש את השירות ולהמשיך את ההפעלה מהמקום שבו הפסיקו. כברירת מחדל, המשך ההפעלה מושבת. כלומר, המשתמש לא יכול להמשיך את ההפעלה כשהשירות לא פועל. כדי להביע הסכמה לשימוש בתכונה הזו, צריך להצהיר על מקלט של לחצן מדיה ולהטמיע את השיטה onPlaybackResumption.

הצהרה על מקלט לחצן המדיה Media3

מתחילים בהצהרה על MediaButtonReceiver במניפסט:

<receiver android:name="androidx.media3.session.MediaButtonReceiver"
  android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.MEDIA_BUTTON" />
  </intent-filter>
</receiver>

הטמעת קריאה חוזרת (callback) להמשך ההפעלה

כשמכשיר Bluetooth או התכונה להמשך ההפעלה בממשק המשתמש של מערכת Android מבקשים להמשיך את ההפעלה, מתבצעת קריאה ל-method‏ onPlaybackResumption().

Kotlin

override fun onPlaybackResumption(
    mediaSession: MediaSession,
    controller: ControllerInfo
): ListenableFuture<MediaItemsWithStartPosition> {
  val settable = SettableFuture.create<MediaItemsWithStartPosition>()
  scope.launch {
    // Your app is responsible for storing the playlist and the start position
    // to use here
    val resumptionPlaylist = restorePlaylist()
    settable.set(resumptionPlaylist)
  }
  return settable
}

Java

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession,
    ControllerInfo controller
) {
  SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create();
  settableFuture.addListener(() -> {
    // Your app is responsible for storing the playlist and the start position
    // to use here
    MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist();
    settableFuture.set(resumptionPlaylist);
  }, MoreExecutors.directExecutor());
  return settableFuture;
}

אם שמרתם פרמטרים אחרים, כמו מהירות ההפעלה, מצב החזרה או מצב הרמיקס, onPlaybackResumption() הוא מקום טוב להגדיר את הנגן עם הפרמטרים האלה לפני ש-Media3 מכין את הנגן ומתחיל את ההפעלה כשהקריאה החוזרת מסתיימת.

הגדרה מתקדמת של בקר ותאימות לאחור

תרחיש נפוץ הוא שימוש ב-MediaController בממשק המשתמש של האפליקציה כדי לשלוט בהפעלה ולהציג את הפלייליסט. באותו הזמן, הסשן חשוף ללקוחות חיצוניים כמו אמצעי הבקרה של המדיה ב-Android ו-Assistant בנייד או בטלוויזיה, Wear OS בשעונים ו-Android Auto במכוניות. אפליקציית הדגמה של סשן של Media3 היא דוגמה לאפליקציה שמטמיעה תרחיש כזה.

לקוחות חיצוניים אלה עשויים להשתמש בממשקי API כמו MediaControllerCompat בספריית AndroidX הישנה או android.media.session.MediaController בפלטפורמת Android. Media3 תואם לאחור לספרייה הקודמת ומאפשר יכולת פעולה הדדית עם Android platform API.

שימוש בחלונית השליטה של התראות המדיה

חשוב להבין שהבקרים הקודמים והבקרים בפלטפורמה משתפים את אותו מצב, ולא ניתן להתאים אישית את החשיפה לפי בקר (לדוגמה, PlaybackState.getActions() ו-PlaybackState.getCustomActions() הזמינים). אפשר להשתמש בבקר ההתראות של המדיה כדי להגדיר את המצב שמוגדר בסשן המדיה בפלטפורמה כך שיתאים לבקרים הקודמים ולבקרים בפלטפורמה.

לדוגמה, אפליקציה יכולה לספק הטמעה של MediaSession.Callback.onConnect() כדי להגדיר את הפקודות הזמינות ואת ההעדפות של לחצני המדיה באופן ספציפי לסשן בפלטפורמה, באופן הבא:

Kotlin

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  if (session.isMediaNotificationController(controller)) {
    val sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(customCommandSeekBackward)
        .add(customCommandSeekForward)
        .build()
    val playerCommands =
      ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
        .remove(COMMAND_SEEK_TO_PREVIOUS)
        .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
        .remove(COMMAND_SEEK_TO_NEXT)
        .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
        .build()
    // Custom button preferences and commands to configure the platform session.
    return AcceptedResultBuilder(session)
      .setMediaButtonPreferences(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default button preferences for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Java

@Override
public ConnectionResult onConnect(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS
            .buildUpon()
            .add(customCommandSeekBackward)
            .add(customCommandSeekForward)
            .build();
    Player.Commands playerCommands =
        ConnectionResult.DEFAULT_PLAYER_COMMANDS
            .buildUpon()
            .remove(COMMAND_SEEK_TO_PREVIOUS)
            .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
            .remove(COMMAND_SEEK_TO_NEXT)
            .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
            .build();
    // Custom button preferences and commands to configure the platform session.
    return new AcceptedResultBuilder(session)
        .setMediaButtonPreferences(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands with default button preferences for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

מתן הרשאה ל-Android Auto לשלוח פקודות בהתאמה אישית

כשמשתמשים ב-MediaLibraryService כדי לתמוך ב-Android Auto באמצעות האפליקציה לנייד, לצורך שליטה ב-Android Auto נדרשות פקודות זמינות מתאימות. אחרת, Media3 ידחה פקודות בהתאמה אישית נכנסות מהשליטה הזו:

Kotlin

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
      .add(customCommandSeekBackward)
      .add(customCommandSeekForward)
      .build()
  if (session.isMediaNotificationController(controller)) {
    // [...] See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available session commands to accept incoming custom commands from Auto.
    return AcceptedResultBuilder(session)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Java

@Override
public ConnectionResult onConnect(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS
          .buildUpon()
          .add(customCommandSeekBackward)
          .add(customCommandSeekForward)
          .build();
  if (session.isMediaNotificationController(controller)) {
    // [...] See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

באפליקציית הדגמה של הסשן יש מודול לכלי רכב שמראה תמיכה ב-Automotive OS, שדורש קובץ APK נפרד.