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

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

ใช้ MediaSessionService

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

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

ในกิจกรรมหรือส่วนที่ประกอบด้วย 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 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 and the start position
    // to use here
    MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist();
    settableFuture.set(resumptionPlaylist);
  }, MoreExecutors.directExecutor());
  return settableFuture;
}

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

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

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

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

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

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