Phát trong nền bằng MediaSessionService

Bạn thường muốn phát nội dung nghe nhì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 sử 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ợ chế độ phát trong nền.

Sử dụng MediaSessionService

Để bật tính năng phát trong nền, bạn cần chứa PlayerMediaSession bên trong một Dịch vụ riêng biệt. Nhờ đó, thiết bị có thể tiếp tục phân phát nội dung nghe nhìn ngay cả khi bạn không đóng ứng dụng nền trước.

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

Khi lưu trữ người chơi bên trong một 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 phát nội dung đa phương tiện bên trong lớp đó.

Việc sử dụng MediaSessionService sẽ giúp các khách hàng bên ngoài như Google có thể Bạn có thể dùng Trợ lý, nút điều khiển nội dung nghe nhìn của hệ thống hoặc các thiết bị đồng hành như Wear OS để khám phá dịch vụ của bạn, kết nối với dịch vụ đó và điều khiển quá trình phát lại, tất cả mà không cần truy cập vào hoạt động trên 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ào cùng một MediaSessionService cùng lúc, mỗi ứng dụng đều có MediaController.

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

Bạn cần triển khai 3 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. Đó là nơi tốt nhất để xây dựng PlayerMediaSession.
  • onTaskRemoved(Intent) được gọi khi người dùng đóng ứng dụng khỏi công việc gần đây. Nếu quá trình phát đang diễn ra, ứng dụng có thể chọn tiếp tục sử dụng dịch vụ đang chạy ở nền trước. Nếu trình phát bị tạm dừng, dịch vụ sẽ không ở chế độ nền trước và cần phải dừng.
  • onDestroy() được gọi khi dịch vụ đang bị 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.

Kotlin

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

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

  // The user dismissed the app from the recent tasks
  override fun onTaskRemoved(rootIntent: Intent?) {
    val player = mediaSession?.player!!
    if (!player.playWhenReady
        || player.mediaItemCount == 0
        || player.playbackState == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf()
    }
  }

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

Java

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

  // The user dismissed the app from the recent tasks
  @Override
  public void onTaskRemoved(@Nullable Intent rootIntent) {
    Player player = mediaSession.getPlayer();
    if (!player.getPlayWhenReady()
        || player.getMediaItemCount() == 0
        || player.getPlaybackState() == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf();
    }
  }

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

Thay vì tiếp tục phát ở chế độ nền, ứng dụng có thể ngừng dịch vụ trong mọi trường hợp khi người dùng đóng ứng dụng:

Kotlin

override fun onTaskRemoved(rootIntent: Intent?) {
  val player = mediaSession.player
  if (player.playWhenReady) {
    // Make sure the service is not in foreground.
    player.pause()
  }
  stopSelf()
}

Java

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  Player player = mediaSession.getPlayer();
  if (player.getPlayWhenReady()) {
    // Make sure the service is not in foreground.
    player.pause();
  }
  stopSelf();
}

Cấp quyền truy cập vào phiên phát nội dung nghe nhìn

Ghi đè phương thức onGetSession() để cấp cho các ứng dụng khác quyền truy cập vào nội dung nghe nhìn của bạn được tạo khi dịch vụ được tạo.

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null
  // [...] lifecycle methods omitted

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

Java

