ควบคุมและโฆษณาการเล่นโดยใช้ MediaSession

เซสชันสื่อเป็นวิธีสากลในการโต้ตอบกับโปรแกรมเล่นเสียงหรือวิดีโอ ใน Media3 โปรแกรมเล่นเริ่มต้นคือคลาส ExoPlayer ซึ่งใช้อินเทอร์เฟซ Player การเชื่อมต่อเซสชันสื่อกับโปรแกรมเล่นช่วยให้แอปสามารถโฆษณาการเล่นสื่อภายนอกและรับคำสั่งการเล่นจากแหล่งที่มาภายนอกได้

คำสั่งอาจมาจากปุ่มจริง เช่น ปุ่มเล่นบนหูฟังหรือรีโมตทีวี นอกจากนี้ คำสั่งอาจมาจากแอปไคลเอ็นต์ที่มีตัวควบคุมสื่อ เช่น คำสั่ง "หยุดชั่วคราว" ไปยัง Google Assistant เซสชันสื่อจะมอบสิทธิ์คำสั่งเหล่านี้ให้กับโปรแกรมเล่นของแอปสื่อ

กรณีที่ควรเลือกเซสชันสื่อ

เมื่อใช้ MediaSession คุณจะอนุญาตให้ผู้ใช้ควบคุมการเล่นได้ดังนี้

  • ผ่านหูฟัง โดยมักจะมีปุ่มหรือการโต้ตอบด้วยการสัมผัสที่ผู้ใช้สามารถดำเนินการกับหูฟังเพื่อเล่นหรือหยุดสื่อชั่วคราว หรือไปยังแทร็กถัดไปหรือก่อนหน้า
  • โดยพูดกับ Google Assistant รูปแบบที่พบบ่อยคือการพูดว่า "Ok Google หยุดชั่วคราว" เพื่อหยุดสื่อที่เล่นอยู่ในอุปกรณ์ชั่วคราว
  • ผ่านนาฬิกา Wear OS วิธีนี้ช่วยให้ผู้ใช้เข้าถึงการควบคุมการเล่นที่ใช้บ่อยที่สุดได้ง่ายขึ้นขณะเล่นบนโทรศัพท์
  • ผ่านตัวควบคุมสื่อ ภาพสไลด์นี้จะแสดงตัวควบคุมสำหรับเซสชันสื่อที่เล่นอยู่แต่ละรายการ
  • บนทีวี อนุญาตให้ดำเนินการกับปุ่มเล่นจริง การควบคุมการเล่นบนแพลตฟอร์ม และการจัดการพลังงาน (เช่น หากทีวี ซาวด์บาร์ หรือรีซีฟเวอร์ A/V ปิดอยู่หรือมีการสลับอินพุต การเล่นในแอปควรหยุดลง)
  • และกระบวนการภายนอกอื่นๆ ที่ต้องส่งผลต่อการเล่น

ซึ่งเหมาะสําหรับ Use Case หลายรายการ โดยเฉพาะอย่างยิ่ง คุณควรพิจารณาใช้ MediaSession ในกรณีต่อไปนี้

  • คุณกำลังสตรีมเนื้อหาวิดีโอแบบยาว เช่น ภาพยนตร์หรือรายการทีวีสด
  • คุณกำลังสตรีมเนื้อหาเสียงแบบยาว เช่น พอดแคสต์หรือเพลย์ลิสต์เพลง
  • คุณกำลังสร้างแอปทีวี

อย่างไรก็ตาม โปรดทราบว่าบาง Use Case อาจไม่เหมาะกับ MediaSession คุณอาจต้อง ใช้เฉพาะ Player ในกรณีต่อไปนี้

  • คุณแสดงเนื้อหาแบบสั้น ซึ่งการมีส่วนร่วมและการโต้ตอบของผู้ใช้มีความสําคัญ
  • ไม่มีวิดีโอที่ใช้งานอยู่เพียงรายการเดียว เช่น ผู้ใช้กำลังเลื่อนดูรายการ และวิดีโอหลายรายการแสดงบนหน้าจอพร้อมกัน
  • คุณกำลังเล่นวิดีโอแนะนำหรืออธิบายแบบครั้งเดียว ซึ่งคุณคาดหวังว่าผู้ใช้จะดูอย่างตั้งใจ
  • เนื้อหาของคุณมีความละเอียดอ่อนด้านความเป็นส่วนตัวและคุณไม่ต้องการให้กระบวนการภายนอกเข้าถึงข้อมูลเมตาของสื่อ (เช่น โหมดไม่ระบุตัวตนในเบราว์เซอร์)

