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