การเล่นขณะล็อกหน้าจอหรือขณะใช้แอปอื่นด้วย MediaSessionService

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

ใช้ MediaSessionService

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

MediaSessionService ช่วยให้เซสชันสื่อทำงานแยกกัน
  จากกิจกรรมของแอปได้
รูปที่ 1: MediaSessionService ช่วยให้เซสชันสื่อทำงานแยกจากกิจกรรมของแอปได้

เมื่อโฮสต์ผู้เล่นภายในบริการ คุณควรใช้ MediaSessionService โดยให้สร้างคลาสที่ขยาย MediaSessionService และสร้างเซสชันสื่อภายในคลาสดังกล่าว

การใช้ MediaSessionService ช่วยให้ไคลเอ็นต์ภายนอก เช่น Google Assistant, ตัวควบคุมสื่อของระบบ, ปุ่มสื่อในอุปกรณ์ต่อพ่วง หรืออุปกรณ์เสริม เช่น Wear OS ค้นพบบริการของคุณ เชื่อมต่อกับบริการ และควบคุมการเล่นได้โดยไม่ต้องเข้าถึงกิจกรรม UI ของแอปเลย ในความเป็นจริง อาจมีแอปไคลเอ็นต์หลายแอปที่เชื่อมต่อกับ MediaSessionService เดียวกัน ในเวลาเดียวกัน โดยแต่ละแอปจะมี MediaController ของตัวเอง

ใช้การจัดการวงจรของบริการ

คุณต้องใช้เมธอดวงจร 2 รายการของบริการ ได้แก่

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

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

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null

  // Create your player and media session in the onCreate lifecycle event
  override fun onCreate() {
    super.onCreate()
    val player = ExoPlayer.Builder(this).build()
    mediaSession = MediaSession.Builder(this, player).build()
  }

  // Remember to release the player and media session in onDestroy
  override fun onDestroy() {
    mediaSession?.run {
      player.release()
      release()
      mediaSession = null
    }
    super.onDestroy()
  }
}

Java

public class PlaybackService extends MediaSessionService {
  private MediaSession mediaSession = null;

  // Create your Player and MediaSession in the onCreate lifecycle event
  @Override
  public void onCreate() {
    super.onCreate();
    ExoPlayer player = new ExoPlayer.Builder(this).build();
    mediaSession = new MediaSession.Builder(this, player).build();
  }

  // Remember to release the player and media session in onDestroy
  @Override
  public void onDestroy() {
    mediaSession.getPlayer().release();
    mediaSession.release();
    mediaSession = null;
    super.onDestroy();
  }
}

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

Kotlin

override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

สำหรับการติดตั้งใช้งาน onTaskRemoved ด้วยตนเองอื่นๆ คุณสามารถใช้ isPlaybackOngoing() เพื่อตรวจสอบว่าระบบพิจารณาว่าการเล่นกำลังดำเนินอยู่และ มีการเริ่มบริการที่ทำงานในเบื้องหน้าหรือไม่

ให้สิทธิ์เข้าถึงเซสชันสื่อ

แทนที่onGetSession()เมธอดเพื่อให้ไคลเอ็นต์อื่นๆ เข้าถึงสื่อ เซสชันที่สร้างขึ้นเมื่อสร้างบริการ

Kotlin

class PlaybackService : MediaSessionService() {
  private var mediaSession: MediaSession? = null
  // [...] lifecycle methods omitted

  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
    mediaSession
}

Java

public class PlaybackService extends MediaSessionService {
  private MediaSession mediaSession = null;
  // [...] lifecycle methods omitted

  @Override
  public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
    return mediaSession;
  }
}

ประกาศบริการในไฟล์ Manifest

แอปต้องมีสิทธิ์ FOREGROUND_SERVICE และ FOREGROUND_SERVICE_MEDIA_PLAYBACK จึงจะเรียกใช้บริการที่ทำงานอยู่เบื้องหน้าสำหรับการเล่นได้

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

นอกจากนี้ คุณต้องประกาศคลาส Service ในไฟล์ Manifest ด้วยตัวกรอง Intent ของ MediaSessionService และ foregroundServiceType ที่มี mediaPlayback

<service
    android:name=".PlaybackService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaSessionService"/>
        <action android:name="android.media.browse.MediaBrowserService"/>
    </intent-filter>
</service>

ควบคุมการเล่นโดยใช้ MediaController

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