หาก Use Case ของคุณไม่ตรงกับ Use Case ที่ระบุไว้ข้างต้น ให้พิจารณาว่าคุณอนุญาตให้แอปเล่นเนื้อหาต่อไปได้ไหมเมื่อผู้ใช้ไม่ได้มีส่วนร่วมกับเนื้อหา หากคำตอบคือใช่ คุณอาจต้องเลือกตัวเลือก MediaSession หากคำตอบคือไม่ คุณอาจต้องใช้ Playerแทน

สร้างเซสชันสื่อ

เซสชันสื่อจะแสดงอยู่ควบคู่ไปกับโปรแกรมเล่นที่จัดการ คุณสามารถสร้างเซสชันสื่อด้วยออบเจ็กต์ Context และ Player คุณควรสร้างและเริ่มต้นเซสชันสื่อเมื่อจำเป็น เช่น วิธีการเกี่ยวกับวงจรชีวิตของ onStart() หรือ onResume() ของ Activity หรือ Fragment หรือวิธีการ onCreate() ของ Service ที่เป็นเจ้าของเซสชันสื่อและโปรแกรมเล่นที่เชื่อมโยง

หากต้องการสร้างเซสชันสื่อ ให้เริ่มต้น Player แล้วส่งให้กับ MediaSession.Builder ดังนี้

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

การจัดการสถานะอัตโนมัติ

ห้องสมุด Media3 จะอัปเดตเซสชันสื่อโดยอัตโนมัติโดยใช้สถานะของเพลเยอร์ คุณจึงไม่ต้องจัดการการแมปจากเพลเยอร์ไปยังเซสชันด้วยตนเอง

วิธีนี้แตกต่างจากแนวทางเดิมที่คุณต้องสร้างและดูแลรักษา PlaybackStateCompat แยกต่างหากจากตัวโปรแกรมเล่นเอง เช่น เพื่อบ่งบอกข้อผิดพลาด

รหัสเซสชันที่ไม่ซ้ำกัน

โดยค่าเริ่มต้น MediaSession.Builder จะสร้างเซสชันโดยใช้สตริงว่างเป็นรหัสเซสชัน ซึ่งเพียงพอแล้วหากแอปตั้งใจจะสร้างอินสแตนซ์เซสชันเดียวเท่านั้น ซึ่งเป็นกรณีที่พบบ่อยที่สุด

หากแอปต้องการจัดการอินสแตนซ์เซสชันหลายรายการพร้อมกัน แอปต้องตรวจสอบว่ารหัสเซสชันของแต่ละเซสชันไม่ซ้ำกัน คุณสามารถตั้งค่ารหัสเซสชันเมื่อสร้างเซสชันด้วย MediaSession.Builder.setId(String id)

หากเห็นIllegalStateExceptionแอปขัดข้องพร้อมข้อความแสดงข้อผิดพลาด IllegalStateException: Session ID must be unique. ID= แสดงว่าอาจมีการสร้างเซสชันโดยไม่คาดคิดก่อนที่จะมีการปล่อยอินสแตนซ์ที่มีรหัสเดียวกันซึ่งสร้างขึ้นก่อนหน้านี้ ระบบจะตรวจหาและแจ้งเตือนกรณีดังกล่าวด้วยการยกเว้นเพื่อหลีกเลี่ยงไม่ให้เซสชันรั่วไหลเนื่องจากข้อผิดพลาดในการเขียนโปรแกรม

มอบสิทธิ์ควบคุมให้กับไคลเอ็นต์รายอื่น

