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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

เผยแพร่ข้อมูลเมตาของเพลย์ลิสต์

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

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

ประกาศรหัสเพลย์ลิสต์และข้อมูลเมตาของเพลย์ลิสต์

ใน Media3 การระบุเพลย์ลิสต์และข้อมูลเมตาที่อธิบายเพลย์ลิสต์จะแยกออกเป็น 2 ขอบเขตของ MediaMetadata ดังนี้

  • รหัสเพลย์ลิสต์ระดับรายการ (MediaItem.mediaMetadata.playlistId):
    • ทำเครื่องหมาย MediaItem ที่กำลังเล่นเป็นส่วนหนึ่งของเพลย์ลิสต์โดยเรียกใช้ MediaMetadata.Builder.setPlaylistId ใน MediaMetadata ของรายการ ขณะที่ MediaItem.mediaId จะระบุแทร็กแต่ละแทร็กโดยไม่ซ้ำกัน
    • playlistId คือสตริงทึบแสงที่แอปกำหนดซึ่งระบุเพลย์ลิสต์อย่างไม่ซ้ำกันในเซสชันต่างๆ ตราบใดที่เพลย์ลิสต์ยังเล่นต่อได้ (เช่น 37i9dQZF1DX76Wlfdnj7AP) และไม่ควรนำมาใช้ซ้ำกับเพลย์ลิสต์อื่น ตัวควบคุมที่เชื่อมต่อจะไม่ตีความรหัสนี้ แต่จะจัดเก็บและส่งกลับไปยังแอปเพื่อเล่นเพลย์ลิสต์ต่อ
    • ทุกแทร็กที่อยู่ในเพลย์ลิสต์จะมี playlistId เดียวกัน ปล่อย playlistId ไม่ตั้งค่า (null) สำหรับแทร็กที่ไม่ได้เล่นจากบริบทเพลย์ลิสต์ เช่น แทร็กสแตนด์อโลนรายการเดียว
  • ข้อมูลเมตาของเพลย์ลิสต์ระดับเพลเยอร์ (Player.getPlaylistMetadata):
    • สร้างอินสแตนซ์ MediaMetadata โดยเฉพาะด้วยแอตทริบิวต์ที่อธิบายเพลย์ลิสต์ (setTitle, setArtworkUri และ setMediaType) แล้วตั้งค่าในเพลเยอร์โดยใช้ Player.setPlaylistMetadata
    • เมื่อตัวควบคุมที่เชื่อมต่อตรวจพบ currentMediaItem.mediaMetadata.playlistId ที่ไม่ใช่ค่า Null ระบบจะตรวจสอบ MediaController.getPlaylistMetadata เพื่อดึงข้อมูลชื่อและ URI ของอาร์ตเวิร์กของเพลย์ลิสต์
    • อัปเดตข้อมูลเมตาของเพลย์ลิสต์เมื่อใดก็ตามที่เพลย์ลิสต์ที่ใช้งานอยู่มีการเปลี่ยนแปลง (เช่น เมื่อผู้ใช้เริ่มเพลย์ลิสต์อื่น) เพื่อให้ getPlaylistMetadata อธิบายเพลย์ลิสต์ที่อ้างอิงโดย playlistId ของรายการปัจจุบันเสมอ

ตัวอย่างต่อไปนี้แสดงวิธีเผยแพร่ข้อมูลเมตาของเพลย์ลิสต์ใน Player และเชื่อมโยงแทร็ก MediaItem กับเพลย์ลิสต์นั้น

Kotlin

// 1. Declare the playlist descriptive metadata on the Player
val playlistMetadata =
  MediaMetadata.Builder()
    .setTitle("Morning Vibes")
    .setArtworkUri(Uri.parse("https://example.com/image/morning-vibes"))
    .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
    .build()
player.setPlaylistMetadata(playlistMetadata)

// 2. Mark the playing track(s) with the playlistId it belongs to
val trackItem =
  MediaItem.Builder()
    .setMediaId("foobar:track:6rqhFgbbKwnb9MLmUQDhG6")
    .setMediaMetadata(
      MediaMetadata.Builder()
        .setTitle("Example Track")
        .setArtist("Example Artist")
        .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
        .setPlaylistId("37i9dQZF1DX76Wlfdnj7AP")
        .build()
    )
    .build()
player.setMediaItem(trackItem)

Java

// 1. Declare the playlist descriptive metadata on the Player
MediaMetadata playlistMetadata =
    new MediaMetadata.Builder()
        .setTitle("Morning Vibes")
        .setArtworkUri(Uri.parse("https://example.com/image/morning-vibes"))
        .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
        .build();
player.setPlaylistMetadata(playlistMetadata);

// 2. Mark the playing track(s) with the playlistId it belongs to
MediaItem trackItem =
    new MediaItem.Builder()
        .setMediaId("foobar:track:6rqhFgbbKwnb9MLmUQDhG6")
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setTitle("Example Track")
                .setArtist("Example Artist")
                .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
                .setPlaylistId("37i9dQZF1DX76Wlfdnj7AP")
                .build())
        .build();
player.setMediaItem(trackItem);

กลับมาเล่นเพลย์ลิสต์อีกครั้ง

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

Kotlin

val playlistItem =
  MediaItem.Builder()
    .setMediaId("37i9dQZF1DX76Wlfdnj7AP")
    .setMediaMetadata(
      MediaMetadata.Builder()
        .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
        .setIsPlayable(true)
        .build()
    )
    .build()
controller.setMediaItem(playlistItem)
controller.prepare()
controller.play()

Java

MediaItem playlistItem =
    new MediaItem.Builder()
        .setMediaId("37i9dQZF1DX76Wlfdnj7AP")
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
                .setIsPlayable(true)
                .build())
        .build();
controller.setMediaItem(playlistItem);
controller.prepare();
controller.play();

แอปของคุณจะขยายเพลย์ลิสต์ MediaItem เป็นแทร็กที่เล่นได้ภายใน MediaSession.Callback.onSetMediaItems (หรือ onAddMediaItems) และเผยแพร่ playlistId และ playlistMetadata อีกครั้งตามที่แสดงในส่วนก่อนหน้า

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

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

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

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

  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.onConnectAsync() เพื่อปรับแต่งค่ากำหนดปุ่มสื่อสำหรับตัวควบคุมที่เชื่อมต่อแต่ละตัวก็ได้

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.onConnectAsync() เพื่อตั้งค่าคำสั่งที่กำหนดเองที่ใช้ได้สำหรับตัวควบคุมที่เชื่อมต่อแต่ละตัว

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

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

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

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

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

ในกรณีนี้ สถานะการเล่นจะเปลี่ยนเป็น STATE_IDLE และ MediaController.getPlayerError() จะแสดงผล PlaybackException ที่ทำให้เกิดการเปลี่ยนสถานะ ตัวควบคุมสามารถตรวจสอบ PlaybackException.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) ได้

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

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

Kotlin

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

Java

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

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

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