通常在應用程式未在前景執行時,播放媒體會比較理想。舉例來說,音樂播放器通常會在使用者鎖定裝置或使用其他應用程式時繼續播放音樂。Media3 程式庫提供一系列的介面,可讓您支援背景播放。
使用 MediaSessionService
如要啟用背景播放功能,請在獨立的服務中納入 Player
和 MediaSession
。如此一來,即使應用程式不在前景運作,裝置仍可繼續提供媒體內容。
在服務中代管玩家時,請使用 MediaSessionService
。如要這麼做,請建立可擴充 MediaSessionService
的類別,並在其中建立媒體工作階段。
使用 MediaSessionService
後,Google 助理等外部用戶端、系統媒體控制項或 Wear OS 等隨附裝置就能探索您的服務、連線至服務,並控制播放作業,完全不需要存取應用程式的 UI 活動。事實上,可能會同時將多個用戶端應用程式連結至同一個 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" />
您也必須使用 MediaSessionService
意圖篩選器,在資訊清單中宣告 Service
類別。
<service
android:name=".PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
當應用程式在搭載 Android 10 (API 級別 29) 以上版本的裝置上執行時,您必須定義包含 mediaPlayback
的 foregroundServiceType
。
使用 MediaController
控制播放
在包含播放器 UI 的活動或片段中,您可以使用 MediaController
在使用者介面和媒體工作階段之間建立連結。您的 UI 會使用媒體控制器,將 UI 中的指令傳送至工作階段中的播放器。如要進一步瞭解如何建立及使用 MediaController
,請參閱「建立 MediaController
」指南。
處理 UI 指令
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 媒體控制項。
自訂通知
如要自訂通知,請使用 DefaultMediaNotificationProvider.Builder
建立 MediaNotification.Provider
,或是建立供應器介面的自訂實作項目。使用 setMediaNotificationProvider
將提供者新增至 MediaSessionService
。
繼續播放
媒體按鈕是 Android 裝置和其他周邊裝置上的硬體按鈕,例如藍牙耳機上的播放或暫停按鈕。執行服務時,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>
實作播放繼續播放回呼
當藍牙裝置或 Android 系統 UI 的恢復功能要求繼續播放時,系統會呼叫 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 會準備播放器並開始播放。
進階控制器設定和回溯相容性
常見的情況是,在應用程式 UI 中使用 MediaController
來控制播放功能並顯示播放清單。同時,工作階段也會向外部用戶端公開,例如 Android 媒體控制項、行動裝置或電視上的 Google 助理、手錶的 Wear OS,以及車輛中的 Android Auto。Media3 工作階段示範應用程式就是實作這類情境的應用程式範例。
這些外部用戶端可以使用舊版 AndroidX 程式庫的 MediaControllerCompat
或 Android 架構的 android.media.session.MediaController
等 API。Media3 可完全回溯相容於舊版程式庫,並提供與 Android 架構 API 的互通性。
使用媒體通知控制器
請務必瞭解這些舊版或架構控制器從架構 PlaybackState.getActions()
和 PlaybackState.getCustomActions()
讀取的值相同。如要判斷架構工作階段的動作和自訂動作,應用程式可以使用媒體通知控制器,並設定可用的指令和自訂版面配置。這項服務會將媒體通知控制器連結至工作階段,工作階段則會使用回呼 onConnect()
傳回的 ConnectionResult
,設定架構工作階段的動作和自訂動作。
以行動裝置專用情境來說,應用程式可以提供 MediaSession.Callback.onConnect()
的實作方式,為架構工作階段設定可用的指令和自訂版面配置,如下所示:
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。