MediaSessionService를 사용한 백그라운드 재생

앱이 포그라운드에 있지 않은 동안 미디어를 재생하는 것이 좋습니다. 대상 예를 들어 음악 플레이어는 일반적으로 사용자가 기기를 잠그면 다른 앱을 사용하고 있는지 확인할 수 있습니다. Media3 라이브러리는 이 인터페이스를 통해 백그라운드 재생을 지원할 수 있습니다.

MediaSessionService 사용

백그라운드 재생을 사용 설정하려면 Player 및 별도의 ServiceMediaSession 이렇게 하면 앱이 다음 위치에 있지 않아도 기기에서 미디어를 계속 제공할 수 있습니다. 있습니다.

<ph type="x-smartling-placeholder">
</ph> MediaSessionService를 사용하면 미디어 세션을 앱의 활동과 별도로 실행할 수 있습니다.
그림 1: MediaSessionService를 통해 미디어 연결 앱의 활동과 별도로 실행되도록 합니다.

서비스 내에서 플레이어를 호스팅할 때는 MediaSessionService를 사용해야 합니다. 이렇게 하려면 MediaSessionService를 확장하는 클래스를 만들고 찾을 수 있습니다.

MediaSessionService를 사용하면 Google과 같은 외부 클라이언트가 가능합니다. 어시스턴트, 시스템 미디어 컨트롤 또는 Wear OS와 같은 호환 기기 연결하고, 연결하고, 재생을 제어할 수 있습니다. UI 활동이 전혀 없습니다. 실제로 여러 개의 클라이언트 앱이 같은 MediaSessionService에 동시에 연결할 수 있으며, 각 앱은 자체적인 MediaController

서비스 수명 주기 구현

서비스의 세 가지 수명 주기 메서드를 구현해야 합니다.

  • onCreate()는 첫 번째 컨트롤러가 연결되고 서비스가 인스턴스화되고 시작될 때 호출됩니다. PlayerMediaSession를 빌드하기에 가장 좋은 위치입니다.
  • 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()
  }
}

자바

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

자바

@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
}

자바

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

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

매니페스트에서 서비스 선언

앱이 포그라운드 서비스를 실행하려면 권한이 필요합니다. FOREGROUND_SERVICE 권한이 있어야 하며, API 34 및 위 FOREGROUND_SERVICE_MEDIA_PLAYBACK:

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

또한 매니페스트에서 MediaSessionService 인텐트 필터로 Service 클래스를 선언해야 합니다.

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

먼저 foregroundServiceType 앱이 Android가 설치된 기기에서 실행될 때 mediaPlayback가 포함됩니다. 10 (API 수준 29) 이상

MediaController를 사용하여 재생 제어

플레이어 UI가 포함된 활동 또는 프래그먼트에서 MediaController를 사용하여 UI와 미디어 세션 간에 링크를 설정할 수 있습니다. UI는 미디어 컨트롤러를 사용하여 UI에서 세션 내 플레이어로 명령어를 전송합니다. MediaController 만들기 및 사용에 관한 자세한 내용은 MediaController 만들기 가이드를 참고하세요.

UI 명령어 처리

MediaSessionMediaSession.Callback MediaSession를 초기화하면 기본값이 생성됨 모든 코드를 자동으로 처리하는 MediaSession.Callback 구현 MediaController가 플레이어에 전송하는 명령어를 실행합니다.

알림

MediaSessionService는 자동으로 MediaNotification를 생성합니다. 대부분의 경우 잘 작동합니다. 기본적으로 게시된 알림은 미디어 세션의 최신 정보로 계속 업데이트되고 재생 컨트롤을 표시하는 MediaStyle 알림입니다. MediaNotification 에서 세션을 인식하고 다른 앱의 재생을 제어하는 데 사용될 수 있습니다. 동일한 세션에 연결된 네트워크를 가리킵니다

예를 들어 MediaSessionService를 사용하는 음악 스트리밍 앱은 MediaSession 구성에 따라 재생 컨트롤과 함께 재생 중인 현재 미디어 항목의 제목, 아티스트, 앨범 아트를 표시하는 MediaNotification를 만듭니다.

필수 메타데이터는 미디어에 제공하거나 다음 스니펫과 같이 미디어 항목의 일부로 선언할 수 있습니다.

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

자바

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 미디어 컨트롤의 명령 버튼을 맞춤설정할 수 있습니다. Android 미디어 컨트롤 맞춤설정에 대해 자세히 알아보기

알림 맞춤설정

알림을 맞춤설정하려면 MediaNotification.Provider 대상: DefaultMediaNotificationProvider.Builder 또는 제공자 인터페이스의 맞춤형 구현을 생성하여 제공할 수 있습니다. 제공업체를 MediaSessionService에 제공합니다. setMediaNotificationProvider

재생 재개

미디어 버튼은 Android 기기 및 기타 주변기기에서 볼 수 있는 하드웨어 버튼입니다. (예: 블루투스 헤드셋의 재생 또는 일시중지 버튼) Media3는 서비스가 실행 중일 때 미디어 버튼 입력을 처리합니다.

Media3 미디어 버튼 수신기 선언

Media3에는 사용자가 앱이 종료된 후, 심지어 기기가 다시 시작된 후에도 재생을 재개할 수 있는 API가 포함되어 있습니다. 기본적으로 재생 재개는 사용 중지되어 있습니다. 즉, 서비스가 실행되고 있지 않으면 사용자가 재생을 재개할 수 없습니다. 선택하려면 먼저 매니페스트에서 MediaButtonReceiver을 선언합니다.

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

재생 재개 콜백 구현

블루투스 기기 또는 Android 시스템 UI 재개 기능에서 재개를 요청하면 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
}

자바

@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이 플레이어를 준비하고 콜백이 완료됩니다.

고급 컨트롤러 구성 및 하위 호환성

일반적인 시나리오는 앱 UI에서 MediaController를 사용하여 표시됩니다. 동시에 세션은 휴대기기 또는 TV의 Android 미디어 컨트롤 및 어시스턴트, 시계의 Wear OS, 자동차의 Android Auto와 같은 외부 클라이언트에 노출됩니다. Media3 세션 데모 앱 은 이러한 시나리오를 구현하는 앱의 예입니다.

이러한 외부 클라이언트는 기존 AndroidX 라이브러리의 MediaControllerCompat 또는 Android 프레임워크의 android.media.session.MediaController와 같은 API를 사용할 수 있습니다. Media3는 기존 라이브러리와 완전히 하위 호환되며 Android 프레임워크 API와의 상호 운용성을 제공합니다.

미디어 알림 컨트롤러 사용

이러한 기존 또는 프레임워크 컨트롤러는 프레임워크 PlaybackState.getActions()PlaybackState.getCustomActions()에서 동일한 값을 읽습니다. 프레임워크 세션의 작업과 맞춤 작업을 결정하기 위해 앱은 미디어 알림 컨트롤러를 사용하고 사용 가능한 명령어와 맞춤 레이아웃을 설정할 수 있습니다. 서비스는 미디어 알림 컨트롤러를 세션에 연결하고 세션은 콜백의 onConnect()에서 반환된 ConnectionResult를 사용하여 프레임워크 세션의 작업과 맞춤 작업을 구성합니다.

모바일 전용 시나리오에서는 앱이 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()
}

자바

@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를 지원하기 위해 필요한 명령이 필요하며, 그러지 않으면 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()
}

자바

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

세션 데모 앱에는 자동차 모듈 는 별도의 APK가 필요한 Automotive OS 지원을 보여줍니다.