Tạo dịch vụ trình duyệt đa phương tiện

Ứng dụng của bạn phải khai báo MediaBrowserService bằng một bộ lọc ý định trong tệp kê khai. Bạn có thể chọn tên dịch vụ của riêng mình. Trong ví dụ sau, tên dịch vụ được chọn là MediaPlaybackService.

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

Khởi chạy phiên phát nội dung nghe nhìn

Khi nhận được phương thức gọi lại trong vòng đời onCreate(), dịch vụ sẽ thực hiện các bước sau:

onCreate() sau đây minh hoạ các bước này:

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());
    }
}

Quản lý các mối kết nối với khách hàng

MediaBrowserService có hai phương thức xử lý các kết nối của ứng dụng khách: onGetRoot() kiểm soát quyền truy cập vào dịch vụ và onLoadChildren() cho phép ứng dụng khách tạo và hiển thị một trình đơn về hệ thống phân cấp nội dung của MediaBrowserService.

Kiểm soát các mối kết nối với khách hàng bằng onGetRoot()

Phương thức onGetRoot() trả về nút gốc của hệ phân cấp nội dung. Nếu phương thức này trả về giá trị rỗng, thì kết nối sẽ bị từ chối.

Để cho phép các ứng dụng kết nối với dịch vụ của bạn và duyệt xem nội dung nghe nhìn của dịch vụ, onGetRoot() phải trả về một BrowserRoot khác rỗng. Đây là mã nhận dạng gốc đại diện cho hệ thống phân cấp nội dung của bạn.

Để cho phép các ứng dụng kết nối với MediaSession của bạn mà không cần duyệt xem, onGetRoot()vẫn phải trả về một BrowserRoot không rỗng, nhưng mã nhận dạng gốc phải biểu thị một hệ thống phân cấp nội dung trống.

Cách triển khai onGetRoot() điển hình có thể có dạng như sau:

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);
    }
}

Trong một số trường hợp, bạn có thể muốn kiểm soát những người có thể kết nối với MediaBrowserService của bạn. Một cách là sử dụng danh sách kiểm soát quyền truy cập (ACL) để chỉ định những kết nối được phép hoặc liệt kê những kết nối bị cấm. Để biết ví dụ về cách triển khai một ACL cho phép các kết nối cụ thể, hãy xem lớp PackageValidator trong ứng dụng mẫu Universal Android Music Player.

Bạn nên cân nhắc việc cung cấp các hệ thống phân cấp nội dung khác nhau tuỳ thuộc vào loại ứng dụng đang thực hiện truy vấn. Cụ thể, Android Auto giới hạn cách người dùng tương tác với các ứng dụng âm thanh. Để biết thêm thông tin, hãy xem phần Phát âm thanh cho ô tô. Bạn có thể xem clientPackageName tại thời điểm kết nối để xác định loại máy khách và trả về một BrowserRoot khác tuỳ thuộc vào máy khách (hoặc rootHints nếu có).

Trao đổi nội dung với onLoadChildren()

Sau khi ứng dụng kết nối, ứng dụng có thể duyệt qua hệ phân cấp nội dung bằng cách thực hiện các lệnh gọi lặp lại đến MediaBrowserCompat.subscribe() để tạo một bản trình bày cục bộ về giao diện người dùng. Phương thức subscribe() gửi lệnh gọi lại onLoadChildren() đến dịch vụ. Dịch vụ này sẽ trả về một danh sách các đối tượng MediaBrowser.MediaItem.

Mỗi MediaItem đều có một chuỗi mã nhận dạng duy nhất, là một mã thông báo không công khai. Khi một ứng dụng khách muốn mở một trình đơn con hoặc phát một mục, ứng dụng đó sẽ chuyển mã nhận dạng. Dịch vụ của bạn chịu trách nhiệm liên kết mã nhận dạng với nút trình đơn hoặc mục nội dung thích hợp.

Cách triển khai onLoadChildren() đơn giản có thể có dạng như sau:

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);
}

Lưu ý: Các đối tượng MediaItem do MediaBrowserService phân phối không được chứa bitmap biểu tượng. Thay vào đó, hãy sử dụng Uri bằng cách gọi setIconUri() khi bạn tạo MediaDescription cho từng mục.

Để biết ví dụ về cách triển khai onLoadChildren(), hãy xem ứng dụng mẫu Universal Android Music Player.

Vòng đời của dịch vụ trình duyệt nội dung nghe nhìn

Hành vi của một dịch vụ Android phụ thuộc vào việc dịch vụ đó có bắt đầu hay liên kết với một hoặc nhiều ứng dụng hay không. Sau khi được tạo, dịch vụ có thể được bắt đầu, liên kết hoặc cả hai. Trong tất cả các trạng thái này, dịch vụ đều hoạt động đầy đủ và có thể thực hiện công việc mà dịch vụ được thiết kế để làm. Điểm khác biệt là thời gian tồn tại của dịch vụ. Một dịch vụ ràng buộc sẽ không bị hủy bỏ cho đến khi tất cả các ứng dụng liên kết của dịch vụ đó hủy liên kết. Một dịch vụ đã bắt đầu có thể bị dừng và huỷ một cách rõ ràng (giả sử dịch vụ đó không còn liên kết với bất kỳ ứng dụng nào).

