Kiểm soát và quảng cáo tính năng phát bằng MediaSession

Phiên phát nội dung đa phương tiện cung cấp cách thức toàn cầu để tương tác với âm thanh hoặc video trình phát. Trong Media3, trình phát mặc định là lớp ExoPlayer, lớp này triển khai giao diện Player. Việc kết nối phiên nội dung nghe nhìn với trình phát sẽ cho phép ứng dụng để quảng cáo việc phát nội dung nghe nhìn bên ngoài và nhận lệnh phát từ nguồn bên ngoài.

Các lệnh có thể bắt nguồn từ các nút vật lý như nút phát trên tai nghe hoặc điều khiển từ xa TV. Chúng cũng có thể đến từ các ứng dụng khách có trình điều khiển nội dung nghe nhìn, chẳng hạn như hướng dẫn "tạm dừng" thành Trợ lý Google. Nội dung nghe nhìn phiên này uỷ quyền các lệnh này cho trình phát của ứng dụng đa phương tiện.

Thời điểm chọn một phiên phát nội dung nghe nhìn

Khi triển khai MediaSession, bạn sẽ cho phép người dùng điều khiển chế độ phát:

  • Thông qua tai nghe. Thường có các nút hoặc tương tác chạm người dùng có thể biểu diễn trên tai nghe để phát hoặc tạm dừng nội dung nghe nhìn hoặc chuyển đến hoặc bản nhạc trước đó.
  • Bằng cách trò chuyện với Trợ lý Google. Một cách nói phổ biến là nói "OK Google, hãy tạm dừng" để tạm dừng mọi nội dung nghe nhìn đang phát trên thiết bị này.
  • Thông qua đồng hồ Wear OS của họ. Điều này cho phép bạn dễ dàng truy cập vào các bộ điều khiển chế độ phát thông thường khi phát trên điện thoại.
  • Thông qua mục Điều khiển nội dung nghe nhìn. Băng chuyền này hiển thị các chế độ điều khiển cho từng phiên nội dung nghe nhìn đang chạy.
  • Trên TV. Cho phép thao tác bằng các nút phát vật lý, phát trên nền tảng điều khiển và quản lý nguồn điện (ví dụ: nếu TV, loa thanh hoặc bộ thu A/V tắt hoặc đầu vào được chuyển, thì quá trình phát sẽ dừng trong ứng dụng).
  • Và bất kỳ quy trình bên ngoài nào khác cần ảnh hưởng đến quá trình phát.

Điều này rất hữu ích trong nhiều trường hợp sử dụng. Cụ thể, bạn nên cân nhắc sử dụng MediaSession khi:

  • Bạn đang phát trực tuyến nội dung video dài, chẳng hạn như phim hoặc chương trình truyền hình trực tuyến.
  • Bạn đang phát trực tuyến nội dung âm thanh dài, chẳng hạn như podcast hoặc nhạc danh sách phát.
  • Bạn đang xây dựng một ứng dụng truyền hình.

Tuy nhiên, không phải trường hợp sử dụng nào cũng phù hợp với MediaSession. Bạn nên chỉ sử dụng Player trong các trường hợp sau:

  • Bạn đang hiển thị nội dung dạng ngắn nơi người dùng tương tác và tương tác với nhau là vô cùng quan trọng.
  • Không có video nào đang hoạt động, chẳng hạn như người dùng đang cuộn qua một danh sách và nhiều video được hiển thị trên màn hình cùng một lúc.
  • Bạn đang phát video giới thiệu hoặc giải thích một lần mà bạn mong muốn người dùng tích cực xem.
  • Nội dung của bạn nhạy cảm về quyền riêng tư và bạn không muốn các quy trình bên ngoài truy cập siêu dữ liệu nội dung nghe nhìn (ví dụ: chế độ ẩn danh trong trình duyệt)

