לרוב רצוי להפעיל מדיה כשאפליקציה לא פועלת בחזית. לדוגמה, בדרך כלל נגן מוזיקה ממשיך להשמיע מוזיקה כשהמשתמש נועל את המכשיר או משתמש באפליקציה אחרת. הספרייה Media3 מספקת סדרה של ממשקים שמאפשרים לתמוך בהפעלה ברקע.
שימוש ב-MediaSessionService
כדי להפעיל הפעלה ברקע, צריך לכלול את Player
ו-MediaSession
בשירות נפרד.
כך המכשיר יכול להמשיך להציג מדיה גם כשהאפליקציה לא בחזית.
כשמארחים נגן בתוך שירות, צריך להשתמש ב-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
ב-Activity או ב-Fragment שמכילים את ממשק המשתמש של הנגן, אפשר להשתמש ב-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 שולחים בקשה להמשך ההפעלה, מתבצעת קריאה לשיטת הקריאה החוזרת של 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.
שימוש בחלונית השליטה בהתראות של מדיה
חשוב להבין שבקרי ה-framework או ב-framework הקודמים קוראים את אותם הערכים מה-framework PlaybackState.getActions()
ומ-PlaybackState.getCustomActions()
. כדי לקבוע פעולות ופעולות מותאמות אישית בסשן של ה-framework, האפליקציה יכולה להשתמש בבקר ההתראות של המדיה ולהגדיר את הפקודות הזמינות ואת הפריסה המותאמת אישית שלה. השירות מחבר את הבקר של התראות המדיה לסשן, והסשן משתמש ב-ConnectionResult
שהוחזר על ידי onConnect()
של הקריאה החוזרת (callback) כדי להגדיר פעולות ופעולות בהתאמה אישית של סשן המסגרת.
בתרחיש לנייד בלבד, אפליקציה יכולה לספק הטמעה של 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 נפרד.