เซสชันสื่อเป็นกุญแจสำคัญในการควบคุมการเล่น ซึ่งช่วยให้คุณกำหนดเส้นทางคำสั่งจากแหล่งที่มาภายนอกไปยังโปรแกรมเล่นที่ทำหน้าที่เล่นสื่อได้ แหล่งที่มาเหล่านี้อาจเป็นปุ่มจริง เช่น ปุ่มเล่นบนชุดหูฟังหรือรีโมตคอนโทรลทีวี หรือคำสั่งโดยอ้อม เช่น บอกให้ Google Assistant "หยุดชั่วคราว" ในทำนองเดียวกัน คุณอาจต้องให้สิทธิ์เข้าถึงระบบ Android เพื่ออำนวยความสะดวกในการควบคุมการแจ้งเตือนและหน้าจอล็อก หรือให้สิทธิ์เข้าถึงนาฬิกา Wear OS เพื่อให้คุณควบคุมการเล่นจากหน้าปัดได้ ลูกค้าภายนอกสามารถใช้ตัวควบคุมสื่อเพื่อออกคำสั่งการเล่นไปยังแอปสื่อได้ โดยเซสชันสื่อจะรับคำสั่งเหล่านี้และส่งต่อไปยังโปรแกรมเล่นสื่อ

แผนภาพที่แสดงการโต้ตอบระหว่าง MediaSession กับ MediaController
รูปที่ 1: ตัวควบคุมสื่อช่วยให้ส่งคำสั่งจากแหล่งที่มาภายนอกไปยังเซสชันสื่อได้ง่าย

เมื่อตัวควบคุมกำลังจะเชื่อมต่อกับเซสชันสื่อ ระบบจะเรียกใช้เมธอด onConnect() คุณสามารถใช้ ControllerInfo ที่ระบุไว้เพื่อตัดสินใจว่าจะยอมรับหรือปฏิเสธคําขอ ดูตัวอย่างการยอมรับคําขอเชื่อมต่อได้ในส่วนประกาศคำสั่งที่ใช้ได้

หลังจากเชื่อมต่อแล้ว รีโมตคอนโทรลจะส่งคำสั่งการเล่นไปยังเซสชันได้ จากนั้นเซสชันจะมอบสิทธิ์คำสั่งเหล่านั้นไปยังโปรแกรมเล่น เซสชันจะจัดการคำสั่งการเล่นและเพลย์ลิสต์ที่กําหนดไว้ในอินเทอร์เฟซ Player โดยอัตโนมัติ

วิธีการเรียกกลับอื่นๆ ช่วยให้คุณจัดการกับคำขอต่างๆ ได้ เช่น คําสั่งการเล่นที่กําหนดเอง และการแก้ไขเพลย์ลิสต์) แคล็กแบ็กเหล่านี้มีออบเจ็กต์ ControllerInfo ด้วยเช่นกันเพื่อให้คุณแก้ไขวิธีตอบสนองต่อคําขอแต่ละรายการตามตัวควบคุมแต่ละรายการได้

แก้ไขเพลย์ลิสต์

เซสชันสื่อสามารถแก้ไขเพลย์ลิสต์ของเพลเยอร์ได้โดยตรงตามที่อธิบายไว้ในคู่มือ ExoPlayer สำหรับเพลย์ลิสต์ ผู้ควบคุมยังแก้ไขเพลย์ลิสต์ได้หาก COMMAND_SET_MEDIA_ITEM หรือ COMMAND_CHANGE_MEDIA_ITEMS พร้อมใช้งานสำหรับผู้ควบคุม

เมื่อเพิ่มรายการใหม่ลงในเพลย์ลิสต์ โดยทั่วไปแล้วโปรแกรมเล่นจะต้องใช้MediaItem อินสแตนซ์ที่มี URI ที่กําหนดเพื่อให้เล่นได้ โดยค่าเริ่มต้น ระบบจะส่งต่อรายการที่เพิ่มใหม่โดยอัตโนมัติไปยังเมธอดของโปรแกรมเล่น เช่น player.addMediaItem หากมีการกําหนด URI