Nếu trường hợp sử dụng của bạn không thuộc bất kỳ trường hợp nào nêu trên, hãy cân nhắc xem bạn có ứng dụng của bạn tiếp tục phát khi người dùng không tích cực tương tác với nội dung. Nếu câu trả lời là có, bạn có thể muốn chọn MediaSession. Nếu câu trả lời là không, bạn có thể muốn sử dụng Player thay thế.

Tạo một phiên nội dung nghe nhìn

Một phiên nội dung đa phương tiện diễn ra cùng với trình phát mà trình phát đó quản lý. Bạn có thể tạo một phiên phát nội dung nghe nhìn với đối tượng ContextPlayer. Bạn nên tạo và khởi chạy một phiên nội dung nghe nhìn khi cần, chẳng hạn như onStart() hoặc Phương thức vòng đời onResume() của Activity, Fragment hoặc onCreate() của Service sở hữu phiên nội dung đa phương tiện và trình phát được liên kết.

Để tạo một phiên phát nội dung đa phương tiện, hãy khởi chạy Player và cung cấp phiên đó cho MediaSession.Builder thích điều này:

Kotlin

val player = ExoPlayer.Builder(context).build()
val mediaSession = MediaSession.Builder(context, player).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();

Tự động xử lý trạng thái

Thư viện Media3 tự động cập nhật phiên phát nội dung nghe nhìn bằng cách sử dụng trạng thái của người chơi. Do đó, bạn không cần phải xử lý thủ công việc ánh xạ từ giữa các phiên.

Đây là điểm thay đổi so với cách tiếp cận cũ, trong đó bạn cần tạo và duy trì PlaybackStateCompat độc lập với chính trình phát, ví dụ: chỉ ra bất kỳ lỗi nào.

Mã phiên duy nhất

Theo mặc định, MediaSession.Builder sẽ tạo một phiên có chuỗi trống dưới dạng mã phiên. Điều này là đủ nếu ứng dụng dự định chỉ tạo một phiên bản thể thao. Đây là trường hợp phổ biến nhất.

Nếu một ứng dụng muốn quản lý nhiều phiên bản cùng lúc, thì ứng dụng phải đảm bảo rằng mã nhận dạng phiên của mỗi phiên là duy nhất. Mã phiên có thể khi tạo phiên bằng MediaSession.Builder.setId(String id).

Nếu bạn thấy IllegalStateException gây ra sự cố cho ứng dụng của mình kèm theo lỗi nhắn tin IllegalStateException: Session ID must be unique. ID= thì nó sẽ là có thể một phiên đã được tạo bất ngờ trước khi một phiên hoạt động được tạo trước đó bản sao có cùng mã nhận dạng đã được huỷ bỏ. Để tránh các phiên bị rò rỉ bởi lỗi lập trình, những trường hợp như vậy được phát hiện và thông báo bằng cách gửi một ngoại lệ.

Cấp quyền kiểm soát cho các ứng dụng khách khác

Phiên phát nội dung đa phương tiện là chìa khoá để điều khiển quá trình phát. Công cụ này cho phép bạn định tuyến các lệnh từ nguồn bên ngoài gửi đến trình phát thực hiện công việc phát nội dung đa phương tiện. Các nguồn này có thể là các nút vật lý như nút phát trên tai nghe hoặc điều khiển từ xa TV hoặc lệnh gián tiếp chẳng hạn như hướng dẫn "tạm dừng" thành Trợ lý Google. Tương tự, bạn nên cấp quyền truy cập vào ứng dụng Android hệ thống hỗ trợ các chế độ kiểm soát thông báo và màn hình khoá hoặc cho Wear OS để bạn có thể điều khiển quá trình phát trên mặt đồng hồ. Khách hàng bên ngoài có thể sử dụng trình điều khiển nội dung nghe nhìn để ra lệnh phát cho ứng dụng đa phương tiện. Đây là mà phiên phát nội dung đa phương tiện của bạn nhận được, sau đó uỷ quyền truy cập các lệnh cho trình phát nội dung đa phương tiện.