Khi một MediaBrowser đang chạy trong một hoạt động khác kết nối với một MediaBrowserService, nó sẽ liên kết hoạt động đó với dịch vụ, khiến dịch vụ bị liên kết (nhưng không bắt đầu). Hành vi mặc định này được tích hợp vào lớp MediaBrowserServiceCompat.

Một dịch vụ chỉ được liên kết (và không được khởi động) sẽ bị huỷ khi tất cả các ứng dụng của dịch vụ đó huỷ liên kết. Nếu hoạt động trên giao diện người dùng của bạn bị ngắt kết nối tại thời điểm này, thì dịch vụ sẽ bị huỷ. Điều này không phải là vấn đề nếu bạn chưa phát nhạc. Tuy nhiên, khi quá trình phát bắt đầu, người dùng có thể muốn tiếp tục nghe ngay cả sau khi chuyển đổi ứng dụng. Bạn không muốn hủy bỏ trình phát khi hủy liên kết giao diện người dùng để làm việc với một ứng dụng khác.

Vì lý do này, bạn cần đảm bảo rằng dịch vụ được bắt đầu khi bắt đầu phát bằng cách gọi startService(). Bạn phải dừng một dịch vụ đã bắt đầu một cách rõ ràng, cho dù dịch vụ đó có được liên kết hay không. Điều này đảm bảo rằng trình phát của bạn vẫn tiếp tục hoạt động ngay cả khi hoạt động giao diện người dùng điều khiển huỷ liên kết.

Để dừng một dịch vụ đã bắt đầu, hãy gọi Context.stopService() hoặc stopSelf(). Hệ thống sẽ dừng và huỷ dịch vụ này càng sớm càng tốt. Tuy nhiên, nếu một hoặc nhiều ứng dụng vẫn liên kết với dịch vụ này, thì lệnh gọi dừng dịch vụ sẽ bị trì hoãn cho đến khi tất cả ứng dụng liên kết đều huỷ liên kết.

Vòng đời của MediaBrowserService được kiểm soát bằng cách tạo, số lượng ứng dụng được liên kết với MediaBrowserService và các lệnh gọi mà MediaBrowserService nhận được từ lệnh gọi lại của phiên nội dung nghe nhìn. Tóm tắt:

  • Dịch vụ này được tạo khi bắt đầu phản hồi một nút đa phương tiện hoặc khi một hoạt động liên kết với dịch vụ này (sau khi kết nối thông qua MediaBrowser).
  • Lệnh gọi lại onPlay() của phiên nội dung nghe nhìn phải bao gồm mã gọi startService(). Điều này đảm bảo rằng dịch vụ sẽ bắt đầu và tiếp tục chạy, ngay cả khi tất cả các hoạt động MediaBrowser trên giao diện người dùng được liên kết với dịch vụ đó huỷ liên kết.
  • Lệnh gọi lại onStop() phải gọi stopSelf(). Nếu dịch vụ đã bắt đầu, thì phương thức này sẽ dừng dịch vụ. Ngoài ra, dịch vụ sẽ bị huỷ nếu không có hoạt động nào liên kết với dịch vụ đó. Nếu không, dịch vụ sẽ vẫn được liên kết cho đến khi tất cả các hoạt động của dịch vụ huỷ liên kết. (Nếu nhận được lệnh gọi startService() tiếp theo trước khi dịch vụ bị huỷ, thì lệnh dừng đang chờ xử lý sẽ bị huỷ.)

Biểu đồ quy trình sau đây minh hoạ cách quản lý vòng đời của một dịch vụ. Bộ đếm biến theo dõi số lượng ứng dụng khách được liên kết:

Vòng đời của dịch vụ

Sử dụng thông báo MediaStyle với dịch vụ trên nền trước

Khi đang phát, dịch vụ phải chạy ở nền trước. Điều này cho phép hệ thống biết rằng dịch vụ đang thực hiện một chức năng hữu ích và không nên bị tắt nếu hệ thống sắp hết bộ nhớ. Dịch vụ trên nền trước phải hiển thị một thông báo để người dùng biết về dịch vụ đó và có thể kiểm soát dịch vụ này (nếu muốn). Lệnh gọi lại onPlay() sẽ đặt dịch vụ ở nền trước. (Xin lưu ý rằng đây là một ý nghĩa đặc biệt của "nền trước". Mặc dù Android coi dịch vụ này ở nền trước cho mục đích quản lý tiến trình, nhưng đối với người dùng, trình phát đang phát ở chế độ nền trong khi một số ứng dụng khác hiển thị ở "nền trước" trên màn hình.)

