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 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 được đề xuất của MediaBrowserServiceMediaBrowserServiceCompat. được xác định trong thư viện hỗ trợ Media-compat. Trong toàn bộ trang này, thuật ngữ "MediaBrowserService" đề cập đến một phiên bản của MediaBrowserServiceCompat.

Khởi động phiên phát nội dung đa phương tiệ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() bên dưới minh hoạ các bước sau:

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 máy khách

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

Kiểm soát việc kết nối máy khách 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 ứng dụ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 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 ứng dụng kết nối với MediaSession mà không cần duyệt web, onGetRoot() vẫn phải trả về một giá trị trình duyệt không rỗng, nhưng mã nhận dạng gốc phải đại diện cho một hệ phân cấp nội dung trống.

Quy trình triển khai onGetRoot() thông thường 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 mình. Bạn có thể sử dụng danh sách kiểm soát quyền truy cập (ACL) để chỉ định các kết nối nào được phép hoặc liệt kê các kết nối nào nên bị cấm. Để biết ví dụ về cách triển khai 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 cung cấp nhiều hệ phân cấp nội dung tuỳ thuộc vào loại ứng dụng khách đang thực hiện truy vấn. Cụ thể, Android Auto sẽ 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 ứng dụng 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 giao diện người dùng cục bộ. Phương thức subscribe() gửi lệnh gọi lại onLoadChildren() đến dịch vụ. Lệnh gọi lại 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. 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 các 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 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 được tạo, một 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, dịch vụ này có đầy đủ chức năng và có thể thực hiện công việc mà nó được thiết kế để thực hiện. Sự khác biệt là dịch vụ sẽ tồn tại trong bao lâu. Dịch vụ ràng buộc sẽ không bị huỷ cho đến khi tất cả ứng dụng khách liên kết của dịch vụ đó huỷ 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 MediaBrowser chạy trong một hoạt động khác kết nối với MediaBrowserService, nó sẽ liên kết hoạt động đó với dịch vụ, khiến dịch vụ bị ràng buộc (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ị hủy khi tất cả các ứng dụng khách của dịch vụ đó huỷ liên kết. Nếu hoạt động trên giao diện người dùng bị ngắt kết nối tại thời điểm này, thì dịch vụ sẽ bị huỷ. Đây không phải là vấn đề nếu bạn chưa phát bản nhạc nào. Tuy nhiên, khi bắt đầu phát, người dùng có thể sẽ tiếp tục nghe nội dung ngay cả sau khi chuyển ứng dụng. Bạn không muốn huỷ bỏ người chơi 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ụ đã bắt đầu khi bắt đầu phát bằng cách gọi startService(). Dịch vụ đã bắt đầu phải được dừng một cách rõ ràng, dù có bị ràng buộc hay không. Điều này đảm bảo rằng người chơi sẽ tiếp tục hoạt động ngay cả khi hoạt động trên giao diện người dùng đang kiểm soát bị 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 lại và huỷ dịch vụ ngay khi có thể. Tuy nhiên, nếu một hoặc nhiều ứng dụng vẫn bị ràng buộc với dịch vụ, cuộc gọi ngừng dịch vụ sẽ bị trì hoãn cho đến khi tất cả các ứng dụng khách đó huỷ liên kết.

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

  • Dịch vụ được tạo khi khởi động theo nút nội dung đa phương tiện hoặc khi một hoạt động liên kết với nút đó (sau khi kết nối qua MediaBrowser).
  • Lệnh gọi lại onPlay() 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ụ sẽ bắt đầu và tiếp tục chạy, ngay cả khi mọi hoạt động MediaBrowser trên giao diện người dùng được liên kết với dịch vụ đều bị 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, dịch vụ sẽ dừng lại. 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 bị ràng buộc cho đến khi tất cả các hoạt động của nó huỷ liên kết. (Nếu một lệnh gọi startService() tiếp theo nhận được 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 đâ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 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 đang phát, 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 hiển thị thông báo để người dùng biết và có thể tuỳ ý kiểm soát thông báo đó. Lệnh gọi lại onPlay() sẽ đặt dịch vụ ở nền trước. (Lưu ý rằng đây là ý nghĩa đặc biệt của "nền trước". Mặc dù Android xem dịch vụ ở nền trước là nhằm mục đích quản lý quy trình, nhưng đối với người dùng, người chơi đang phát trong 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ị thông báo, tốt nhất là hiển thị một hoặc nhiều chức năng đ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. 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ụ bên dưới sử dụng NotificationCompat.MediaStyle, được thiết kế cho các ứng dụng đa phương tiện. Hướng dẫn này trình bày cách tạo một thông báo trình bày 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.

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 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, một tính năng hữu ích.
  • Trong tình huống "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. Bạn nên xem các nút điều khiển phương tiện di chuyển trên màn hình khoá, vì vậy, bạn có thể 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 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ỏ. Tuy nhiên, đối với các thông báo MediaStyle trước Android 7.0, màu sắc được sử dụng cho toàn bộ nền thông báo. Kiểm tra màu nền. Để mắt nhẹ nhàng và tránh các màu quá sáng hoặc huỳnh quang.

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

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