public class PlaybackService extends MediaSessionService {
  private MediaSession mediaSession = null;
  // [...] lifecycle methods omitted

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

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

Ứng dụng cần có quyền để chạy dịch vụ trên nền trước. Thêm quyền FOREGROUND_SERVICE vào tệp kê khai và nếu bạn nhắm đến API 34 trở lên, hãy thêm FOREGROUND_SERVICE_MEDIA_PLAYBACK:

<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 trong tổng số MediaSessionService.

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

Bạn phải xác định một foregroundServiceType bao gồm mediaPlayback khi ứng dụng của bạn đang chạy trên một thiết bị Android 10 (API cấp 29) trở lên.

Điều khiển chế độ phát bằng MediaController

Trong Hoạt động hoặc Mảnh chứa giao diện người dùng của 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 phát nội dung đ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 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 Tạo 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 trên giao diện người dùng

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 giá trị mặc định Việc triển khai MediaSession.Callback tự động xử lý tất cả lệnh MediaController gửi đến người chơi của bạn.

Thông báo

MediaSessionService sẽ tự động tạo một MediaNotification cho bạn và 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à một thông báo MediaStyle luôn được cập nhật thông tin mới nhất từ phiên phát nội dung nghe nhìn và hiển thị các nút điều khiển phát. MediaNotification nhận biết phiên của bạn và có thể dùng để điều khiển quá trình phát cho bất kỳ ứng dụng nào khác được kết nối với cùng một phiên.

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

Siêu dữ liệu bắt buộc có thể được cung cấp trong phương tiện truyền thông hoặc được khai báo trong 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();

Các ứng dụng có thể tuỳ chỉnh các nút lệnh của các chế độ điều khiển Android Media. Đọc thêm về cách tuỳ chỉnh Android Media kiểm soát.

Tuỳ chỉnh thông báo

Để tuỳ chỉnh thông báo, hãy tạo một MediaNotification.Provider bằng DefaultMediaNotificationProvider.Builder hoặc bằng cách tạo một phương thức 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

Nút nội dung đa phương tiện là các nút phần cứng có trên thiết bị Android và thiết bị ngoại vi khác trên thiết bị, chẳng hạn như nút phát hoặc nút tạm dừng trên tai nghe Bluetooth. Đa phương tiện 3 xử lý đầu vào bằng nút nội dung nghe nhìn cho bạn khi dịch vụ đang chạy.

Khai báo trình thu nhận nút nội dung nghe nhìn Media3

Media3 có một API để cho phép người dùng tiếp tục phát lại sau khi ứng dụng đã chấm dứt và ngay cả sau khi thiết bị đã ngừng đã khởi động lại. Theo mặc định, tính năng tiếp tục phát sẽ bị 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, hãy 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 thiết bị Bluetooth hoặc thiết bị Bluetooth yêu cầu tiếp tục phát Tính năng tiếp tục giao diện người dùng của hệ thống Android, onPlaybackResumption() sẽ được gọi.

Kotlin

override fun onPlaybackResumption(
    mediaSession: MediaSession,
    controller: ControllerInfo
): ListenableFuture<MediaItemsWithStartPosition> {
  val settable = SettableFuture.create<MediaItemsWithStartPosition>()
  scope.launch {
    // Your app is responsible for storing the playlist and the start position
    // to use here
    val resumptionPlaylist = restorePlaylist()
    settable.set(resumptionPlaylist)
  }
  return settable
}

Java

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession,
    ControllerInfo controller
) {
  SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create();
  settableFuture.addListener(() -> {
    // Your app is responsible for storing the playlist 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 phù 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.

Cấu hình tay điều khiển nâng cao 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 phát và hiển thị danh sách phát. Đồng thời, phiên hoạt động đó sẽ hiển thị cho các ứng dụng bên ngoài như chế độ điều khiển nội dung nghe nhìn trên Android và Trợ lý trên thiết bị di động hoặc TV, Wear OS cho đồng hồ và Android Auto trên ô tô. Ứng dụng minh hoạ phiên Media3 là ví dụ về một ứng dụng triển khai tình huống như vậy.

Các ứng dụng 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 khung 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 khung Android.

Sử dụng trình điều khiển thông báo nội dung nghe nhìn

Quan trọng là bạn phải hiểu rằng những bộ điều khiển cũ hoặc bộ điều khiển khung này sẽ đọc cùng các giá trị trong khung PlaybackState.getActions()PlaybackState.getCustomActions(). Để xác định các thao tác và thao tác tuỳ chỉnh của phiên khung, ứng dụng có thể sử dụng trình điều khiển thông báo nội dung nghe nhìn đặt các lệnh có sẵn và bố cục tuỳ chỉnh. Dịch vụ này kết nối trình điều khiển thông báo nội dung đa phương tiện với phiên của bạn và phiên này sử dụng ConnectionResult do onConnect() của lệnh gọi lại trả về để định cấu hình các hành động và hành động tuỳ chỉnh của phiên khung.

Trong trường hợp chỉ dành cho thiết bị di động, ứ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à bố cục tuỳ chỉnh dành riêng cho phiên khung như sau:

Kotlin

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  if (session.isMediaNotificationController(controller)) {
    val sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(customCommandSeekBackward)
        .add(customCommandSeekForward)
        .build()
    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 layout and available commands to configure the legacy/framework session.
    return AcceptedResultBuilder(session)
      .setCustomLayout(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default custom layout for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Java

@Override
public ConnectionResult onConnect(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS
            .buildUpon()
            .add(customCommandSeekBackward)
            .add(customCommandSeekForward)
            .build();
    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 layout and available commands to configure the legacy/framework session.
    return new AcceptedResultBuilder(session)
        .setCustomLayout(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands without default custom layout for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

Uỷ quyền cho Android Auto gửi lệnh tuỳ chỉnh

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

Kotlin

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
      .add(customCommandSeekBackward)
      .add(customCommandSeekForward)
      .build()
  if (session.isMediaNotificationController(controller)) {
    // [...] See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available session commands to accept incoming custom commands from Auto.
    return AcceptedResultBuilder(session)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default custom layout for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Java

@Override
public ConnectionResult onConnect(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS
          .buildUpon()
          .add(customCommandSeekBackward)
          .add(customCommandSeekForward)
          .build();
  if (session.isMediaNotificationController(controller)) {
    // [...] See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands without default custom layout for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

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