Sơ đồ minh hoạ sự tương tác giữa MediaSession và MediaController.
Hình 1: Trình điều khiển nội dung nghe nhìn hỗ trợ việc truyền từ các nguồn bên ngoài sang phiên phát nội dung đa phương tiện.

Khi bộ điều khiển sắp kết nối với phiên phát nội dung nghe nhìn của bạn, onConnect() được gọi. Bạn có thể sử dụng ControllerInfo được cung cấp để quyết định có chấp nhận hay không hoặc từ chối yêu cầu. Xem ví dụ về cách chấp nhận yêu cầu kết nối trong phần Khai báo các lệnh hiện có.

Sau khi kết nối, tay điều khiển có thể gửi lệnh phát cho phiên này. Chiến lược phát hành đĩa đơn sau đó uỷ quyền các lệnh đó cho người chơi. Phát và danh sách phát các lệnh được xác định trong giao diện Player sẽ được tự động xử lý bởi phiên hoạt động.

Các phương thức gọi lại khác cho phép bạn xử lý, chẳng hạn như yêu cầu các lệnh phát tuỳ chỉnhsửa đổi danh sách phát). Các lệnh gọi lại này cũng bao gồm đối tượng ControllerInfo tương tự như vậy để bạn có thể sửa đổi cách bạn phản hồi từng yêu cầu theo từng đơn vị kiểm soát.

Sửa đổi danh sách phát

Một phiên nội dung nghe nhìn có thể trực tiếp sửa đổi danh sách phát của trình phát như được giải thích trong thời gian Hướng dẫn dành cho danh sách phát của ExoPlayer. Các đơn vị kiểm soát cũng có thể sửa đổi danh sách phát nếu COMMAND_SET_MEDIA_ITEM hoặc COMMAND_CHANGE_MEDIA_ITEMS Bộ điều khiển có thể sử dụng.

Khi thêm mục mới vào danh sách phát, trình phát thường yêu cầu MediaItemURI đã xác định để làm cho chúng có thể phát được. Theo mặc định, các mặt hàng mới được thêm sẽ được chuyển tiếp tự động vào các phương thức của trình phát như player.addMediaItem nếu các phương thức đó có URI được xác định.

Nếu muốn tuỳ chỉnh các thực thể MediaItem được thêm vào trình phát, bạn có thể ghi đè onAddMediaItems(). Đây là bước cần thiết khi bạn muốn hỗ trợ các bộ điều khiển yêu cầu nội dung đa phương tiện mà không có URI được xác định. Thay vào đó, MediaItem thường có một hoặc nhiều trường sau đây được đặt để mô tả nội dung nghe nhìn được yêu cầu:

  • MediaItem.id: Mã nhận dạng chung cho nội dung nghe nhìn.
  • MediaItem.RequestMetadata.mediaUri: URI yêu cầu có thể sử dụng một tuỳ chỉnh và không nhất thiết phải mà người chơi trực tiếp chơi được.
  • MediaItem.RequestMetadata.searchQuery: Truy vấn tìm kiếm văn bản, chẳng hạn như từ Trợ lý Google.
  • MediaItem.MediaMetadata: Siêu dữ liệu có cấu trúc, chẳng hạn như "tiêu đề" hoặc "nghệ sĩ".

Để có thêm lựa chọn tuỳ chỉnh cho danh sách phát hoàn toàn mới, bạn có thể ghi đè bổ sung onSetMediaItems() cho phép bạn xác định mục bắt đầu và vị trí trong danh sách phát. Ví dụ: bạn có thể mở rộng một mục được yêu cầu cho toàn bộ danh sách phát và hướng dẫn trình phát bắt đầu tại chỉ mục của mục được yêu cầu ban đầu. Đáp mẫu triển khai onSetMediaItems() có tính năng này trong ứng dụng minh hoạ phiên hoạt động.

