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

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

MediaSessionService 사용

백그라운드 재생을 사용 설정하려면 별도의 Service 내에 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()
  }
}

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

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

앱에 포그라운드 서비스를 실행할 권한이 필요합니다. 매니페스트에 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()

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 미디어 컨트롤의 명령 버튼을 맞춤설정할 수 있습니다. 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
}

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

재생 속도, 반복 모드 또는 셔플 모드와 같은 다른 매개변수를 저장한 경우 Media3에서 플레이어를 준비하고 콜백이 완료될 때 재생을 시작하기 전에 onPlaybackResumption()에서 이러한 매개변수를 사용하여 플레이어를 구성하는 것이 좋습니다.

고급 컨트롤러 구성 및 이전 버전과의 호환성

일반적인 시나리오는 앱 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()
}

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

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