建立媒體瀏覽器服務

應用程式必須在資訊清單中使用意圖篩選器宣告 MediaBrowserService。您可以自行選擇服務名稱;以下範例為「MediaPlaybackService」。

<service android:name=".MediaPlaybackService">
  <intent-filter>
    <action android:name="android.media.browse.MediaBrowserService" />
  </intent-filter>
</service>

注意: 建議實作 MediaBrowserServiceMediaBrowserServiceCompat。 定義位於 media-compat 支援資料庫。 在該網頁中,「MediaBrowserService」一詞指的是 (共 MediaBrowserServiceCompat 個)。

初始化媒體工作階段

服務收到 onCreate() 生命週期回呼方法時,應執行下列步驟:

下列 onCreate() 程式碼示範了這些步驟:

Kotlin

private const val MY_MEDIA_ROOT_ID = "media_root_id"
private const val MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id"

class MediaPlaybackService : MediaBrowserServiceCompat() {

    private var mediaSession: MediaSessionCompat? = null
    private lateinit var stateBuilder: PlaybackStateCompat.Builder

    override fun onCreate() {
        super.onCreate()

        // Create a MediaSessionCompat
        mediaSession = MediaSessionCompat(baseContext, LOG_TAG).apply {

            // Enable callbacks from MediaButtons and TransportControls
            setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
                    or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
            )

            // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
            stateBuilder = PlaybackStateCompat.Builder()
                    .setActions(PlaybackStateCompat.ACTION_PLAY
                                    or PlaybackStateCompat.ACTION_PLAY_PAUSE
                    )
            setPlaybackState(stateBuilder.build())

            // MySessionCallback() has methods that handle callbacks from a media controller
            setCallback(MySessionCallback())

            // Set the session's token so that client activities can communicate with it.
            setSessionToken(sessionToken)
        }
    }
}

Java

public class MediaPlaybackService extends MediaBrowserServiceCompat {
    private static final String MY_MEDIA_ROOT_ID = "media_root_id";
    private static final String MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id";

    private MediaSessionCompat mediaSession;
    private PlaybackStateCompat.Builder stateBuilder;

    @Override
    public void onCreate() {
        super.onCreate();

        // Create a MediaSessionCompat
        mediaSession = new MediaSessionCompat(context, LOG_TAG);

        // Enable callbacks from MediaButtons and TransportControls
        mediaSession.setFlags(
              MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
              MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);

        // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
        stateBuilder = new PlaybackStateCompat.Builder()
                            .setActions(
                                PlaybackStateCompat.ACTION_PLAY |
                                PlaybackStateCompat.ACTION_PLAY_PAUSE);
        mediaSession.setPlaybackState(stateBuilder.build());

        // MySessionCallback() has methods that handle callbacks from a media controller
        mediaSession.setCallback(new MySessionCallback());

        // Set the session's token so that client activities can communicate with it.
        setSessionToken(mediaSession.getSessionToken());
    }
}

管理用戶端連線

MediaBrowserService 有兩種處理用戶端連線的方法: onGetRoot() 控制項 服務存取權 onLoadChildren() 可讓用戶端建構及顯示 MediaBrowserService 的內容階層選單。

使用 onGetRoot() 控制用戶端連線

onGetRoot() 方法會傳回內容階層的根節點。如果 方法會傳回空值,代表連線遭拒。

如要讓用戶端連線至您的服務及瀏覽媒體內容, onGetRoot() 必須傳回非空值 BrowserRoot,也就是 代表內容階層

如要讓用戶端在不瀏覽的情況下連線至 MediaSession,onGetRoot() 仍然必須傳回非空值 BrowserRoot,但根 ID 應該代表 空白的內容階層

onGetRoot() 的常見實作方式可能如下所示:

Kotlin

override fun onGetRoot(
        clientPackageName: String,
        clientUid: Int,
        rootHints: Bundle?
): MediaBrowserServiceCompat.BrowserRoot {

    // (Optional) Control the level of access for the specified package name.
    // You'll need to write your own logic to do this.
    return if (allowBrowsing(clientPackageName, clientUid)) {
        // Returns a root ID that clients can use with onLoadChildren() to retrieve
        // the content hierarchy.
        MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null)
    } else {
        // Clients can connect, but this BrowserRoot is an empty hierarchy
        // so onLoadChildren returns nothing. This disables the ability to browse for content.
        MediaBrowserServiceCompat.BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null)
    }
}