Khi chạy ở nền trước, dịch vụ phải hiển thị một thông báo, tốt nhất là có một hoặc nhiều nút điều khiển hoạt động truyền dữ liệu. Thông báo cũng phải bao gồm thông tin hữu ích trong siêu dữ liệu của phiên.

Tạo và hiển thị thông báo khi trình phát bắt đầu phát. Nơi tốt nhất để thực hiện việc này là bên trong phương thức MediaSessionCompat.Callback.onPlay().

Ví dụ dưới đây sử dụng NotificationCompat.MediaStyle, được thiết kế cho các ứng dụng đa phương tiện. Ví dụ này cho biết cách tạo một thông báo hiển thị siêu dữ liệu và các nút điều khiển thao tác truyền dữ liệu. Phương thức tiện lợi getController() cho phép bạn tạo một trình điều khiển nội dung nghe nhìn ngay từ phiên phát nội dung nghe nhìn.

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());

Khi sử dụng thông báo MediaStyle, hãy lưu ý đến hành vi của các chế độ cài đặt NotificationCompat sau:

  • Khi bạn sử dụng setContentIntent(), dịch vụ của bạn sẽ tự động bắt đầu khi người dùng nhấp vào thông báo. Đây là một tính năng hữu ích.
  • Trong trường hợp "không đáng tin cậy" như màn hình khoá, chế độ hiển thị mặc định cho nội dung thông báo là VISIBILITY_PRIVATE. Có lẽ bạn muốn thấy các nút điều khiển phương tiện trên màn hình khoá, vì vậy VISIBILITY_PUBLIC là lựa chọn phù hợp.
  • Hãy cẩn thận khi bạn đặt màu nền. Trong một thông báo thông thường trên Android phiên bản 5.0 trở lên, màu sắc chỉ được áp dụng cho nền của biểu tượng ứng dụng nhỏ. Nhưng đối với thông báo MediaStyle trước Android 7.0, màu này được dùng cho toàn bộ nền thông báo. Kiểm tra màu nền. Dùng màu dịu mắt và tránh màu quá sáng hoặc màu huỳnh quang.

Các chế độ cài đặt này chỉ có khi bạn đang sử dụng NotificationCompat.MediaStyle:

  • Sử dụng setMediaSession() để liên kết thông báo với phiên tập thể dục. Nhờ đó, các ứng dụng bên thứ ba và thiết bị đồng hành có thể truy cập và kiểm soát phiên.
  • Sử dụng setShowActionsInCompactView() để thêm tối đa 3 thao tác sẽ xuất hiện trong contentView có kích thước tiêu chuẩn của thông báo. (Ở đây, nút tạm dừng được chỉ định.)
  • Trong Android 5.0 (cấp độ API 21) trở lên, bạn có thể vuốt thông báo để dừng trình phát khi dịch vụ không còn chạy ở nền trước nữa. Bạn không thể thực hiện việc này trong các phiên bản trước. Để cho phép người dùng xoá thông báo và dừng phát trước Android 5.0 (API cấp độ 21), bạn có thể thêm một nút huỷ ở góc trên bên phải của thông báo bằng cách gọi setShowCancelButton(true)setCancelButtonIntent().

Khi thêm nút tạm dừng và nút huỷ, bạn sẽ cần một PendingIntent để đính kèm vào thao tác phát. Phương thức MediaButtonReceiver.buildMediaButtonPendingIntent() có nhiệm vụ chuyển đổi một thao tác PlaybackState thành PendingIntent.

Bật tính năng duyệt tìm nội dung nghe nhìn bằng AVRCP

Ngoài các ứng dụng tuỳ chỉnh như Android Auto, lớp Bluetooth của hệ thống cũng đóng vai trò là một ứng dụng khách cho MediaBrowserService để hỗ trợ duyệt danh mục từ xa không dây (AVRCP).

Trên Android 16 và Android 17, nền tảng này yêu cầu các ứng dụng không sử dụng Media3 phải hiển thị một hoạt động cụ thể có bộ lọc ý định để được xác thực khi duyệt xem.

Thêm bộ lọc ý định cụ thể này vào một hoạt động đã xuất trong AndroidManifest.xml. Lưu ý rằng chúng tôi cố ý bỏ qua CATEGORY_DEFAULT để ngăn ứng dụng của bạn xuất hiện trong các trình đơn "Mở bằng" chung cho các tệp âm thanh cục bộ:

<activity
    android:name=".BluetoothValidationActivity"
    android:exported="true"
    android:theme="@android:style/Theme.NoDisplay"
    android:excludeFromRecents="true"
    android:noHistory="true">
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="content" />
    <data android:host="media" />
    <!-- Specific path check used by Bluetooth stack for validation -->
    <data android:pathPrefix="/internal/audio/media/" />
    <data android:mimeType="audio/*" />
  </intent-filter>
</activity>