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

Ứng dụng phải khai báo MediaBrowserService bằng 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, đó là "MediaPlaybackService".

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

Lưu ý: Cách triển khai MediaBrowserService được đề xuất là MediaBrowserServiceCompat. được xác định trong phần tử thư viện hỗ trợ media-compat. Xuyên suốt trang này, thuật ngữ "MediaBrowserService" đề cập đến một thực thể của trong tổng số MediaBrowserServiceCompat.

Khởi chạy phiên 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ụ phải thực hiện các bước sau:

onCreate() dưới đâ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ý kết nối khách hàng

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

Kiểm soát kết nối ứng dụ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 trả về giá trị rỗng, kết nối bị từ chối.

Để cho phép khách hàng kết nối với dịch vụ của bạn và duyệt qua nội dung đa phương tiện của dịch vụ đó, onGetRoot() phải trả về một BrowserRoot không rỗng, là một ID gốc đại diện cho hệ thống phân cấp nội dung của bạn.

Để cho phép ứng dụng kết nối với MediaSession mà không cần duyệt qua, onGetRoot() vẫn phải trả về một BrowserRoot không rỗng, nhưng ID gốc phải đại diện cho một hệ thống phân cấp nội dung trống.

Quy trình 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ào 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ê một cách khác những kết nối bị cấm. Để xem ví dụ về cách triển khai ACL cho phép các kết nối cụ thể, hãy xem PackageValidator trong một gói trong Universal Android Music Player ứng dụng mẫu.

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 kiểu khách hàng nào đ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ự động. Bạn có thể xem clientPackageName tại thời điểm kết nối để xác định ứng dụng loại và trả về một BrowserRoot khác tuỳ thuộc vào ứng dụng (hoặc rootHints nếu có).

Đang trao đổi nội dung với onLoadChildren()

Sau khi kết nối, ứng dụng có thể truyền tải 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ộ của 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ụ, lệnh này sẽ trả về danh sách các đối tượng MediaBrowser.MediaItem.

Mỗi MediaItem có một chuỗi mã nhận dạng duy nhất. Đây là một mã thông báo mờ. Khi muốn mở trình đơn con hoặc phát một mục, ứng dụng sẽ chuyển mã nhận dạng này. 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 đơn giản của onLoadChildren() 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 mỗi 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 dịch vụ trình duyệt nội dung đa phương tiệ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 hay nhiều ứng dụng. Sau khi tạo, dịch vụ có thể được bắt đầu, liên kết hoặc cả hai. Ở tất cả các trạng thái này, ứng dụng có đầy đủ chức năng và có thể thực hiện công việc được thiết kế để thực hiện. Điểm khác biệt là khoảng thời gian dịch vụ sẽ tồn tại. Dịch vụ ràng buộc sẽ không bị huỷ bỏ cho đến khi tất cả ứng dụng ràng buộc của dịch vụ đó huỷ liên kết. Dịch vụ đã bắt đầu có thể bị dừng và huỷ bỏ một cách rõ ràng (giả sử dịch vụ đó không còn bị ràng buộc với bất kỳ ứng dụng nào).

Khi một MediaBrowser 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ụ được liên kết (nhưng chưa bắt đầu). Hành vi mặc định này được tích hợp vào lớp MediaBrowserServiceCompat.

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

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

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

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

  • Dịch vụ được tạo khi khởi động để 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 hoạt động đó (sau khi kết nối qua MediaBrowser).
  • Lệnh gọi lại onPlay() của phiên phát nội dung đa phương tiện phải bao gồm mã gọi startService(). Điều này đảm bảo dịch vụ bắt đầu và tiếp tục chạy, ngay cả khi tất cả hoạt động MediaBrowser của 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() sẽ gọi stopSelf(). Nếu dịch vụ đã được bắt đầu, thì dịch vụ sẽ dừng lại. Ngoài ra, dịch vụ sẽ bị huỷ bỏ 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 bị ràng buộc cho đến khi tất cả các hoạt động của dịch vụ đó được huỷ liên kết. (Nếu lệnh gọi startService() tiếp theo được nhận trước khi dịch vụ bị huỷ, thì lệnh dừng đang chờ xử lý sẽ bị huỷ.)

Sơ đồ quy trình sau 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 bị ràng buộc:

Vòng đời dịch vụ

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

Khi một dịch vụ đang hoạt động, dịch vụ đó phải chạy ở nền trước. Điều này cho 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 cho thấy thông báo để người dùng biết về dịch vụ đó và có thể kiểm soát nếu muốn. Lệnh gọi lại onPlay() sẽ đưa dịch vụ lên 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 xem xét dịch vụ ở nền trước nhằm mục đích quản lý quy trình, nhưng đối với người dùng, trình phát đang phát trong nền còn một số ứng dụng khác chỉ hiển thị ở "nền trước" trên màn hình).

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

Tạo và hiển thị thông báo khi người chơi bắt đầu chơi. Bạn nên thực hiện việc này bên trong phương thức MediaSessionCompat.Callback.onPlay().

Ví dụ bên dưới sử dụng phương pháp NotificationCompat.MediaStyle, được thiết kế cho các ứng dụng đa phương tiện. Hướng dẫn 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 phương thức điều khiển truyền tải. Phương thức tiện lợi getController() cho phép bạn tạo trình điều khiển nội dung nghe nhìn ngay trong phiên phát nội dung nghe nhìn của mình.

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 ý hành vi của các thông báo này Chế độ cài đặt NotificationCompat:

  • Khi bạn sử dụng setContentIntent(), dịch vụ của bạn sẽ tự động bắt đầu khi có thông báo được nhấp vào, một tính năng tiện dụng.
  • Trong trường hợp "không đáng tin cậy" tình hình hiện tại chẳng hạn như màn hình khoá, chế độ hiển thị mặc định cho nội dung thông báo là VISIBILITY_PRIVATE. Bạn có thể muốn thấy các nút điều khiển chuyển giao trên màn hình khoá, vì vậy bạn có thể sử dụng VISIBILITY_PUBLIC.
  • Hãy cẩn thận khi đặt màu nền. Trong một thông báo thông thường ở Android phiên bản 5.0 trở lên, màu 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 được dùng cho toàn bộ nền thông báo. Kiểm tra màu nền của bạn. Tìm nhẹ nhàng cho mắt và tránh các màu quá sáng hoặc màu huỳnh quang.

Các chế độ cài đặt này chỉ dùng đượ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 của bạn. Chế độ này cho phép các ứng dụng bên thứ ba và các thiết bị đồng hành để truy cập và kiểm soát phiên.
  • Sử dụng setShowActionsInCompactView() để thêm tối đa 3 hành động sẽ xuất hiện contentView có kích thước chuẩn của thông báo. (Ở đây, nút tạm dừng đã chỉ định.)
  • Trong Android 5.0 (API cấp 21) trở lên, bạn có thể vuốt thông báo sang bên để dừng trình phát sau khi dịch vụ không còn chạy ở nền trước. Bạn không thể thực hiện 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 phiên bản 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 bằng cách gọi setShowCancelButton(true)setCancelButtonIntent().

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