Java

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {

    // (Optional) Control the level of access for the specified package name.
    // You'll need to write your own logic to do this.
    if (allowBrowsing(clientPackageName, clientUid)) {
        // Returns a root ID that clients can use with onLoadChildren() to retrieve
        // the content hierarchy.
        return new BrowserRoot(MY_MEDIA_ROOT_ID, null);
    } else {
        // Clients can connect, but this BrowserRoot is an empty hierarchy
        // so onLoadChildren returns nothing. This disables the ability to browse for content.
        return new BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null);
    }
}

在某些情況下,你可能想控管 至你的 MediaBrowserService。其中一種方法是使用存取控制清單 (ACL) 指定允許的連線,或者列舉 禁止禁止的連線範例:如何實作 ACL 允許特定連線 PackageValidator 通用 Android 音樂播放器課程 範例應用程式。

您應該考慮提供 查詢的是哪種類型的用戶端具體來說,Android Auto 限制了 使用者與音訊應用程式的互動情形。詳情請參閱播放音訊: 自動。個人中心 可在連線時查看 clientPackageName 來判斷用戶端 ,並根據用戶端傳回不同的 BrowserRoot (或 rootHints) 如果有的話)。

onLoadChildren() 通訊內容

用戶端連線後,可以重複呼叫 MediaBrowserCompat.subscribe() 以建構本機的 UI 表示法,藉此掃遍內容階層。subscribe() 方法會將回呼 onLoadChildren() 傳送至服務,並傳回 MediaBrowser.MediaItem 物件清單。

每個 MediaItem 都有專屬的 ID 字串,這個字串為不透明權杖。當客戶想要開啟子選單或播放項目時,會傳遞 ID。您的服務負責將 ID 與適當的選單節點或內容項目建立關聯。

實作簡單的 onLoadChildren() 可能如下所示:

Kotlin

override fun onLoadChildren(
        parentMediaId: String,
        result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>
) {
    //  Browsing not allowed
    if (MY_EMPTY_MEDIA_ROOT_ID == parentMediaId) {
        result.sendResult(null)
        return
    }

    // Assume for example that the music catalog is already loaded/cached.

    val mediaItems = emptyList<MediaBrowserCompat.MediaItem>()

    // Check if this is the root menu:
    if (MY_MEDIA_ROOT_ID == parentMediaId) {
        // Build the MediaItem objects for the top level,
        // and put them in the mediaItems list...
    } else {
        // Examine the passed parentMediaId to see which submenu we're at,
        // and put the children of that menu in the mediaItems list...
    }
    result.sendResult(mediaItems)
}

Java

@Override
public void onLoadChildren(final String parentMediaId,
    final Result<List<MediaItem>> result) {

    //  Browsing not allowed
    if (TextUtils.equals(MY_EMPTY_MEDIA_ROOT_ID, parentMediaId)) {
        result.sendResult(null);
        return;
    }

    // Assume for example that the music catalog is already loaded/cached.

    List<MediaItem> mediaItems = new ArrayList<>();

    // Check if this is the root menu:
    if (MY_MEDIA_ROOT_ID.equals(parentMediaId)) {
        // Build the MediaItem objects for the top level,
        // and put them in the mediaItems list...
    } else {
        // Examine the passed parentMediaId to see which submenu we're at,
        // and put the children of that menu in the mediaItems list...
    }
    result.sendResult(mediaItems);
}

注意: MediaBrowserService 提供的 MediaItem 物件 不應包含圖示點陣圖。呼叫 Uri,改用 Uri setIconUri() 只要為每個項目建立 MediaDescription

如需實作 onLoadChildren() 的範例,請參閱 Android 通用音樂播放器範例應用程式。

媒體瀏覽器服務生命週期

Android 服務的行為取決於其為啟動繫結至一或多個用戶端。建立服務之後,可以啟動和/或繫結服務。而且在這些狀態下都能正常運作,並可執行其預定工作。差別在於服務存在的時間長短。除非所有繫結用戶端解除繫結,否則繫結服務不會遭到刪除。您可以明確停止並刪除已啟動的服務 (假設服務不再繫結至任何用戶端)。