จัดการคำสั่ง MediaController

MediaSession จะรับคำสั่งจากคอนโทรลเลอร์ผ่าน MediaSession.Callback การเริ่มต้น MediaSession จะสร้างการติดตั้งใช้งานเริ่มต้นของ MediaSession.Callback ซึ่งจะจัดการคำสั่งทั้งหมดที่ MediaController ส่งไปยังเพลเยอร์โดยอัตโนมัติ

การแจ้งเตือน

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

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

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

Kotlin

val mediaItem =
    MediaItem.Builder()
      .setMediaId("media-1")
      .setUri(mediaUri)
      .setMediaMetadata(
        MediaMetadata.Builder()
          .setArtist("David Bowie")
          .setTitle("Heroes")
          .setArtworkUri(artworkUri)
          .build()
      )
      .build()

mediaController.setMediaItem(mediaItem)
mediaController.prepare()
mediaController.play()

Java

MediaItem mediaItem =
    new MediaItem.Builder()
        .setMediaId("media-1")
        .setUri(mediaUri)
        .setMediaMetadata(
            new MediaMetadata.Builder()
                .setArtist("David Bowie")
                .setTitle("Heroes")
                .setArtworkUri(artworkUri)
                .build())
        .build();

mediaController.setMediaItem(mediaItem);
mediaController.prepare();
mediaController.play();

วงจรการแจ้งเตือน

ระบบจะสร้างการแจ้งเตือนทันทีที่PlayerมีMediaItemอินสแตนซ์ ในเพลย์ลิสต์

การอัปเดตการแจ้งเตือนทั้งหมดจะเกิดขึ้นโดยอัตโนมัติตามสถานะของ Player และ MediaSession

นำการแจ้งเตือนออกไม่ได้ขณะที่บริการที่ทำงานอยู่เบื้องหน้ากำลังทำงาน หากต้องการนำการแจ้งเตือนออกทันที คุณต้องโทรหา Player.release() หรือล้างเพลย์ลิสต์โดยใช้ Player.clearMediaItems()

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

การปรับแต่งการแจ้งเตือน

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

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

หากต้องการปรับแต่งการแจ้งเตือนเพิ่มเติม ให้สร้าง MediaNotification.Provider ด้วย DefaultMediaNotificationProvider.Builder หรือสร้างการติดตั้งใช้งานอินเทอร์เฟซผู้ให้บริการที่กำหนดเอง เพิ่มผู้ให้บริการลงใน MediaSessionService ด้วย setMediaNotificationProvider

การกลับมาเล่นอีกครั้ง

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

ประกาศตัวรับปุ่มสื่อของ Media3

เริ่มต้นด้วยการประกาศ MediaButtonReceiver ในไฟล์ Manifest ดังนี้

<receiver android:name="androidx.media3.session.MediaButtonReceiver"
  android:exported="true">
  <intent-filter>
    <action android:name="android.intent.action.MEDIA_BUTTON" />
  </intent-filter>
</receiver>

ใช้การเรียกกลับเพื่อเล่นต่อ

เมื่ออุปกรณ์บลูทูธหรือฟีเจอร์เล่นต่อของ UI ระบบ Android ขอให้เล่นต่อ ระบบจะเรียกใช้เมธอดเรียกกลับของ onPlaybackResumption()

Kotlin

override fun onPlaybackResumption(
    mediaSession: MediaSession,
    controller: ControllerInfo
): ListenableFuture<MediaItemsWithStartPosition> {
  val settable = SettableFuture.create<MediaItemsWithStartPosition>()
  scope.launch {
    // Your app is responsible for storing the playlist, metadata (like title
    // and artwork) of the current item and the start position to use here.
    val resumptionPlaylist = restorePlaylist()
    settable.set(resumptionPlaylist)
  }
  return settable
}

Java

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession,
    ControllerInfo controller
) {
  SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create();
  settableFuture.addListener(() -> {
    // Your app is responsible for storing the playlist, metadata (like title
    // and artwork) of the current item and the start position to use here.
    MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist();
    settableFuture.set(resumptionPlaylist);
  }, MoreExecutors.directExecutor());
  return settableFuture;
}

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

