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

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

שימוש ב-MediaSessionService

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

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

כשמארחים שחקן בשירות, צריך להשתמש ב-MediaSessionService. כדי לעשות את זה, צריך ליצור כיתה שמרחיבה את 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. מדיה3 מטפל עבורך בקלט של לחצני מדיה כשהשירות פועל.

הצהרה על המקלט של לחצן המדיה של 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 או ה-Bluetooth שולחים בקשה להמשך ההפעלה תכונת החידוש בממשק המשתמש של מערכת Android, 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 מכינה את הנגן ומתחילה את ההפעלה כש הקריאה החוזרת (callback) תסתיים.

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

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

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

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

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

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

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 נפרד.