當其他活動中執行的 MediaBrowser 連線至 MediaBrowserService 時,會將活動繫結至服務,使服務繫結 (但不會啟動)。這個預設行為是內建在 MediaBrowserServiceCompat 類別中。

當服務的所有用戶端解除繫結時,只會繫結 (未啟動) 的服務將會遭到刪除。如果此時 UI 活動中斷連線,系統就會刪除服務。如果你還沒播放任何音樂,則不受此限。不過,當廣告開始播放時,使用者可能會希望在切換應用程式後能繼續聆聽內容。當您取消 UI 與其他應用程式的繫結時,並不需要刪除玩家。

因此,您必須確認服務啟動時已啟動 呼叫 startService() 才能播放。A 罩杯 無論是否已繫結,都必須明確停止服務。這個 可確保播放器即使控制 UI 也能繼續執行 將活動解除繫結。

如要停止已啟用的服務,請呼叫 Context.stopService()stopSelf()。系統會盡快停止並銷毀服務。但是,如果一或多個用戶端仍然繫結至服務,則停止服務的呼叫會延遲,直到所有用戶端解除繫結為止。

MediaBrowserService 的生命週期取決於建立方式、繫結的用戶端數量,以及從媒體工作階段回呼收到的呼叫。總結:

  • 當服務因媒體按鈕回應或活動繫結至該服務 (透過其 MediaBrowser 連線之後) 時,此服務就會建立。
  • 媒體工作階段 onPlay() 回呼應包含呼叫 startService() 的程式碼。這可確保服務會啟動並持續執行,即使所有與服務繫結的 UI MediaBrowser 活動已繫結至該服務也一樣。
  • onStop() 回呼應呼叫 stopSelf()。如果服務已啟動,就會停止服務。此外,如果沒有活動繫結,服務會遭到刪除。否則服務會保持繫結,直到所有活動解除繫結為止。(如果在服務刪除前收到後續的 startService() 呼叫,系統會取消待處理的停止作業)。

以下流程圖展示了服務的生命週期管理方式。變數計數器會追蹤已繫結的用戶端數量:

服務生命週期

搭配前景服務使用 MediaStyle 通知

服務播放時,應在前景執行。這可讓系統知道服務正在執行有用的功能,且在記憶體不足時不應終止。前景服務必須顯示通知,讓使用者瞭解這類服務並視需要控制。onPlay() 回呼應將服務置於前景。(請注意,這是「前景」的特殊意義。Android 會將服務視為在前景執行服務,但對使用者進行程序管理時,使用者會在背景執行,有些則在「前景」中會顯示其他應用程式)。

當服務在前景執行時,必須顯示通知,並最好搭配一或多個傳輸控制項。通知中也應該含有來自工作階段中繼資料的實用資訊。

建構並在播放器開始播放時顯示通知。最好在 MediaSessionCompat.Callback.onPlay() 方法內執行這項操作。

以下範例使用 NotificationCompat.MediaStyle, 專為媒體應用程式設計說明如何建構顯示中繼資料和傳輸控制項的通知。便利方法 getController() 可讓你直接在媒體工作階段建立媒體控制器。

Kotlin

// Given a media session and its context (usually the component containing the session)
// Create a NotificationCompat.Builder

// Get the session's metadata
val controller = mediaSession.controller
val mediaMetadata = controller.metadata
val description = mediaMetadata.description

val builder = NotificationCompat.Builder(context, channelId).apply {
    // Add the metadata for the currently playing track
    setContentTitle(description.title)
    setContentText(description.subtitle)
    setSubText(description.description)
    setLargeIcon(description.iconBitmap)

    // Enable launching the player by clicking the notification
    setContentIntent(controller.sessionActivity)

    // Stop the service when the notification is swiped away
    setDeleteIntent(
            MediaButtonReceiver.buildMediaButtonPendingIntent(
                    context,
                    PlaybackStateCompat.ACTION_STOP
            )
    )

    // Make the transport controls visible on the lockscreen
    setVisibility(NotificationCompat.VISIBILITY_PUBLIC)

    // Add an app icon and set its accent color
    // Be careful about the color
    setSmallIcon(R.drawable.notification_icon)
    color = ContextCompat.getColor(context, R.color.primaryDark)

    // Add a pause button
    addAction(
            NotificationCompat.Action(
                    R.drawable.pause,
                    getString(R.string.pause),
                    MediaButtonReceiver.buildMediaButtonPendingIntent(
                            context,
                            PlaybackStateCompat.ACTION_PLAY_PAUSE
                    )
            )
    )

    // Take advantage of MediaStyle features
    setStyle(android.support.v4.media.app.NotificationCompat.MediaStyle()
            .setMediaSession(mediaSession.sessionToken)
            .setShowActionsInCompactView(0)

            // Add a cancel button
            .setShowCancelButton(true)
            .setCancelButtonIntent(
                    MediaButtonReceiver.buildMediaButtonPendingIntent(
                            context,
                            PlaybackStateCompat.ACTION_STOP
                    )
            )
    )
}