หากต้องการปรับแต่งอินสแตนซ์ MediaItem ที่เพิ่มลงในเพลเยอร์ คุณสามารถ override onAddMediaItems() ขั้นตอนนี้จำเป็นเมื่อคุณต้องการรองรับตัวควบคุมที่ขอสื่อโดยไม่มี URI ที่กําหนดไว้ แต่โดยทั่วไป MediaItem จะมีการตั้งค่าช่องต่อไปนี้อย่างน้อย 1 ช่องเพื่ออธิบายสื่อที่ขอ

  • MediaItem.id: รหัสทั่วไปที่ระบุสื่อ
  • MediaItem.RequestMetadata.mediaUri: URI คำขอที่อาจใช้สคีมาที่กำหนดเองและไม่จำเป็นต้องเล่นได้โดยตรงโดยโปรแกรมเล่น
  • MediaItem.RequestMetadata.searchQuery: คําค้นหาที่เป็นข้อความ เช่น จาก Google Assistant
  • MediaItem.MediaMetadata: ข้อมูลเมตาที่มีโครงสร้าง เช่น "ชื่อ" หรือ "ศิลปิน"

หากต้องการตัวเลือกการปรับแต่งเพิ่มเติมสำหรับเพลย์ลิสต์ใหม่ทั้งหมด คุณสามารถลบล้างonSetMediaItems()เพิ่มเติมได้ ซึ่งจะช่วยให้คุณกำหนดรายการเริ่มต้นและตำแหน่งในเพลย์ลิสต์ได้ เช่น คุณสามารถขยายรายการที่ขอรายการเดียวเป็นทั้งเพลย์ลิสต์และสั่งให้โปรแกรมเล่นเริ่มต้นที่ดัชนีของรายการที่ขอในตอนแรก คุณสามารถดูตัวอย่างการใช้งาน onSetMediaItems() กับฟีเจอร์นี้ได้ในแอปสาธิตเซสชัน

จัดการเลย์เอาต์ที่กำหนดเองและคำสั่งที่กำหนดเอง

ส่วนต่อไปนี้จะอธิบายวิธีแสดงโฆษณาเลย์เอาต์ที่กำหนดเองของปุ่มคำสั่งที่กำหนดเองไปยังแอปไคลเอ็นต์ และมอบสิทธิ์ให้ตัวควบคุมส่งคำสั่งที่กำหนดเอง

กําหนดเลย์เอาต์ที่กําหนดเองของเซสชัน

หากต้องการระบุแอปไคลเอ็นต์ว่าคุณต้องการแสดงการควบคุมการเล่นใดต่อผู้ใช้ ให้ตั้งค่าเลย์เอาต์ที่กำหนดเองของเซสชันเมื่อสร้าง MediaSession ในเมธอด onCreate() ของบริการ

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

ประกาศเพลเยอร์และคำสั่งที่กำหนดเองที่ใช้ได้

แอปพลิเคชันสื่อสามารถกำหนดคำสั่งที่กำหนดเองได้ เช่น สามารถใช้ในเลย์เอาต์ที่กำหนดเอง เช่น คุณอาจต้องการใช้ปุ่มที่อนุญาตให้ผู้ใช้บันทึกรายการสื่อลงในรายการรายการโปรด MediaController จะส่งคําสั่งที่กําหนดเองและ MediaSession.Callback จะรับคําสั่งเหล่านั้น

คุณสามารถกำหนดคำสั่งเซสชันที่กำหนดเองซึ่งพร้อมใช้งานสำหรับ MediaController เมื่อเชื่อมต่อกับเซสชันสื่อ ซึ่งทำได้โดย overriding MediaSession.Callback.onConnect() กำหนดค่าและแสดงชุดคำสั่งที่ใช้ได้เมื่อยอมรับคำขอการเชื่อมต่อจาก MediaController ในเมธอด Callback ของ 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();
  }
}

หากต้องการรับคําขอคําสั่งที่กําหนดเองจาก MediaController ให้ลบล้างวิธี onCustomCommand() ใน 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)
      );
    }
    ...
  }
}

คุณสามารถติดตามว่าตัวควบคุมสื่อใดกำลังส่งคำขอโดยใช้พร็อพเพอร์ตี้ packageName ของออบเจ็กต์ MediaSession.ControllerInfo ที่ส่งไปยังเมธอด Callback ซึ่งจะช่วยให้คุณปรับแต่งลักษณะการทํางานของแอปเพื่อตอบสนองต่อคําสั่งหนึ่งๆ ได้ หากคําสั่งนั้นมาจากระบบ แอปของคุณเอง หรือแอปไคลเอ็นต์อื่นๆ

