Phát trong nền bằng MediaSessionService

Thông thường, bạn nên phát nội dung đa phương tiện khi ứng dụng không chạy ở nền trước. Ví dụ: trình phát nhạc thường tiếp tục phát nhạc khi người dùng khoá thiết bị hoặc đang dùng một ứng dụng khác. Thư viện Media3 cung cấp một loạt giao diện cho phép bạn hỗ trợ tính năng phát trong nền.

Sử dụng MediaSessionService

Để bật tính năng phát trong nền, bạn nên chứa PlayerMediaSession bên trong một Dịch vụ. Điều này cho phép thiết bị tiếp tục cung cấp nội dung đa phương tiện ngay cả khi ứng dụng của bạn không chạy ở nền trước.

MediaSessionService cho phép phiên phát nội dung nghe nhìn chạy riêng biệt với hoạt động của ứng dụng
Hình 1: MediaSessionService cho phép phiên đa phương tiện chạy riêng biệt với hoạt động của ứng dụng

Khi lưu trữ một trình phát bên trong Dịch vụ, bạn nên sử dụng MediaSessionService. Để thực hiện việc này, hãy tạo một lớp mở rộng MediaSessionService và tạo phiên đa phương tiện bên trong lớp đó.

Việc sử dụng MediaSessionService giúp các ứng dụng khách bên ngoài như Trợ lý Google, các nút điều khiển nội dung đa phương tiện của hệ thống, các nút nội dung đa phương tiện trên thiết bị ngoại vi hoặc các thiết bị đồng hành như Wear OS có thể khám phá dịch vụ của bạn, kết nối với dịch vụ đó và điều khiển chế độ phát mà không cần truy cập vào hoạt động giao diện người dùng của ứng dụng. Trên thực tế, có thể có nhiều ứng dụng khách được kết nối với cùng một MediaSessionService cùng một lúc, mỗi ứng dụng có MediaController riêng.

Triển khai vòng đời của dịch vụ

Bạn cần triển khai 2 phương thức vòng đời của dịch vụ:

  • onCreate() được gọi khi bộ điều khiển đầu tiên sắp kết nối và dịch vụ được tạo thực thể và bắt đầu. Đây là nơi tốt nhất để tạo PlayerMediaSession.
  • onDestroy() được gọi khi dịch vụ đang dừng. Bạn cần giải phóng tất cả tài nguyên, bao gồm cả trình phát và phiên.

Bạn có thể tuỳ ý ghi đè onTaskRemoved(Intent) để tuỳ chỉnh những gì xảy ra khi người dùng đóng ứng dụng khỏi các tác vụ gần đây. Theo mặc định, dịch vụ sẽ tiếp tục chạy nếu đang phát và sẽ dừng nếu không.

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null

  // Create your Player and MediaSession in the onCreate lifecycle event
  override fun onCreate() {
    super.onCreate()
    val player = ExoPlayer.Builder(this).build()
    mediaSession = MediaSession.Builder(this, player).build()
  }

  // Remember to release the player and media session in onDestroy
  override fun onDestroy() {
    mediaSession?.run {
      player.release()
      release()
      mediaSession = null
    }
    super.onDestroy()
  }

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

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

  // Remember to release the player and media session in onDestroy
  @Override
  public void onDestroy() {
    mediaSession.getPlayer().release();
    mediaSession.release();
    mediaSession = null;
    super.onDestroy();
  }

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

Ngoài việc tiếp tục phát trong nền, bạn có thể dừng dịch vụ trong mọi trường hợp khi người dùng đóng ứng dụng:

Kotlin

@OptIn(UnstableApi::class)
override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@OptIn(markerClass = UnstableApi.class)
@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

Đối với mọi cách triển khai thủ công khác của onTaskRemoved, bạn có thể sử dụng isPlaybackOngoing() để kiểm tra xem quá trình phát có được coi là đang diễn ra hay không và dịch vụ trên nền trước đã bắt đầu hay chưa.

Cấp quyền truy cập vào phiên đa phương tiện