// Display the notification and place the service in the foreground
startForeground(id, builder.build())

Java

// Given a media session and its context (usually the component containing the session)
// Create a NotificationCompat.Builder

// Get the session's metadata
MediaControllerCompat controller = mediaSession.getController();
MediaMetadataCompat mediaMetadata = controller.getMetadata();
MediaDescriptionCompat description = mediaMetadata.getDescription();

NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId);

builder
    // Add the metadata for the currently playing track
    .setContentTitle(description.getTitle())
    .setContentText(description.getSubtitle())
    .setSubText(description.getDescription())
    .setLargeIcon(description.getIconBitmap())

    // Enable launching the player by clicking the notification
    .setContentIntent(controller.getSessionActivity())

    // Stop the service when the notification is swiped away
    .setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context,
       PlaybackStateCompat.ACTION_STOP))

    // Make the transport controls visible on the lockscreen
    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)

    // Add an app icon and set its accent color
    // Be careful about the color
    .setSmallIcon(R.drawable.notification_icon)
    .setColor(ContextCompat.getColor(context, R.color.primaryDark))

    // Add a pause button
    .addAction(new NotificationCompat.Action(
        R.drawable.pause, getString(R.string.pause),
        MediaButtonReceiver.buildMediaButtonPendingIntent(context,
            PlaybackStateCompat.ACTION_PLAY_PAUSE)))

    // Take advantage of MediaStyle features
    .setStyle(new MediaStyle()
        .setMediaSession(mediaSession.getSessionToken())
        .setShowActionsInCompactView(0)

        // Add a cancel button
       .setShowCancelButton(true)
       .setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context,
           PlaybackStateCompat.ACTION_STOP)));

// Display the notification and place the service in the foreground
startForeground(id, builder.build());

使用 MediaStyle 通知時,請注意這些行為 NotificationCompat 設定:

  • 使用「setContentIntent()」時,系統會在收到通知時自動啟動服務 是個實用的功能
  • 「不受信任」情況 例如鎖定螢幕,通知內容的預設顯示設定為 VISIBILITY_PRIVATE。您可能會想查看 螢幕鎖定畫面上的傳輸控制項,因此 VISIBILITY_PUBLIC 是你的最佳選擇。
  • 設定背景顏色時請務必小心。收到以下時間的普通通知: Android 5.0 以上版本,這個顏色只會套用至 小型應用程式圖示。但對於 Android 7.0 以下版本的 MediaStyle 通知, 則會用於整個通知背景測試背景顏色。開始 不要使用超亮或螢光色澤的眼睛,

這些設定只有在使用 NotificationCompat.MediaStyle 時才能使用:

  • 使用「setMediaSession()」 來連結通知和工作階段。允許第三方應用程式存取 存取及控制工作階段。
  • 使用 setShowActionsInCompactView() 即可新增要顯示的動作 (最多 3 個) 通知的標準大小 contentView(這裡的暫停按鈕是 )。
  • 在 Android 5.0 (API 級別 21) 以上版本中,只要滑開通知即可停止 因為服務不再於前景執行。你無法執行的操作 這個方法允許使用者移除通知並停止播放 在 Android 5.0 (API 級別 21) 之前,您可以在 通知 setShowCancelButton(true)setCancelButtonIntent()

新增暫停和取消按鈕時,您需要附加 PendingIntent 加入播放動作MediaButtonReceiver.buildMediaButtonPendingIntent() 方法會執行轉換工作 將 PlaybackState 動作傳入 PendingIntent。