อัปเดตเลย์เอาต์ที่กําหนดเองหลังจากการโต้ตอบของผู้ใช้

หลังจากจัดการคําสั่งที่กําหนดเองหรือการโต้ตอบอื่นๆ กับโปรแกรมเล่นแล้ว คุณอาจต้องอัปเดตเลย์เอาต์ที่แสดงใน UI ของคอนโทรลเลอร์ ตัวอย่างทั่วไปคือปุ่มเปิด/ปิดที่เปลี่ยนไอคอนหลังจากเรียกการดำเนินการที่เชื่อมโยงกับปุ่มนี้ หากต้องการอัปเดตเลย์เอาต์ คุณสามารถใช้ตัวเลือกต่อไปนี้ 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));

ปรับแต่งลักษณะการทำงานของคำสั่งการเล่น

หากต้องการปรับแต่งลักษณะการทํางานของคําสั่งที่กําหนดไว้ในอินเทอร์เฟซ Player เช่น play() หรือ seekToNext() ให้ใส่ Player ใน ForwardingSimpleBasePlayer ก่อนส่งไปยัง MediaSession

Kotlin

val player = (logic to build a Player instance)

val forwardingPlayer = object : ForwardingSimpleBasePlayer(player) {
  // Customizations
}

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

Java

ExoPlayer player = (logic to build a Player instance)

ForwardingSimpleBasePlayer forwardingPlayer =
    new ForwardingSimpleBasePlayer(player) {
      // Customizations
    };

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

ดูข้อมูลเพิ่มเติมเกี่ยวกับ ForwardingSimpleBasePlayer ได้ที่คู่มือ ExoPlayer เกี่ยวกับการปรับแต่ง

ระบุตัวควบคุมที่ส่งคําขอของคําสั่งผู้เล่น

เมื่อการเรียกใช้เมธอด Player มาจาก MediaController คุณสามารถระบุแหล่งที่มาของต้นทางด้วย MediaSession.controllerForCurrentRequest และรับ ControllerInfo สําหรับคําขอปัจจุบันได้ ดังนี้

Kotlin

class CallerAwarePlayer(player: Player) :
  ForwardingSimpleBasePlayer(player) {

  override fun handleSeek(
    mediaItemIndex: Int,
    positionMs: Long,
    seekCommand: Int,
  ): ListenableFuture<*> {
    Log.d(
      "caller",
      "seek operation from package ${session.controllerForCurrentRequest?.packageName}",
    )
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand)
  }
}

Java

public class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  public CallerAwarePlayer(Player player) {
    super(player);
  }

  @Override
  protected ListenableFuture<?> handleSeek(
        int mediaItemIndex, long positionMs, int seekCommand) {
    Log.d(
        "caller",
        "seek operation from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    return super.handleSeek(mediaItemIndex, positionMs, seekCommand);
  }
}

ตอบสนองต่อปุ่มสื่อ

ปุ่มสื่อคือปุ่มฮาร์ดแวร์ที่มีอยู่ในอุปกรณ์ Android และอุปกรณ์ต่อพ่วงอื่นๆ เช่น ปุ่มเล่น/หยุดชั่วคราวบนชุดหูฟังบลูทูธ Media3 จะจัดการเหตุการณ์ปุ่มสื่อให้คุณเมื่อเหตุการณ์มาถึงเซสชันและเรียกใช้Playerเมธอดที่เหมาะสมในโปรแกรมเล่นเซสชัน

แอปสามารถลบล้างลักษณะการทำงานเริ่มต้นได้โดยลบล้าง MediaSession.Callback.onMediaButtonEvent(Intent) ในกรณีเช่นนี้ แอปสามารถ/ต้องจัดการข้อมูลเฉพาะทั้งหมดของ API ด้วยตนเอง

การจัดการและการรายงานข้อผิดพลาด

