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

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

ใช้ MediaSessionService

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

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

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

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

ใช้ Service Lifecycle

คุณต้องใช้วิธีการของวงจรชีวิต 2 วิธีสำหรับบริการของคุณ ดังนี้

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

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

Kotlin

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

  // Create your Player and MediaSession 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()
  }

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

Java

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

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

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

Kotlin

@OptIn(UnstableApi::class)
override fun onTaskRemoved(rootIntent: Intent?) {
  pauseAllPlayersAndStopSelf()
}

Java

@OptIn(markerClass = UnstableApi.class)
@Override
public void onTaskRemoved(@Nullable Intent rootIntent) {
  pauseAllPlayersAndStopSelf();
}

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

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

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

Kotlin

class PlaybackService : MediaSessionService() {

  // [...] lifecycle methods omitted

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

Java

class PlaybackService extends MediaSessionService {

  // [...] 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>

ใช้ Callback การกลับมาเล่นต่อ

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

Kotlin

override fun onPlaybackResumption(
  mediaSession: MediaSession,
  controller: MediaSession.ControllerInfo,
  isForPlayback: Boolean,
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
  val settableFuture = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
  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.
      val resumptionPlaylist = restorePlaylist()
      settableFuture.set(resumptionPlaylist)
    },
    MoreExecutors.directExecutor(),
  )
  return settableFuture
}

Java

@Override
public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption(
    MediaSession mediaSession, ControllerInfo controller, boolean isForPlayback) {
  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 จะเตรียมเพลเยอร์และเริ่มเล่นเมื่อการเรียกกลับเสร็จสมบูรณ์

ระบบจะเรียกใช้เมธอดนี้ในระหว่างการเปิดเครื่องเพื่อสร้างการแจ้งเตือนการกลับมาทำงานอีกครั้งของ Android System UI หลังจากรีบูตอุปกรณ์โดยตั้งค่า isForPlayback เป็น false สำหรับการแจ้งเตือนแบบริชมีเดีย ขอแนะนำให้กรอกข้อมูลในช่อง 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 เข้ากันได้แบบย้อนหลังกับไลบรารีเดิมอย่างสมบูรณ์ และทำงานร่วมกับ API ของแพลตฟอร์ม Android ได้

ระบุตัวควบคุมที่เชื่อถือได้

แอปใดก็ตามสามารถพยายามเชื่อมต่อกับเซสชันหรือคลังสื่อของคุณได้ หากต้องการจำกัดสิทธิ์เข้าถึงตัวควบคุมระบบ ตัวควบคุมที่มีสิทธิ์ควบคุมเนื้อหาสื่อ และแอปของคุณเอง คุณสามารถใช้ ControllerInfo.isTrusted() เพื่อตรวจสอบสิทธิ์เข้าถึงขั้นพื้นฐานได้ หรือจะระบุตัวควบคุมที่เฉพาะเจาะจงมากขึ้น เช่น ตัวควบคุมการแจ้งเตือนสื่อหรือตัวควบคุม Android Auto ตามที่อธิบายไว้ในส่วนต่อไปนี้ก็ได้

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

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

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

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  if (session.isMediaNotificationController(controller)) {
    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 immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setMediaButtonPreferences(listOf(seekBackButton, seekForwardButton))
        .setAvailablePlayerCommands(playerCommands)
        .build()
    )
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

@Override
public ListenableFuture<ConnectionResult> onConnectAsync(
    MediaSession session, MediaSession.ControllerInfo controller) {
  if (session.isMediaNotificationController(controller)) {
    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 immediateFuture(
        new AcceptedResultBuilder(session, controller)
            .setMediaButtonPreferences(ImmutableList.of(seekBackButton, seekForwardButton))
            .setAvailablePlayerCommands(playerCommands)
            .build());
  }
  // Default commands with default button preferences for all other controllers.
  return immediateFuture(new AcceptedResultBuilder(session, controller).build());
}

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

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

Kotlin

override fun onConnectAsync(
  session: MediaSession,
  controller: MediaSession.ControllerInfo,
): ListenableFuture<ConnectionResult> {
  val sessionCommands =
    ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(customCommand).build()
  if (session.isMediaNotificationController(controller)) {
    // ... See above.
  } else if (session.isAutoCompanionController(controller)) {
    // Available commands to accept incoming custom commands from Auto.
    return immediateFuture(
      AcceptedResultBuilder(session, controller)
        .setAvailableSessionCommands(sessionCommands)
        .build()
    )
  }
  // Default commands for all other controllers.
  return immediateFuture(AcceptedResultBuilder(session, controller).build())
}

Java

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

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