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

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

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

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

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

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

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

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

อย่างไรก็ตาม MediaSession ไม่ได้เหมาะกับ Use Case บางรายการ คุณอาจต้อง ใช้เฉพาะ 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 จะอัปเดตเซสชันสื่อโดยอัตโนมัติโดยใช้สถานะของเพลเยอร์ คุณจึงไม่ต้องจัดการการแมปจากเพลเยอร์ไปยังเซสชันด้วยตนเอง

ซึ่งแตกต่างจากเซสชันสื่อของแพลตฟอร์มที่คุณต้องสร้างและดูแลPlaybackStateแยกต่างหากจากตัวเพลเยอร์เอง เช่น เพื่อระบุข้อผิดพลาด

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

โดยค่าเริ่มต้น 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() กับฟีเจอร์นี้ได้ในแอปสาธิตเซสชัน

จัดการค่ากำหนดปุ่มสื่อ

ตัวควบคุมทุกตัว เช่น UI ของระบบ, Android Auto หรือ Wear OS จะตัดสินใจได้เองว่าจะแสดงปุ่มใดต่อผู้ใช้ หากต้องการระบุตัวควบคุมการเล่นที่ต้องการแสดงต่อผู้ใช้ ให้ระบุค่ากำหนดปุ่มสื่อใน MediaSession ค่ากําหนดเหล่านี้ประกอบด้วยรายการอินสแตนซ์ CommandButton ที่เรียงลําดับ โดยแต่ละรายการจะกําหนดค่ากําหนดสําหรับปุ่มในอินเทอร์เฟซผู้ใช้

กำหนดปุ่มคำสั่ง

อินสแตนซ์ CommandButton ใช้เพื่อกำหนดค่ากำหนดปุ่มสื่อ ปุ่มแต่ละปุ่มจะกำหนดแง่มุม 3 ด้านขององค์ประกอบ UI ที่ต้องการ ดังนี้

  1. ไอคอน ซึ่งกำหนดลักษณะที่ปรากฏ คุณต้องตั้งค่าไอคอนเป็นค่าคงที่ที่กําหนดไว้ล่วงหน้าเมื่อสร้าง CommandButton.Builder โปรดทราบว่าไฟล์นี้ไม่ใช่ทรัพยากรบิตแมปหรือรูปภาพจริง ค่าคงที่ทั่วไปช่วยให้ตัวควบคุมเลือกทรัพยากรที่เหมาะสมเพื่อให้รูปลักษณ์และความรู้สึกสอดคล้องกันภายใน UI ของตนเองได้ หากไม่มีค่าคงที่ไอคอนที่กําหนดไว้ล่วงหน้าใดที่เหมาะกับกรณีการใช้งานของคุณ คุณก็ใช้ setCustomIconResId แทนได้
  2. คําสั่ง ซึ่งกําหนดการดําเนินการที่ทริกเกอร์เมื่อผู้ใช้โต้ตอบกับปุ่ม คุณสามารถใช้ setPlayerCommand สำหรับ Player.Command หรือ setSessionCommand สำหรับ SessionCommand ที่กําหนดไว้ล่วงหน้าหรือกําหนดเอง
  3. ช่อง ซึ่งกำหนดตําแหน่งที่จะวางปุ่มใน UI ของตัวควบคุม ช่องนี้เป็นข้อมูลที่ไม่บังคับและระบบจะตั้งค่าโดยอัตโนมัติตามไอคอนและคําสั่ง เช่น ช่วยให้ระบุได้ว่าปุ่มควรแสดงในพื้นที่การนำทาง "ไปข้างหน้า" ของ UI แทนพื้นที่ "รายการเพิ่มเติม" เริ่มต้น

Kotlin

val button =
  CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
    .setSessionCommand(SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY))
    .setSlots(CommandButton.SLOT_FORWARD)
    .build()

Java

CommandButton button =
    new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
        .setSessionCommand(new SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY))
        .setSlots(CommandButton.SLOT_FORWARD)
        .build();

เมื่อระบบแก้ไขค่ากำหนดของปุ่มสื่อแล้ว ระบบจะใช้อัลกอริทึมต่อไปนี้

  1. สําหรับ CommandButton แต่ละรายการในค่ากําหนดของปุ่มสื่อ ให้วางปุ่มในช่องแรกที่ใช้ได้และได้รับอนุญาต
  2. หากช่องกลาง ไปข้างหน้า และย้อนกลับไม่มีปุ่ม ให้เพิ่มปุ่มเริ่มต้นสำหรับช่องนี้

คุณสามารถใช้ CommandButton.DisplayConstraints เพื่อสร้างตัวอย่างวิธีแก้ปัญหาค่ากำหนดปุ่มสื่อโดยขึ้นอยู่กับข้อจำกัดการแสดง UI

ตั้งค่ากำหนดปุ่มสื่อ