Quản lý bố cục tuỳ chỉnh và lệnh tuỳ chỉnh

Các phần sau đây mô tả cách quảng cáo bố cục tuỳ chỉnh của các nút lệnh cho ứng dụng khách và cho phép bộ điều khiển gửi các lệnh.

Xác định bố cục tuỳ chỉnh của phiên

Để cho các ứng dụng khách biết bạn muốn hiển thị bộ điều khiển chế độ phát nào hãy đặt bố cục tuỳ chỉnh của phiên khi tạo MediaSession trong phương thức onCreate() của .

Kotlin

override fun onCreate() {
  super.onCreate()

  val likeButton = CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build()
  val favoriteButton = CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(SessionCommand(SAVE_TO_FAVORITES, Bundle()))
    .build()

  session =
    MediaSession.Builder(this, player)
      .setCallback(CustomMediaSessionCallback())
      .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
      .build()
}

Java

@Override
public void onCreate() {
  super.onCreate();

  CommandButton likeButton = new CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build();
  CommandButton favoriteButton = new CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
    .build();

  Player player = new ExoPlayer.Builder(this).build();
  mediaSession =
      new MediaSession.Builder(this, player)
          .setCallback(new CustomMediaSessionCallback())
          .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
          .build();
}

Khai báo trình phát có sẵn và lệnh tuỳ chỉnh

Ứng dụng đa phương tiện có thể xác định các lệnh tuỳ chỉnh có thể dùng trong một bố cục tuỳ chỉnh. Ví dụ: bạn có thể muốn triển khai các nút cho phép để lưu một mục nội dung đa phương tiện vào danh sách mục yêu thích. MediaController sẽ gửi lệnh tuỳ chỉnh và MediaSession.Callback sẽ nhận các lệnh đó.

Bạn có thể xác định những lệnh trong phiên tuỳ chỉnh nào dùng được cho MediaController khi thiết bị này kết nối với phiên phát nội dung nghe nhìn của bạn. Bạn đạt được điều này bằng cách ghi đè MediaSession.Callback.onConnect(). Định cấu hình và trả về tập hợp các lệnh có sẵn khi chấp nhận yêu cầu kết nối từ MediaController trong phương thức gọi lại onConnect:

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  override fun onConnect(
    session: MediaSession,
    controller: MediaSession.ControllerInfo
  ): MediaSession.ConnectionResult {
    val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build()
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  @Override
  public ConnectionResult onConnect(
    MediaSession session,
    ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
}

Để nhận các yêu cầu lệnh tuỳ chỉnh từ MediaController, hãy ghi đè phương thức Phương thức onCustomCommand() trong Callback.

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: MediaSession.ControllerInfo,
    customCommand: SessionCommand,
    args: Bundle
  ): ListenableFuture<SessionResult> {
    if (customCommand.customAction == SAVE_TO_FAVORITES) {
      // Do custom logic here
      saveToFavorites(session.player.currentMediaItem)
      return Futures.immediateFuture(
        SessionResult(SessionResult.RESULT_SUCCESS)
      )
    }
    ...
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  ...
  @Override
  public ListenableFuture<SessionResult> onCustomCommand(
    MediaSession session, 
    ControllerInfo controller,
    SessionCommand customCommand,
    Bundle args
  ) {
    if(customCommand.customAction.equals(SAVE_TO_FAVORITES)) {
      // Do custom logic here
      saveToFavorites(session.getPlayer().getCurrentMediaItem());
      return Futures.immediateFuture(
        new SessionResult(SessionResult.RESULT_SUCCESS)
      );
    }
    ...
  }
}

Bạn có thể theo dõi xem trình điều khiển nội dung nghe nhìn nào đang đưa ra yêu cầu bằng cách sử dụng Thuộc tính packageName của đối tượng MediaSession.ControllerInfo được truyền vào phương thức Callback. Điều này cho phép bạn điều chỉnh để phản hồi một lệnh nhất định nếu lệnh đó bắt nguồn từ hệ thống, ứng dụng riêng hoặc các ứng dụng khách khác.

