앱이 포그라운드에 있지 않은 동안 미디어를 재생하는 것이 좋습니다. 예를 들어 음악 플레이어는 일반적으로 사용자가 기기를 잠그거나 다른 앱을 사용할 때도 음악을 계속 재생합니다. Media3 라이브러리는 백그라운드 재생을 지원할 수 있는 일련의 인터페이스를 제공합니다.
MediaSessionService 사용
백그라운드 재생을 사용 설정하려면 별도의 서비스 내에 Player
및 MediaSession
를 포함해야 합니다.
이렇게 하면 앱이 포그라운드에 있지 않은 동안에도 기기가 미디어를 계속 제공할 수 있습니다.
서비스 내에서 플레이어를 호스팅할 때는 MediaSessionService
를 사용해야 합니다.
이렇게 하려면 MediaSessionService
를 확장하는 클래스를 만들고 그 안에 미디어 세션을 만듭니다.
MediaSessionService
를 사용하면 Google 어시스턴트, 시스템 미디어 컨트롤, Wear OS와 같은 호환 기기와 같은 외부 클라이언트가 앱의 UI 활동에 전혀 액세스하지 않고도 서비스를 검색하고, 서비스에 연결하고, 재생을 제어할 수 있습니다. 실제로 동일한 MediaSessionService
에 여러 클라이언트 앱을 동시에 연결할 수 있으며 이때 각 앱에는 자체 MediaController
가 있습니다.
서비스 수명 주기 구현
서비스의 세 가지 수명 주기 메서드를 구현해야 합니다.
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() } }
자바
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 명령어 처리
MediaSession
는 MediaSession.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
를 사용하여 재생을 제어하고 재생목록을 표시하는 것입니다. 동시에 세션은 휴대기기 또는 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를 지원하려면 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 모듈이 있습니다.