ข้อผิดพลาดที่เซสชันแสดงและรายงานไปยังตัวควบคุมมี 2 ประเภท ข้อผิดพลาดร้ายแรงจะรายงานการเล่นทางเทคนิคที่ไม่สําเร็จของโปรแกรมเล่นเซสชันที่ขัดจังหวะการเล่น ระบบจะรายงานข้อผิดพลาดร้ายแรงไปยังตัวควบคุมโดยอัตโนมัติเมื่อเกิดข้อผิดพลาด ข้อผิดพลาดที่ไม่ร้ายแรงคือข้อผิดพลาดที่ไม่เกี่ยวข้องกับเทคนิคหรือนโยบาย ซึ่งจะไม่ขัดจังหวะการเล่นและแอปพลิเคชันจะส่งไปยังตัวควบคุมด้วยตนเอง

ข้อผิดพลาดร้ายแรงในการเล่น

โปรแกรมเล่นจะรายงานข้อผิดพลาดร้ายแรงในการเล่นไปยังเซสชัน จากนั้นจะรายงานไปยังตัวควบคุมเพื่อเรียกผ่าน Player.Listener.onPlayerError(PlaybackException) และ Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException)

ในกรณีเช่นนี้ สถานะการเล่นจะเปลี่ยนเป็น STATE_IDLE และ MediaController.getPlaybackError() จะแสดงผล PlaybackException ที่ทําให้เกิดการเปลี่ยนสถานะ ผู้ควบคุมสามารถตรวจสอบ PlayerException.errorCode เพื่อดูข้อมูลเกี่ยวกับสาเหตุของข้อผิดพลาด

สําหรับความสามารถในการทํางานร่วมกัน ระบบจะทําซ้ำข้อผิดพลาดร้ายแรงไปยัง PlaybackStateCompat ของเซสชันแพลตฟอร์มโดยเปลี่ยนสถานะเป็น STATE_ERROR และตั้งค่ารหัสและข้อความแสดงข้อผิดพลาดตาม PlaybackException

การปรับแต่งข้อผิดพลาดร้ายแรง

หากต้องการระบุข้อมูลที่แปลแล้วและมีความหมายให้แก่ผู้ใช้ คุณสามารถปรับแต่งรหัสข้อผิดพลาด ข้อความแสดงข้อผิดพลาด และข้อมูลเพิ่มเติมเกี่ยวกับข้อผิดพลาดในการเล่นร้ายแรงได้โดยใช้ ForwardingPlayer เมื่อสร้างเซสชัน ดังนี้

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

โปรแกรมเล่นที่ส่งต่อจะลงทะเบียน Player.Listener กับโปรแกรมเล่นจริง และขัดขวางการเรียกกลับที่รายงานข้อผิดพลาด จากนั้นระบบจะมอบสิทธิ์PlaybackExceptionที่กําหนดเองให้แก่ผู้ฟังที่ลงทะเบียนไว้ในโปรแกรมเล่นการส่งต่อ ผู้เล่นการส่งต่อต้องลบล้าง Player.addListener และ Player.removeListener เพื่อให้เข้าถึงตัวฟังได้เพื่อส่งรหัสข้อผิดพลาด ข้อความ หรือข้อมูลเพิ่มเติมที่กําหนดเอง

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.
  }
}

ข้อผิดพลาดที่ไม่ร้ายแรง

แอปสามารถส่งข้อผิดพลาดที่ไม่ร้ายแรงซึ่งไม่ได้มาจากข้อยกเว้นทางเทคนิคไปยังตัวควบคุมทั้งหมดหรือตัวควบคุมที่เฉพาะเจาะจงได้ ดังนี้

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

ระบบจะทําซ้ำข้อผิดพลาดที่ไม่ร้ายแรงซึ่งส่งไปยังตัวควบคุมการแจ้งเตือนสื่อไปยัง PlaybackStateCompat ของเซสชันแพลตฟอร์ม ดังนั้น ระบบจะตั้งค่าเฉพาะรหัสข้อผิดพลาดและข้อความแสดงข้อผิดพลาดเป็น PlaybackStateCompat ตามลำดับ ส่วน PlaybackStateCompat.state จะไม่มีการเปลี่ยนแปลงเป็น STATE_ERROR

รับข้อผิดพลาดที่ไม่ร้ายแรง

MediaController ได้รับข้อผิดพลาดที่ไม่ร้ายแรงจากการใช้ 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.
              }
            });