Cập nhật bố cục tuỳ chỉnh sau khi người dùng tương tác

Sau khi xử lý một lệnh tuỳ chỉnh hoặc bất kỳ tương tác nào khác với người chơi, bạn có thể muốn cập nhật bố cục hiển thị trong giao diện người dùng trình điều khiển. Ví dụ điển hình là nút bật tắt thay đổi biểu tượng sau khi kích hoạt hành động được liên kết bằng nút này. Để cập nhật bố cục, bạn có thể sử dụng MediaSession.setCustomLayout:

Kotlin

val removeFromFavoritesButton = CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(SessionCommand(REMOVE_FROM_FAVORITES, Bundle()))
  .build()
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

CommandButton removeFromFavoritesButton = new CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle()))
  .build();
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton));

Tuỳ chỉnh hành vi của lệnh phát

Cách tuỳ chỉnh hành vi của một lệnh được xác định trong giao diện Player, chẳng hạn như dưới dạng play() hoặc seekToNext(), hãy gói Player trong một ForwardingPlayer.

Kotlin

val player = ExoPlayer.Builder(context).build()

val forwardingPlayer = object : ForwardingPlayer(player) {
  override fun play() {
    // Add custom logic
    super.play()
  }

  override fun setPlayWhenReady(playWhenReady: Boolean) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady)
  }
}

val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();

ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) {
  @Override
  public void play() {
    // Add custom logic
    super.play();
  }

  @Override
  public void setPlayWhenReady(boolean playWhenReady) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady);
  }
};

MediaSession mediaSession =
  new MediaSession.Builder(context, forwardingPlayer).build();

Để biết thêm thông tin về ForwardingPlayer, hãy xem hướng dẫn về ExoPlayer trên Tuỳ chỉnh.

Xác định trình điều khiển yêu cầu của một lệnh người chơi

Khi lệnh gọi đến phương thức Player do MediaController bắt nguồn, bạn có thể xác định nguồn gốc xuất xứ bằng MediaSession.controllerForCurrentRequest và nhận ControllerInfo cho yêu cầu hiện tại:

Kotlin

class CallerAwareForwardingPlayer(player: Player) :
  ForwardingPlayer(player) {

  override fun seekToNext() {
    Log.d(
      "caller",
      "seekToNext called from package ${session.controllerForCurrentRequest?.packageName}"
    )
    super.seekToNext()
  }
}

Java

public class CallerAwareForwardingPlayer extends ForwardingPlayer {
  public CallerAwareForwardingPlayer(Player player) {
    super(player);
  }

  @Override
  public void seekToNext() {
    Log.d(
        "caller",
        "seekToNext called from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    super.seekToNext();
  }
}

Nút phản hồi với nội dung nghe nhìn

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 , chẳng hạn như nút phát/tạm dừng trên tai nghe Bluetooth. Xử lý Media3 cho bạn khi họ đến phiên và gọi phương thức Player thích hợp trên trình phát trong phiên.

Một ứng dụng có thể ghi đè hành vi mặc định bằng cách ghi đè MediaSession.Callback.onMediaButtonEvent(Intent). Trong trường hợp như vậy, ứng dụng có thể/cần tự xử lý tất cả chi tiết API cụ thể.

Xử lý và báo cáo lỗi

Có 2 loại lỗi mà một phiên phát ra và báo cáo cho bộ điều khiển. Lỗi nghiêm trọng báo cáo lỗi phát lại kỹ thuật của phiên trình phát làm gián đoạn quá trình phát. Các lỗi nghiêm trọng sẽ được báo cáo cho bộ điều khiển tự động khi chúng xảy ra. Lỗi không nghiêm trọng là lỗi không liên quan đến kỹ thuật hoặc chính sách không làm gián đoạn quá trình phát và được gửi đến bộ điều khiển bằng ứng dụng theo cách thủ công.

Lỗi phát lại nghiêm trọng

Lỗi phát lại nghiêm trọng được trình phát báo cáo cho phiên, sau đó đã báo cáo cho bộ điều khiển để gọi qua Player.Listener.onPlayerError(PlaybackException)Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException)

