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

앱이 포그라운드에 있지 않은 동안 미디어를 재생하는 것이 좋습니다. 예를 들어 음악 플레이어는 일반적으로 사용자가 기기를 잠그거나 다른 앱을 사용하고 있을 때도 음악을 계속 재생합니다. Media3 라이브러리는 백그라운드 재생을 지원할 수 있는 일련의 인터페이스를 제공합니다.

MediaSessionService 사용

백그라운드 재생을 사용 설정하려면 별도의 서비스 내에 PlayerMediaSession를 포함해야 합니다. 이렇게 하면 앱이 포그라운드에 있지 않은 동안에도 기기가 미디어를 계속 제공할 수 있습니다.

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>

앱이 Android 10 (API 수준 29) 이상이 설치된 기기에서 실행되는 경우 mediaPlayback가 포함된 foregroundServiceType를 정의해야 합니다.

MediaController를 사용하여 재생 제어

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

UI 명령어 처리

MediaSessionMediaSession.Callback를 통해 컨트롤러로부터 명령어를 수신합니다. MediaSession를 초기화하면 MediaController가 플레이어에게 전송하는 모든 명령을 자동으로 처리하는 MediaSession.Callback의 기본 구현이 생성됩니다.

알림

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 미디어 컨트롤 맞춤설정에 대해 자세히 알아보기

알림 맞춤설정

알림을 맞춤설정하려면 DefaultMediaNotificationProvider.Builder를 사용하거나 제공자 인터페이스의 맞춤 구현을 만들어 MediaNotification.Provider를 만듭니다. setMediaNotificationProvider를 사용하여 MediaSessionService에 제공업체를 추가합니다.

재생 재개

미디어 버튼은 블루투스 헤드셋의 재생 또는 일시중지 버튼과 같이 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를 사용하여 재생을 제어하고 재생목록을 표시하는 것입니다. 동시에 세션은 Android 미디어 컨트롤, 휴대기기 또는 TV의 어시스턴트, 시계의 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를 지원하려면 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 지원을 보여주는 automotive 모듈이 있습니다.