Ghi đè phương thức onGetSession() để cấp cho các ứng dụng khách khác quyền truy cập vào phiên đa phương tiện mà bạn đã tạo khi dịch vụ được tạo.

Kotlin

class PlaybackService : MediaSessionService() {

  // [...] lifecycle methods omitted

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

class PlaybackService extends MediaSessionService {

  // [...] lifecycle methods omitted

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

Khai báo dịch vụ trong tệp kê khai

Một ứng dụng yêu cầu quyền FOREGROUND_SERVICEFOREGROUND_SERVICE_MEDIA_PLAYBACK để chạy dịch vụ trên nền trước để phát:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

Bạn cũng phải khai báo lớp Service trong tệp kê khai bằng bộ lọc ý định của MediaSessionService và một foregroundServiceType bao gồm mediaPlayback.

<service
    android:name=".PlaybackService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaSessionService"/>
        <action android:name="android.media.browse.MediaBrowserService"/>
    </intent-filter>
</service>

Kiểm soát chế độ phát bằng MediaController

Trong Hoạt động hoặc Mảnh chứa giao diện người dùng trình phát, bạn có thể thiết lập một đường liên kết giữa giao diện người dùng và phiên đa phương tiện bằng MediaController. Giao diện người dùng sử dụng trình điều khiển nội dung nghe nhìn để gửi các lệnh từ giao diện người dùng đến trình phát trong phiên. Hãy xem hướng dẫn MediaController để biết thông tin chi tiết về cách tạo và sử dụng MediaController.

Xử lý các lệnh MediaController

MediaSession nhận các lệnh từ bộ điều khiển thông qua MediaSession.Callback. Việc khởi chạy MediaSession sẽ tạo một cách triển khai mặc định của MediaSession.Callback tự động xử lý tất cả các lệnh mà MediaController gửi đến trình phát.

Thông báo

MediaSessionService sẽ tự động tạo MediaNotification cho bạn. Thông báo này sẽ hoạt động trong hầu hết các trường hợp. Theo mặc định, thông báo đã xuất bản là thông báo MediaStyle luôn được cập nhật thông tin mới nhất từ phiên đa phương tiện và hiển thị các nút điều khiển chế độ phát. MediaNotification nhận biết phiên của bạn và có thể dùng để kiểm soát chế độ phát cho mọi ứng dụng khác được kết nối với cùng một phiên.

Ví dụ: một ứng dụng phát trực tuyến nhạc sử dụng MediaSessionService sẽ tạo MediaNotification hiển thị tiêu đề, nghệ sĩ và ảnh bìa đĩa nhạc cho mục nội dung nghe nhìn hiện đang phát cùng với bộ điều khiển chế độ phát dựa trên cấu hình MediaSession.

Bạn có thể cung cấp siêu dữ liệu bắt buộc trong nội dung đa phương tiện hoặc khai báo siêu dữ liệu đó là một phần của mục nội dung đa phương tiện như trong đoạn mã sau:

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

Vòng đời của thông báo

Thông báo được tạo ngay khi Player có các thực thể MediaItem trong danh sách phát.

Tất cả các bản cập nhật thông báo đều diễn ra tự động dựa trên trạng thái PlayerMediaSession.

Không thể xoá thông báo khi dịch vụ trên nền trước đang chạy. Để xoá thông báo ngay lập tức, bạn phải gọi Player.release() hoặc xoá danh sách phát bằng Player.clearMediaItems().

Nếu trình phát bị tạm dừng, dừng hoặc gặp lỗi trong hơn 10 phút mà không có thêm hoạt động tương tác nào của người dùng, thì dịch vụ sẽ tự động chuyển ra khỏi trạng thái dịch vụ trên nền trước để hệ thống có thể huỷ dịch vụ đó. Bạn có thể triển khai tính năng tiếp tục phát để cho phép người dùng khởi động lại vòng đời của dịch vụ và tiếp tục phát vào một thời điểm sau đó.

Tuỳ chỉnh thông báo

Bạn có thể tuỳ chỉnh siêu dữ liệu về mục hiện đang phát bằng cách sửa đổi MediaItem.MediaMetadata. Nếu muốn cập nhật siêu dữ liệu của một mục hiện có, bạn có thể sử dụng Player.replaceMediaItem để cập nhật siêu dữ liệu mà không làm gián đoạn quá trình phát.

Bạn cũng có thể tuỳ chỉnh một số nút hiển thị trong thông báo bằng cách đặt lựa chọn ưu tiên cho nút nội dung đa phương tiện tuỳ chỉnh cho các nút điều khiển nội dung đa phương tiện của Android. Tìm hiểu thêm về cách tuỳ chỉnh các nút điều khiển nội dung đa phương tiện của Android.

Để tuỳ chỉnh thêm thông báo, hãy tạo MediaNotification.Provider bằng DefaultMediaNotificationProvider.Builder hoặc bằng cách tạo một cách triển khai tuỳ chỉnh của giao diện nhà cung cấp. Thêm nhà cung cấp vào MediaSessionService bằng setMediaNotificationProvider.

Tiếp tục phát

Sau khi MediaSessionService bị chấm dứt và ngay cả sau khi thiết bị khởi động lại, bạn vẫn có thể cung cấp tính năng tiếp tục phát để cho phép người dùng khởi động lại dịch vụ và tiếp tục phát từ nơi dừng trước đó. Theo mặc định, tính năng tiếp tục phát sẽ tắt. Điều này có nghĩa là người dùng không thể tiếp tục phát khi dịch vụ của bạn không chạy. Để chọn sử dụng tính năng này, bạn cần khai báo một trình nhận nút nội dung đa phương tiện và triển khai phương thức onPlaybackResumption.

Khai báo trình nhận nút nội dung đa phương tiện Media3

Bắt đầu bằng cách khai báo MediaButtonReceiver trong tệp kê khai:

<receiver android:name="androidx.media3.session.MediaButtonReceiver"
  android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.MEDIA_BUTTON" />
  </intent-filter>
</receiver>

Triển khai lệnh gọi lại tiếp tục phát

Khi tính năng tiếp tục phát được yêu cầu bởi thiết bị Bluetooth hoặc tính năng tiếp tục của Giao diện người dùng hệ thống Android , phương thức gọi lại onPlaybackResumption() sẽ được gọi.

Kotlin

override fun onPlaybackResumption(
  mediaSession: MediaSession,
  controller: MediaSession.ControllerInfo,
  isForPlayback: Boolean,
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
  val settableFuture = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
  settableFuture.addListener(
    {
      // Your app is responsible for storing the playlist, metadata (like title
      // and artwork) of the current item and the start position to use here.
      val resumptionPlaylist = restorePlaylist()
      settableFuture.set(resumptionPlaylist)
    },
    MoreExecutors.directExecutor(),
  )
  return settableFuture
}

Java

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession, ControllerInfo controller, boolean isForPlayback) {
  SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create();
  settableFuture.addListener(
      () -> {
        // Your app is responsible for storing the playlist, metadata (like title
        // and artwork) of the current item and the start position to use here.
        MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist();
        settableFuture.set(resumptionPlaylist);
      },
      MoreExecutors.directExecutor());
  return settableFuture;
}

Nếu bạn đã lưu trữ các tham số khác như tốc độ phát, chế độ lặp lại hoặc chế độ phát ngẫu nhiên, thì onPlaybackResumption() là nơi thích hợp để định cấu hình trình phát bằng các tham số này trước khi Media3 chuẩn bị trình phát và bắt đầu phát khi lệnh gọi lại hoàn tất.

Phương thức này được gọi trong thời gian khởi động để tạo thông báo tiếp tục của Giao diện người dùng hệ thống Android sau khi khởi động lại thiết bị với isForPlayback được đặt thành false. Đối với thông báo đa dạng thức, bạn nên điền các trường MediaMetadata như titleartworkData hoặc artworkUri của mục hiện tại bằng các giá trị có sẵn cục bộ, vì có thể chưa có quyền truy cập vào mạng. Bạn cũng có thể thêm MediaConstants.EXTRAS_KEY_COMPLETION_STATUSMediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGE vào MediaMetadata.extras để cho biết vị trí phát tiếp tục.

Cấu hình nâng cao của bộ điều khiển và khả năng tương thích ngược

Một trường hợp phổ biến là sử dụng MediaController trong giao diện người dùng ứng dụng để kiểm soát chế độ phát và hiển thị danh sách phát. Đồng thời, phiên này được hiển thị cho các ứng dụng khách bên ngoài như các nút điều khiển nội dung đa phương tiện của Android và Trợ lý trên thiết bị di động hoặc TV, Wear OS cho đồng hồ và Android Auto trong ô tô. Ứng dụng minh hoạ phiên Media3 là một ví dụ về ứng dụng triển khai trường hợp như vậy.

Các ứng dụng khách bên ngoài này có thể sử dụng các API như MediaControllerCompat của thư viện AndroidX cũ hoặc android.media.session.MediaController của nền tảng Android. Media3 hoàn toàn tương thích ngược với thư viện cũ và cung cấp khả năng tương tác với API nền tảng Android.

Xác định bộ điều khiển đáng tin cậy

Mọi ứng dụng đều có thể cố gắng kết nối với phiên hoặc thư viện nội dung đa phương tiện của bạn. Nếu muốn hạn chế quyền truy cập vào các bộ điều khiển hệ thống, bộ điều khiển có quyền kiểm soát nội dung đa phương tiện và ứng dụng của riêng bạn, bạn có thể sử dụng ControllerInfo.isTrusted() để kiểm tra quyền truy cập cơ bản. Ngoài ra, bạn có thể xác định các bộ điều khiển cụ thể hơn như bộ điều khiển thông báo nội dung đa phương tiện hoặc bộ điều khiển Android Auto như mô tả trong các phần sau.

Sử dụng bộ điều khiển thông báo nội dung đa phương tiện

Điều quan trọng là bạn phải hiểu rằng các bộ điều khiển nền tảng và cũ này chia sẻ cùng trạng thái và không thể tuỳ chỉnh khả năng hiển thị theo bộ điều khiển (ví dụ: PlaybackState.getActions()PlaybackState.getCustomActions() có sẵn). Bạn có thể sử dụng bộ điều khiển thông báo nội dung đa phương tiện để định cấu hình tập hợp trạng thái trong phiên đa phương tiện của nền tảng cho khả năng tương thích với các bộ điều khiển nền tảng và cũ này.

Ví dụ: một ứng dụng có thể cung cấp cách triển khai MediaSession.Callback.onConnect() để đặt các lệnh có sẵn và lựa chọn ưu tiên cho nút nội dung đa phương tiện dành riêng cho phiên nền tảng như sau:

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  if (session.isMediaNotificationController(controller)) {
    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 button preferences and commands to configure the platform session.
    return immediateFuture(
      AcceptedResultBuilder(session)
        .setMediaButtonPreferences(listOf(seekBackButton, seekForwardButton))
        .setAvailablePlayerCommands(playerCommands)
        .build()
    )
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    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 button preferences and commands to configure the platform session.
    return immediateFuture(
        new AcceptedResultBuilder(session)
            .setMediaButtonPreferences(ImmutableList.of(seekBackButton, seekForwardButton))
            .setAvailablePlayerCommands(playerCommands)
            .build());
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session).build());
}

Cho phép Android Auto gửi các lệnh tuỳ chỉnh

Khi sử dụng MediaLibraryService và để hỗ trợ Android Auto bằng ứng dụng di động, bộ điều khiển Android Auto yêu cầu các lệnh có sẵn thích hợp, nếu không, Media3 sẽ từ chối các lệnh tuỳ chỉnh đến từ bộ điều khiển đó:

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build()
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
      AcceptedResultBuilder(session).setAvailableSessionCommands(sessionCommands).build()
    )
  }
  // Default commands for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build();
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
        new AcceptedResultBuilder(session)
            .setAvailableSessionCommands(sessionCommands)
            .build());
  }
  // Default commands for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session).build());
}

Ứng dụng minh họa phiên có một mô-đun ô tô, mô-đun này minh hoạ khả năng hỗ trợ cho Automotive OS yêu cầu một tệp APK riêng.