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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

หากต้องการปรับแต่งอินสแตนซ์ MediaItem ที่เพิ่มลงในเพลเยอร์ คุณสามารถลบล้าง 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 ของตนเอง หากค่าคงที่ของไอคอนที่กำหนดไว้ล่วงหน้าไม่ตรงกับ Use Case ของคุณ ให้ใช้ setCustomIconResId แทน
  2. คำสั่ง ซึ่งกำหนดการดำเนินการที่เรียกใช้เมื่อผู้ใช้โต้ตอบกับปุ่ม คุณสามารถใช้ setPlayerCommand สำหรับ Player.Command หรือ setSessionCommand สำหรับ SessionCommand ที่กำหนดไว้ล่วงหน้าหรือกำหนดเอง
  3. ช่อง ซึ่งกำหนดตำแหน่งที่ควรวางปุ่มใน UI ของตัวควบคุม ช่องนี้เป็นข้อมูลที่ไม่บังคับและระบบจะตั้งค่าโดยอัตโนมัติตามไอคอนและคำสั่ง เช่น ช่วยให้ระบุได้ว่าควรแสดงปุ่มในพื้นที่การนำทาง "ไปข้างหน้า" ของ UI แทนพื้นที่ "รายการเพิ่มเติม" เริ่มต้น

Kotlin

val button =
  CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
    .setPlayerCommand(Player.COMMAND_SEEK_FORWARD)
    .setSlots(CommandButton.SLOT_FORWARD)
    .build()

Java

CommandButton button =
    new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15)
        .setPlayerCommand(Player.COMMAND_SEEK_FORWARD)
        .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 onConnectAsync(
    session: MediaSession,
    controller: ControllerInfo,
  ): ListenableFuture<ConnectionResult> {
    val sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return Futures.immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
}

Java

private static class CustomMediaSessionCallback implements MediaSession.Callback {

  // Configure commands available to the controller in onConnect()
  @Override
  public ListenableFuture<ConnectionResult> onConnectAsync(
      MediaSession session, ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS
            .buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return Futures.immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setAvailableSessionCommands(sessionCommands)
            .build());
  }
}

หากต้องการรับคำขอคำสั่งที่กำหนดเองจาก MediaController ให้ลบล้างเมธอด onCustomCommand() ใน Callback

Kotlin

private class CustomCallback : MediaSession.Callback {
  // ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: 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))
    }
    // ...
    return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
  }
}

Java

private static class CustomCallback 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));
    }
    // ...
    return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
  }
}

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

ปรับแต่งคำสั่งเพลเยอร์เริ่มต้น

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

Kotlin

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

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

Java

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

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

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

ระบุตัวควบคุมที่ขอคำสั่งโปรแกรมเล่น

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

Kotlin

private class CallerAwarePlayer(player: Player) : ForwardingSimpleBasePlayer(player) {
  private lateinit var session: MediaSession

  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

private static final class CallerAwarePlayer extends ForwardingSimpleBasePlayer {
  private MediaSession session;

  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 ที่เกี่ยวข้อง สำหรับกรณีการใช้งานขั้นสูง เหตุการณ์ปุ่มสื่อจะถูกสกัดกั้นใน MediaSession.Callback.onMediaButtonEvent(Intent)

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

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

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

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

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

การตั้งค่าข้อผิดพลาดของเพลเยอร์ที่กำหนดเอง

นอกเหนือจากข้อผิดพลาดร้ายแรงที่รายงานโดยเพลเยอร์แล้ว แอปพลิเคชันยังสามารถตั้งค่า PlaybackException ที่กำหนดเองในระดับ MediaSession โดยใช้ MediaSession.setPlaybackException(PlaybackException) ได้ด้วย ซึ่งช่วยให้แอปพลิเคชันส่งสัญญาณสถานะข้อผิดพลาดไปยังตัวควบคุมที่เชื่อมต่อได้ คุณสามารถตั้งค่าข้อยกเว้นสำหรับตัวควบคุมที่เชื่อมต่อทั้งหมดหรือสำหรับ ControllerInfo ที่เฉพาะเจาะจงได้

เมื่อแอปตั้งค่า PlaybackException โดยใช้ API นี้

  • ระบบจะแจ้งเตือนอินสแตนซ์ MediaController ที่เชื่อมต่ออยู่ ระบบจะเรียกใช้ Callback Listener.onPlayerError(PlaybackException) และ Listener.onPlayerErrorChanged(@Nullable PlaybackException) ในตัวควบคุมพร้อมข้อยกเว้นที่ระบุ

  • เมธอด MediaController.getPlayerError() จะแสดงผล PlaybackException ที่แอปพลิเคชันตั้งค่าไว้

  • สถานะการเล่นของตัวควบคุมที่ได้รับผลกระทบจะเปลี่ยนเป็น Player.STATE_IDLE

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

หากต้องการล้าง PlaybackException ที่กำหนดเองซึ่งตั้งค่าไว้ก่อนหน้านี้และคืนค่าการรายงานสถานะโปรแกรมเล่นตามปกติ แอปจะเรียกใช้ MediaSession.setPlaybackException(/* playbackException= */ null) หรือ MediaSession.setPlaybackException(ControllerInfo, /* playbackException= */ null) ได้

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

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

Kotlin

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

Java

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

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

Kotlin

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

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

private static 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.
              }
            });