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

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

ใช้ MediaSessionService

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

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

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

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

ใช้วงจรการบริการ

คุณต้องใช้วิธีวงจร 3 วิธีสําหรับบริการ ได้แก่

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

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

  // The user dismissed the app from the recent tasks
  override fun onTaskRemoved(rootIntent: Intent?) {
    val player = mediaSession?.player!!
    if (!player.playWhenReady
        || player.mediaItemCount == 0
        || player.playbackState == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf()
    }
  }

  // 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();
  }

  // The user dismissed the app from the recent tasks
  @Override
  public void onTaskRemoved(@Nullable Intent rootIntent) {
    Player player = mediaSession.getPlayer();
    if (!player.getPlayWhenReady()
        || player.getMediaItemCount() == 0
        || player.getPlaybackState() == Player.STATE_ENDED) {
      // Stop the service if not playing, continue playing in the background
      // otherwise.
      stopSelf();
    }
  }

  // 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?) {
  val player = mediaSession.player
  if (player.playWhenReady) {
    // Make sure the service is not in foreground.
    player.pause()
  }
  stopSelf()
}

Java

@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  Player player = mediaSession.getPlayer();
  if (player.getPlayWhenReady()) {
    // Make sure the service is not in foreground.
    player.pause();
  }
  stopSelf();
}

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

ลบล้างเมธอด 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 สำหรับไฟล์ Manifest และหากคุณกำหนดเป้าหมายเป็น API 34 และ ข้างต้นและ 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

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

คุณต้องกำหนด foregroundServiceType ซึ่งรวม mediaPlayback ไว้ด้วยเมื่อแอปของคุณทำงานบนอุปกรณ์ที่ใช้ Android 10 (API ระดับ 29) ขึ้นไป

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

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

จัดการคำสั่งของ UI

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

แอปสามารถปรับแต่งปุ่มคำสั่งของการควบคุม Android Media ได้ อ่านเพิ่มเติม เกี่ยวกับการปรับแต่ง Android Media ควบคุม

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

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

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

ปุ่มสื่อคือปุ่มฮาร์ดแวร์ที่พบในอุปกรณ์ Android และอุปกรณ์ต่อพ่วงอื่นๆ อุปกรณ์ เช่น ปุ่มเล่นหรือหยุดชั่วคราวบนชุดหูฟังบลูทูธ สื่อ3 จัดการอินพุตปุ่มสื่อให้คุณเมื่อบริการทำงานอยู่

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

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

ใช้ Callback ของการกลับมาเล่นอีกครั้ง

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

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 จะเตรียมโปรแกรมเล่นและเริ่มเล่นเมื่อ Callback เสร็จสมบูรณ์

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

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

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

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

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

สำหรับสถานการณ์บนอุปกรณ์เคลื่อนที่เท่านั้น แอปสามารถให้บริการ 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 layout and available commands to configure the legacy/framework session.
    return AcceptedResultBuilder(session)
      .setCustomLayout(
        ImmutableList.of(
          createSeekBackwardButton(customCommandSeekBackward),
          createSeekForwardButton(customCommandSeekForward))
      )
      .setAvailablePlayerCommands(playerCommands)
      .setAvailableSessionCommands(sessionCommands)
      .build()
  }
  // Default commands with default custom layout 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 layout and available commands to configure the legacy/framework session.
    return new AcceptedResultBuilder(session)
        .setCustomLayout(
            ImmutableList.of(
                createSeekBackwardButton(customCommandSeekBackward),
                createSeekForwardButton(customCommandSeekForward)))
        .setAvailablePlayerCommands(playerCommands)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
  // Default commands without default custom layout 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 with default custom layout 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 without default custom layout for all other controllers.
  return new AcceptedResultBuilder(session).build();
}

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