Trong trường hợp như vậy, trạng thái phát sẽ chuyển đổi thành STATE_IDLEMediaController.getPlaybackError() trả về PlaybackException đã gây ra chuyển đổi. Bộ điều khiển có thể kiểm tra PlayerException.errorCode để biết thông tin về lý do xảy ra lỗi.

Để có khả năng tương tác, một lỗi nghiêm trọng sẽ được sao chép vào PlaybackStateCompat của phiên hoạt động trên nền tảng bằng cách chuyển trạng thái sang STATE_ERROR và cài đặt mã lỗi và thông báo theo PlaybackException.

Tuỳ chỉnh lỗi nghiêm trọng

Để cung cấp thông tin có ý nghĩa và được bản địa hoá cho người dùng, mã lỗi, và thông báo lỗi bổ sung của lỗi phát nghiêm trọng có thể được tuỳ chỉnh bằng bằng cách sử dụng ForwardingPlayer khi tạo phiên:

Kotlin

val forwardingPlayer = ErrorForwardingPlayer(player)
val session = MediaSession.Builder(context, forwardingPlayer).build()

Java

Player forwardingPlayer = new ErrorForwardingPlayer(player);
MediaSession session =
    new MediaSession.Builder(context, forwardingPlayer).build();

Trình phát chuyển tiếp đăng ký Player.Listener cho người chơi thực và chặn các lệnh gọi lại báo cáo lỗi. Một Sau đó, PlaybackException được uỷ quyền cho các trình nghe được đăng ký trên trình phát chuyển tiếp. Để làm được điều này, trình phát chuyển tiếp ghi đè Player.addListenerPlayer.removeListener để có quyền truy cập vào trình nghe để gửi mã lỗi, thông báo hoặc dữ liệu bổ sung tuỳ chỉnh:

Kotlin

