應用程式不在前景時,通常會需要播放媒體。舉例來說,使用者鎖定裝置或使用其他應用程式時,音樂播放器通常會繼續播放音樂。Media3 程式庫提供一系列介面,可讓您支援背景播放功能。
使用 MediaSessionService
如要啟用背景播放功能,請在獨立的 Service 中加入 Player 和 MediaSession。即使應用程式不在前景,裝置仍可繼續提供媒體服務。
MediaSessionService 可讓媒體工作階段與應用程式的活動分開執行在服務中代管玩家時,您應使用 MediaSessionService。如要這麼做,請建立擴充 MediaSessionService 的類別,並在其中建立媒體工作階段。
使用 MediaSessionService 後,Google 助理、系統媒體控制項、周邊裝置上的媒體按鈕,或 Wear OS 等隨附裝置等外部用戶端,就能探索及連線至您的服務,並控制播放作業,完全不必存取應用程式的 UI 活動。事實上,多個用戶端應用程式可以同時連線至同一個 MediaSessionService,每個應用程式都有自己的 MediaController。
實作服務生命週期
您必須實作服務的兩個生命週期方法:
- 當第一個控制器即將連線,且服務已例項化並啟動時,系統會呼叫
onCreate()。是建構Player和MediaSession的最佳場所。 - 服務停止時,系統會呼叫
onDestroy()。所有資源 (包括播放器和工作階段) 都必須釋出。
您可以選擇覆寫 onTaskRemoved(Intent),自訂使用者從最近的工作中關閉應用程式時的行為。根據預設,如果正在播放內容,服務會繼續執行;否則會停止。
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() } // 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(); } // 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?) { pauseAllPlayersAndStopSelf() }
Java
@Override public void onTaskRemoved(@Nullable Intent rootIntent) { pauseAllPlayersAndStopSelf(); }
如要手動實作 onTaskRemoved,可以使用 isPlaybackOngoing() 檢查播放作業是否正在進行,以及前景服務是否已啟動。
提供媒體工作階段存取權
覆寫 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 和 FOREGROUND_SERVICE_MEDIA_PLAYBACK 權限,才能執行播放前景服務:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
您也必須在資訊清單中宣告 Service 類別,並使用 MediaSessionService 的意圖篩選器和包含 mediaPlayback 的 foregroundServiceType。
<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 控制播放功能
在含有播放器 UI 的 Activity 或 Fragment 中,您可以使用 MediaController 建立 UI 與媒體工作階段之間的連結。UI 會使用媒體控制器,將指令從 UI 傳送至工作階段中的播放器。如要進一步瞭解如何建立及使用 MediaController,請參閱「建立 MediaController」指南。
處理 MediaController 指令
MediaSession 會透過 MediaSession.Callback 接收控制器的指令。初始化 MediaSession 會建立 MediaSession.Callback 的預設實作項目,自動處理 MediaController 傳送至播放器的所有指令。
通知
系統會自動為您建立 MediaNotification,在大多數情況下應該都能正常運作。MediaSessionService根據預設,發布的通知是 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();
通知生命週期
只要播放清單中有 Player MediaItem 執行個體,系統就會建立通知。
所有通知更新都會根據 Player 和 MediaSession 狀態自動進行。
前景服務執行期間無法移除通知。如要立即移除通知,請呼叫 Player.release() 或使用 Player.clearMediaItems() 清除播放清單。
如果播放器暫停、停止或發生錯誤超過 10 分鐘,且使用者未進一步互動,服務就會自動脫離前景服務狀態,以便系統終止服務。您可以實作播放續播功能,讓使用者重新啟動服務生命週期,並在稍後繼續播放。
自訂通知
您可以修改 MediaItem.MediaMetadata,自訂目前播放項目的中繼資料。如要更新現有項目的中繼資料,可以使用 Player.replaceMediaItem 更新中繼資料,不會中斷播放。
你也可以為 Android 媒體控制項設定自訂媒體按鈕偏好設定,自訂通知中顯示的部分按鈕。進一步瞭解如何自訂 Android 媒體控制項。
如要進一步自訂通知本身,請建立 MediaNotification.Provider,並使用 DefaultMediaNotificationProvider.Builder,或建立提供者介面的自訂實作項目。使用 setMediaNotificationProvider 將提供者新增至 MediaSessionService。
繼續播放
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>
實作繼續播放回呼
當藍牙裝置或 Android 系統 UI 的繼續播放功能要求繼續播放時,系統會呼叫 onPlaybackResumption() 回呼方法。
Kotlin
override fun onPlaybackResumption( mediaSession: MediaSession, controller: ControllerInfo, isForPlayback: Boolean, ): ListenableFuture<MediaItemsWithStartPosition> { val settable = SettableFuture.create<MediaItemsWithStartPosition>() scope.launch { // 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() settable.set(resumptionPlaylist) } return settable }
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 準備播放器並開始播放之前,使用這些參數設定播放器。
裝置以 isForPlayback 設為 false 的狀態重新啟動後,系統會在啟動時呼叫這個方法,建立 Android 系統 UI 繼續通知。如果是豐富通知,建議您填寫目前項目的 MediaMetadata 欄位,例如 title 和 artworkData 或 artworkUri,並使用本機可用的值,因為網路存取可能尚未開放。您也可以在 MediaMetadata.extras 中新增 MediaConstants.EXTRAS_KEY_COMPLETION_STATUS 和 MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE,指出要從哪個位置繼續播放。
進階控制器設定和回溯相容性
常見情境是在應用程式 UI 中使用 MediaController 控制播放功能及顯示播放清單。同時,工作階段會向外部用戶端公開,例如 Android 媒體控制項和行動裝置/電視上的 Google 助理、手錶上的 Wear OS,以及車輛上的 Android Auto。Media3 工作階段試用版應用程式就是實作這類情境的應用程式範例。
這些外部用戶端可能會使用舊版 AndroidX 程式庫的 MediaControllerCompat 或 Android 平台的 android.media.session.MediaController 等 API。Media3 完全回溯相容於舊版程式庫,並與 Android 平台 API 互通。
識別信任的控制器
任何應用程式都可以嘗試連線至媒體工作階段或媒體庫。如要限制對系統控制器、具備媒體內容控制權限的控制器,以及您自己應用程式的存取權,可以使用 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_AND_LIBRARY_COMMANDS.buildUpon() .add(customCommand) .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 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。