เมธอดนี้จะเรียกใช้ในระหว่างเวลาบูตเพื่อสร้างการแจ้งเตือนการกลับมาทำงานต่อของ UI ระบบ Android หลังจากรีบูตอุปกรณ์ สำหรับ Rich Notification ขอแนะนำให้กรอกข้อมูลในMediaMetadataฟิลด์ เช่น title และ artworkData หรือ artworkUri ของสินค้าปัจจุบันด้วยค่าที่ใช้ได้ในพื้นที่ เนื่องจากอาจยังไม่มีสิทธิ์เข้าถึงเครือข่าย นอกจากนี้ คุณยังเพิ่ม MediaConstants.EXTRAS_KEY_COMPLETION_STATUSและ MediaConstants.EXTRAS_KEY_COMPLETION_PERCENTAGEลงใน MediaMetadata.extras เพื่อระบุตำแหน่งการเล่นต่อได้ด้วย

การกำหนดค่าคอนโทรลเลอร์ขั้นสูงและความเข้ากันได้แบบย้อนหลัง

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

ไคลเอ็นต์ภายนอกเหล่านี้อาจใช้ API เช่น MediaControllerCompat ของไลบรารี AndroidX รุ่นเดิมหรือ android.media.session.MediaController ของแพลตฟอร์ม Android Media3 เข้ากันได้แบบย้อนหลังกับไลบรารีเดิมอย่างเต็มรูปแบบ และ มอบการทำงานร่วมกันกับ Android Platform API

ใช้ตัวควบคุมการแจ้งเตือนสื่อ

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

ตัวอย่างเช่น แอปสามารถให้การใช้งานของ MediaSession.Callback.onConnect() เพื่อตั้งค่าคำสั่งที่ใช้ได้และ ค่ากำหนดปุ่มสื่อสำหรับเซสชันแพลตฟอร์มโดยเฉพาะได้ดังนี้

Kotlin

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  if (session.isMediaNotificationController(controller)) {
    val sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(customCommandSeekBackward)
        .add(customCommandSeekForward)
        .build()
    val playerCommands =
      ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
        .remove(COMMAND_SEEK_TO_PREVIOUS)
        .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
        .remove(COMMAND_SEEK_TO_NEXT)
        .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
        .build()
    // Custom button preferences and commands to configure the platform session.
    return AcceptedResultBuilder(session)
      .setMediaButtonPreferences(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default button preferences for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Java

@Override
public ConnectionResult onConnect(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS
            .buildUpon()
            .add(customCommandSeekBackward)
            .add(customCommandSeekForward)
            .build();
    Player.Commands playerCommands =
        ConnectionResult.DEFAULT_PLAYER_COMMANDS
            .buildUpon()
            .remove(COMMAND_SEEK_TO_PREVIOUS)
            .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
            .remove(COMMAND_SEEK_TO_NEXT)
            .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
            .build();
    // Custom button preferences and commands to configure the platform session.
    return new AcceptedResultBuilder(session)
        .setMediaButtonPreferences(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands with default button preferences for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

ให้สิทธิ์ Android Auto ในการส่งคำสั่งที่กำหนดเอง

เมื่อใช้ MediaLibraryService และเพื่อรองรับ Android Auto ด้วยแอปบนอุปกรณ์เคลื่อนที่ ตัวควบคุม Android Auto ต้องมีคำสั่งที่เหมาะสม มิฉะนั้น Media3 จะปฏิเสธ คำสั่งที่กำหนดเองขาเข้าจากตัวควบคุมนั้น

Kotlin

override fun onConnect(
  session: MediaSession,
  controller: MediaSession.ControllerInfo
): ConnectionResult {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
      .add(customCommandSeekBackward)
      .add(customCommandSeekForward)
      .build()
  if (session.isMediaNotificationController(controller)) {
    // [...] See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available session commands to accept incoming custom commands from Auto.
    return AcceptedResultBuilder(session)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands for all other controllers.
  return AcceptedResultBuilder(session).build()
}

Java

@Override
public ConnectionResult onConnect(
    MediaSession session, MediaSession.ControllerInfo controller) {
  SessionCommands sessionCommands =
      ConnectionResult.DEFAULT_SESSION_COMMANDS
          .buildUpon()
          .add(customCommandSeekBackward)
          .add(customCommandSeekForward)
          .build();
  if (session.isMediaNotificationController(controller)) {
    // [...] See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

แอปเดโมเซสชันมี โมดูลยานยนต์ ซึ่งแสดงการรองรับ Automotive OS ที่ต้องใช้ APK แยกต่างหาก