class ErrorForwardingPlayer(private val context: Context, player: Player) :
  ForwardingPlayer(player) {

  private val listeners: MutableList<Player.Listener> = mutableListOf()

  private var customizedPlaybackException: PlaybackException? = null

  init {
    player.addListener(ErrorCustomizationListener())
  }

  override fun addListener(listener: Player.Listener) {
    listeners.add(listener)
  }

  override fun removeListener(listener: Player.Listener) {
    listeners.remove(listener)
  }

  override fun getPlayerError(): PlaybackException? {
    return customizedPlaybackException
  }

  private inner class ErrorCustomizationListener : Player.Listener {

    override fun onPlayerErrorChanged(error: PlaybackException?) {
      customizedPlaybackException = error?.let { customizePlaybackException(it) }
      listeners.forEach { it.onPlayerErrorChanged(customizedPlaybackException) }
    }

    override fun onPlayerError(error: PlaybackException) {
      listeners.forEach { it.onPlayerError(customizedPlaybackException!!) }
    }

    private fun customizePlaybackException(
      error: PlaybackException,
    ): PlaybackException {
      val buttonLabel: String
      val errorMessage: String
      when (error.errorCode) {
        PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
          buttonLabel = context.getString(R.string.err_button_label_restart_stream)
          errorMessage = context.getString(R.string.err_msg_behind_live_window)
        }
        // Apps can customize further error messages by adding more branches.
        else -> {
          buttonLabel = context.getString(R.string.err_button_label_ok)
          errorMessage = context.getString(R.string.err_message_default)
        }
      }
      val extras = Bundle()
      extras.putString("button_label", buttonLabel)
      return PlaybackException(errorMessage, error.cause, error.errorCode, extras)
    }

    override fun onEvents(player: Player, events: Player.Events) {
      listeners.forEach {
        it.onEvents(player, events)
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

Java

private static class ErrorForwardingPlayer extends ForwardingPlayer {

  private final Context context;
  private List<Player.Listener> listeners;
  @Nullable private PlaybackException customizedPlaybackException;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
    listeners = new ArrayList<>();
    player.addListener(new ErrorCustomizationListener());
  }

  @Override
  public void addListener(Player.Listener listener) {
    listeners.add(listener);
  }

  @Override
  public void removeListener(Player.Listener listener) {
    listeners.remove(listener);
  }

  @Nullable
  @Override
  public PlaybackException getPlayerError() {
    return customizedPlaybackException;
  }

  private class ErrorCustomizationListener implements Listener {

    @Override
    public void onPlayerErrorChanged(@Nullable PlaybackException error) {
      customizedPlaybackException =
          error != null ? customizePlaybackException(error, context) : null;
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerErrorChanged(customizedPlaybackException);
      }
    }

    @Override
    public void onPlayerError(PlaybackException error) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerError(checkNotNull(customizedPlaybackException));
      }
    }

    private PlaybackException customizePlaybackException(
        PlaybackException error, Context context) {
      String buttonLabel;
      String errorMessage;
      switch (error.errorCode) {
        case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW:
          buttonLabel = context.getString(R.string.err_button_label_restart_stream);
          errorMessage = context.getString(R.string.err_msg_behind_live_window);
          break;
        // Apps can customize further error messages by adding more case statements.
        default:
          buttonLabel = context.getString(R.string.err_button_label_ok);
          errorMessage = context.getString(R.string.err_message_default);
          break;
      }
      Bundle extras = new Bundle();
      extras.putString("button_label", buttonLabel);
      return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras);
    }

    @Override
    public void onEvents(Player player, Events events) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onEvents(player, events);
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

Lỗi không nghiêm trọng

Hệ thống có thể gửi các lỗi không nghiêm trọng không bắt nguồn từ một trường hợp ngoại lệ về kỹ thuật bởi một ứng dụng cho tất cả hoặc cho một đơn vị kiểm soát cụ thể:

Kotlin

val sessionError = SessionError(
  SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
  context.getString(R.string.error_message_authentication_expired),
)

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError)

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
mediaSession.mediaNotificationControllerInfo?.let {
  mediaSession.sendError(it, sessionError)
}

Java

SessionError sessionError = new SessionError(
    SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
    context.getString(R.string.error_message_authentication_expired));

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError);

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
ControllerInfo mediaNotificationControllerInfo =
    mediaSession.getMediaNotificationControllerInfo();
if (mediaNotificationControllerInfo != null) {
  mediaSession.sendError(mediaNotificationControllerInfo, sessionError);
}

Một lỗi không nghiêm trọng gửi đến trình điều khiển thông báo nội dung nghe nhìn sẽ được sao chép vào PlaybackStateCompat phiên hoạt động trên nền tảng. Do đó, chỉ có mã lỗi và thông báo lỗi sẽ được đặt thành PlaybackStateCompat tương ứng, trong khi PlaybackStateCompat.state chưa được thay đổi thành STATE_ERROR.

Nhận lỗi không nghiêm trọng

MediaController gặp lỗi không nghiêm trọng khi triển khai MediaController.Listener.onError:

Kotlin

val future = MediaController.Builder(context, sessionToken)
  .setListener(object : MediaController.Listener {
    override fun onError(controller: MediaController, sessionError: SessionError) {
      // Handle nonfatal error.
    }
  })
  .buildAsync()

Java

MediaController.Builder future =
    new MediaController.Builder(context, sessionToken)
        .setListener(
            new MediaController.Listener() {
              @Override
              public void onError(MediaController controller, SessionError sessionError) {
                // Handle nonfatal error.
              }
            });