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

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

שימוש ב-MediaSessionService

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

ה-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() כדי לבדוק אם ההפעלה נחשבת להפעלה מתמשכת והשירות בחזית הופעל.

מתן גישה להפעלת המדיה

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

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

ההתראה נוצרת ברגע שיש 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>

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

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

השיטה הזו מופעלת בזמן האתחול כדי ליצור את ההתראה על חידוש ממשק המשתמש של מערכת 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 מדור קודם או MediaControllerCompat של פלטפורמת Android.android.media.session.MediaController ‫Media3 תואמת לאחור באופן מלא לספרייה הקודמת ומספקת יכולת פעולה הדדית עם ה-API של פלטפורמת Android.

זיהוי בקרי מהימנים

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

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

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

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

Kotlin

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): 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 AcceptedResultBuilder(session)
      .setMediaButtonPreferences(listOf(seekBackButton, seekForwardButton))
      .setAvailablePlayerCommands(playerCommands)
      .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)) {
    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(seekBackButton, seekForwardButton))
        .setAvailablePlayerCommands(playerCommands)
        .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_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 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(customCommand).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 נפרד.