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

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

שימוש ב-MediaSessionService

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

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

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

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

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

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

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

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()
  }

  // The user dismissed the app from the recent tasks
  override fun onTaskRemoved(rootIntent: Intent?) {
    val player = mediaSession?.player!!
    if (!player.playWhenReady
        || player.mediaItemCount == 0
        || player.playbackState == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf()
    }
  }

  // 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();
  }

  // The user dismissed the app from the recent tasks
  @Override
  public void onTaskRemoved(@Nullable Intent rootIntent) {
    Player player = mediaSession.getPlayer();
    if (!player.getPlayWhenReady()
        || player.getMediaItemCount() == 0
        || player.getPlaybackState() == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf();
    }
  }

  // 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?) {
  val player = mediaSession.player
  if (player.playWhenReady) {
    // Make sure the service is not in foreground.
    player.pause()
  }
  stopSelf()
}

Java

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  Player player = mediaSession.getPlayer();
  if (player.getPlayWhenReady()) {
    // Make sure the service is not in foreground.
    player.pause();
  }
  stopSelf();
}

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

כדי לתת ללקוחות אחרים גישה למדיה שלך, צריך לשנות את השיטה 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 למניפסט, ואם מטרגטים ל-API 34 מעל גם 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.

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

עליך להגדיר foregroundServiceType שכולל את הנתון mediaPlayback כשהאפליקציה שלך פועלת במכשיר עם Android 10 (רמת API 29) ואילך.

שליטה בהפעלה באמצעות 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();

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

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

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

המשך ההפעלה

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

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

Media3 כולל API שמאפשר למשתמשים להמשיך הפעלה אחרי שהאפליקציה הסתיימה וגם אחרי שהמכשיר נסגר בוצעה הפעלה מחדש. כברירת מחדל, האפשרות לחידוש ההפעלה מושבתת. כלומר, המשתמש לא ניתן להמשיך את ההפעלה כשהשירות אינו פועל. כדי להביע הסכמה, קודם כול מגדירים את המאפיין 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 framework API.

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

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

בתרחיש של שימוש בנייד בלבד, אפליקציה יכולה לספק הטמעה של 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 layout and available commands to configure the legacy/framework session.
    return AcceptedResultBuilder(session)
      .setCustomLayout(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default custom layout 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 layout and available commands to configure the legacy/framework session.
    return new AcceptedResultBuilder(session)
        .setCustomLayout(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands without default custom layout 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 with default custom layout 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 without default custom layout for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

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