วิธีที่ง่ายที่สุดในการตั้งค่ากำหนดของปุ่มสื่อคือการระบุรายการเมื่อสร้าง MediaSession หรือจะลบล้างค่า MediaSession.Callback.onConnect เพื่อปรับแต่งค่ากำหนดปุ่มสื่อสำหรับตัวควบคุมที่เชื่อมต่อแต่ละตัวก็ได้

Kotlin

val mediaSession =
  MediaSession.Builder(context, player)
    .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
    .build()

Java

MediaSession mediaSession =
  new MediaSession.Builder(context, player)
      .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton))
      .build();

อัปเดตค่ากําหนดของปุ่มสื่อหลังจากการโต้ตอบของผู้ใช้

หลังจากจัดการการโต้ตอบกับโปรแกรมเล่นแล้ว คุณอาจต้องอัปเดตปุ่มที่แสดงใน UI ของคอนโทรลเลอร์ ตัวอย่างทั่วไปคือปุ่มเปิด/ปิดที่จะเปลี่ยนไอคอนและการทํางานหลังจากเรียกใช้การดําเนินการที่เกี่ยวข้องกับปุ่มนี้ หากต้องการอัปเดตค่ากำหนดของปุ่มสื่อ ให้ใช้ MediaSession.setMediaButtonPreferences เพื่ออัปเดตค่ากำหนดสำหรับตัวควบคุมทั้งหมดหรือตัวควบคุมที่เฉพาะเจาะจง

Kotlin

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(
  ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

// Handle "favoritesButton" action, replace by opposite button
mediaSession.setMediaButtonPreferences(
    ImmutableList.of(likeButton, removeFromFavoritesButton));

เพิ่มคําสั่งที่กําหนดเองและปรับแต่งลักษณะการทํางานเริ่มต้น

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

ประกาศและจัดการคําสั่งที่กําหนดเอง

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

หากต้องการกำหนดคำสั่งที่กำหนดเอง คุณต้องลบล้าง MediaSession.Callback.onConnect() เพื่อตั้งค่าคำสั่งที่กำหนดเองที่ใช้ได้สำหรับตัวควบคุมที่เชื่อมต่อแต่ละตัว

Kotlin

private 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 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 ซึ่งช่วยให้คุณปรับแต่งลักษณะการทํางานของแอปเพื่อตอบสนองต่อคําสั่งหนึ่งๆ ได้ หากคําสั่งนั้นมาจากระบบ แอปของคุณเอง หรือแอปไคลเอ็นต์อื่นๆ

ปรับแต่งคำสั่งเริ่มต้นของโปรแกรมเล่น

ระบบจะมอบหมายคําสั่งเริ่มต้นและการจัดการสถานะทั้งหมดให้กับ Player ใน MediaSession หากต้องการปรับแต่งลักษณะการทํางานของคําสั่งที่กําหนดไว้ในอินเทอร์เฟซ 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เมธอดที่เหมาะสมในโปรแกรมเล่นเซสชัน

เราขอแนะนำให้จัดการเหตุการณ์ปุ่มสื่อขาเข้าทั้งหมดในเมธอด Player ที่เกี่ยวข้อง สำหรับ Use Case ขั้นสูงขึ้น คุณสามารถสกัดกั้นเหตุการณ์ของปุ่มสื่อใน MediaSession.Callback.onMediaButtonEvent(Intent)

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

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

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

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

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

สําหรับความสามารถในการทํางานร่วมกัน ระบบจะทําซ้ำข้อผิดพลาดร้ายแรงไปยังเซสชันแพลตฟอร์มโดยเปลี่ยนสถานะเป็น 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();

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

Kotlin

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

  override fun getState(): State {
    var state = super.getState()
    if (state.playerError != null) {
      state =
        state.buildUpon()
          .setPlayerError(customizePlaybackException(state.playerError!!))
          .build()
    }
    return state
  }

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

Java

class ErrorForwardingPlayer extends ForwardingSimpleBasePlayer {

  private final Context context;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
  }

  @Override
  protected State getState() {
    State state = super.getState();
    if (state.playerError != null) {
      state =
          state.buildUpon()
              .setPlayerError(customizePlaybackException(state.playerError))
              .build();
    }
    return state;
  }

  private PlaybackException customizePlaybackException(PlaybackException error) {
    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;
      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);
  }
}

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

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

Kotlin

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

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

// Option 2: Sending a nonfatal error to the media notification controller only
// 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));

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

// Option 2: Sending a nonfatal error to the media notification controller only
// 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);
}

เมื่อส่งข้อผิดพลาดที่ไม่ร้ายแรงไปยังตัวควบคุมการแจ้งเตือนสื่อ ระบบจะทําซ้ำรหัสข้อผิดพลาดและข้อความแสดงข้อผิดพลาดไปยังเซสชันสื่อของแพลตฟอร์ม ขณะที่PlaybackState.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.
              }
            });