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

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

שימוש ב-MediaSessionService

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

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

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

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

Java

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

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

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

Kotlin

@OptIn(UnstableApi::class)
override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@OptIn(markerClass = UnstableApi.class)
@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

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

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

צריך לשנות את ה-method‏ onGetSession() כדי לתת ללקוחות אחרים גישה לסשן המדיה שנוצר כשהשירות נוצר.

Kotlin

class PlaybackService : MediaSessionService() {

  // [...] lifecycle methods omitted

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

Java

class PlaybackService extends MediaSessionService {

  // [...] 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

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

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

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

התראה

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

לדוגמה, אפליקציית סטרימינג של מוזיקה שמשתמשת ב-MediaSessionService תיצור MediaNotification שמציג את שם השיר, האומן ועטיפת האלבום של קובץ המדיה שמופעל כרגע, לצד רכיבי UI להפעלה שמבוססים על ההגדרות של 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();

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

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

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

Kotlin

override fun onPlaybackResumption(
  mediaSession: MediaSession,
  controller: MediaSession.ControllerInfo,
  isForPlayback: Boolean,
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
  val settableFuture = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
  settableFuture.addListener(
    {
      // Your app is responsible for storing the playlist, metadata (like title
      // and artwork) of the current item and the start position to use here.
      val resumptionPlaylist = restorePlaylist()
      settableFuture.set(resumptionPlaylist)
    },
    MoreExecutors.directExecutor(),
  )
  return settableFuture
}

Java

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

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

ה-method הזו נקראת בזמן ההפעלה כדי ליצור את ההתראה על המשך הפעולה של ממשק המשתמש של מערכת Android אחרי הפעלה מחדש של המכשיר, כשהערך של isForPlayback מוגדר כ-false. בהתראה עשירה, מומלץ למלא את השדות MediaMetadata כמו title ו-artworkData או artworkUri של הפריט הנוכחי בערכים שזמינים באופן מקומי, כי יכול להיות שעדיין אין גישה לרשת. אפשר גם להוסיף את MediaConstants.EXTRAS_KEY_COMPLETION_STATUS ו-MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE ל-MediaMetadata.extras כדי לציין את מיקום ההפעלה להמשך.

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

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

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

זיהוי של נאמני מידע מהימנים

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

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

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

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

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  if (session.isMediaNotificationController(controller)) {
    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 immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setMediaButtonPreferences(listOf(seekBackButton, seekForwardButton))
        .setAvailablePlayerCommands(playerCommands)
        .build()
    )
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    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 immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setMediaButtonPreferences(ImmutableList.of(seekBackButton, seekForwardButton))
            .setAvailablePlayerCommands(playerCommands)
            .build());
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

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

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

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build()
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
  // Default commands for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

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

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