이전에는 Android Music 앱을 만들 때 필요한 클래스, 타입들과 기본적인 구조에 대해 살펴보았다.
이번에는 살펴본 내용으로 직접 MediaLibraryService 만들어 보자.
MediaLibraryService는 MediaSessionService를 상속받고 있으며 abstract class이다.

그리고 한 메소드를 override 해야 하는데 onGetSession이다.
원래 MediaSessionService에 있는 abstract 함수인데 MediaLibraryService에서는 override되어 MediaLibrarySession을 반환하도록 되어 있다.

MediaLibrarySession은 MediaSession을 상속받고 있기 때문에 abstract 함수를 override를 할 수 있었던 것이고, MediaSession에 있는 음악 조절 기능들도 모두 사용할 수 있다.

그럼 MediaLibrarySession을 상속받아 구현해보자.
class PlayerService : MediaLibraryService() {
private var mediaLibrarySession: MediaLibrarySession? = null
private val mediaLibrarySessionCallback = object : MediaLibrarySession.Callback { }
override fun onCreate() {
super.onCreate()
mediaLibrarySession = MediaLibrarySession.Builder(
this,
ExoPlayer.Builder(this).build(),
mediaLibrarySessionCallback
).build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) =
mediaLibrarySession
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
val player = mediaLibrarySession?.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()
}
}
override fun onDestroy() {
mediaLibrarySession?.run {
player.release()
release()
mediaLibrarySession = null
}
super.onDestroy()
}
}
미디어를 조작하거나 탐색하기 위한 정보가 들어있다.
생성하려면 Builder로 생성해야 하며, 필요한 파라미터는 Context, Player, MediaLibrarySession.Callback이다.
특히 MediaLibrarySession.Callback이 다른 점인데, 이 곳에 있는 onSearch같은 함수를 override하여 탐색을 구현할 수 있다.

MediaSessionService와 같이 필수로 override 해야하는 abstract function이다.
다만 이전과는 다르게 MediaSession이 아닌 MediaLibrarySession을 반환해야 한다.
앱이 Task에 지워졌을 때 어떻게 할 것인지 결정할 수 있는 함수이다.
우리는 App이 종료되었을 때도 음악은 계속 재생되어야 하기 때문에 유지시켜 주어야 한다.
공식 문서에서는 이 조건을 위와 같이 했는데 뜻은 이러하다.
player.playWhenReady: getPlaybackState() == STATE_READ일 때 음악을 재생할 것인지 아닌지 결정하는 변수이다. 음악 재생이 준비되었을 때 재생할 것인지를 true, false로 결정할 수 있다.player.mediaItemCount: 현재 플레이리스트에 있는 미디어 아이템 갯수를 반환한다. 위의 코드에서는 갯수가 0일 때 정지하는 것으로 조건을 설정하였다.player.playbackState: 현재 플레이어의 상태를 나타낸다. 조건에서는 STATE_END일 때 정지한다.STATE에는 아래의 4가지 상태가 있다.

MediaLibraryService 공식 문서에서는 onTaskRemoved를 override하지 않지만, MediaSessionService 공식 문서에서는 onTaskRemoved를 위와 같이 override한다.
MediaLibraryService로 되면서 뭔가 변화되었기 때문인가 생각했지만 변화된 것은 딱히 없었다.

그래서 MediaSessionService문서와 같이 보면서 작성해야 한다.
위의 코드에서 보면 player.playbackState == Player.STATE_ENDED의 조건으로 되어 있다.
하지만 Player.STATE_ENDED는 한 곡이 끝나도 호출되기 때문에 한곡 재생이나 반복 재생으로 세팅되어도 꺼지게 된다.
마침 player에 repeatMode를 받을 수 있고, REPEAT_MODE_OFF라는 것도 있어서
공식 문서와는 조건을 다르게 주었다.
override fun onTaskRemoved(rootIntent: Intent?) {
val player = mediaLibrarySession?.player!!
if (!player.playWhenReady
|| player.mediaItemCount == 0
|| (player.playbackState == Player.STATE_ENDED
&& player.repeatMode == Player.REPEAT_MODE_OFF) //곡이 끝났고, 반복 재생 모드가 아닐 때
) {
stopSelf()
}
}
Media3가 나름 최신 라이브러리이니, 나도 Compose로 UI를 만들어 Activity에 연결해 보려고 한다.
MediaLibraryService가 완성되었으니 이제 연결해보자.
MediaLibraryService는 MediaController, MediaBrowser 둘 다 만들 수 있다.
그러니 MediaController와 MediaBrowser를 Compose 여러곳에서 사용할 수 있도록 CompositionLocal을 만들자.
val LocalMediaController = compositionLocalOf<MediaController?> { error("No MediaSession provided") }
val LocalMediaBrowser = compositionLocalOf<MediaBrowser?> { error("No LocalMediaBrowser provided") }
그다음 private 필드를 activity에 만들어 놓자.
compose에서 CompositionLocal을 사용해야 하니, 이 변수는 mutableState로 만든다.
private var mediaController by mutableStateOf<MediaController?>(null)
private var mediaBrowser by mutableStateOf<MediaBrowser?>(null)
MediaController와 MediaBrowser는 Future로 받아오게 된다.
만약 Future로 받아오는 중에 앱이 중단되거나 사용자가 끄면 받아오는 것을 중단시켜야 한다.
그래서 onStop에서 중단할 수 있도록 Future도 private Field로 만들어주자.
private lateinit var mediaControllerFuture: ListenableFuture<MediaController>
private lateinit var mediaBrowserFuture: ListenableFuture<MediaBrowser>
이제 이 변수들을 초기화 하는 코드를 작성해 보자.
//MediaService 토큰 받기
val sessionToken = SessionToken(this, ComponentName(this, MediaService::class.java))
//토큰으로 MediaController의 future 생성
mediaControllerFuture = MediaController.Builder(this, sessionToken)
.buildAsync()
//토큰으로 MediaBrowser의 future 생성
mediaBrowserFuture = MediaBrowser.Builder(this, sessionToken)
.buildAsync()
//MediaController가 준비되면 저장
mediaControllerFuture.addListener({
mediaController = mediaControllerFuture.get()
}, MoreExecutors.directExecutor())
//MediaBrowser가 준비되면 저장
mediaBrowserFuture.addListener({
mediaBrowser = mediaBrowserFuture.get()
}, MoreExecutors.directExecutor())
이제 이것을 CompositionLocalProvider에 넣어주면 된다.
CompositionLocalProvider(
LocalMediaController.provides(mediaController),
LocalMediaBrowser.provides(mediaBrowser)
) {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
MusicScreen(modifier = Modifier.padding(innerPadding))
}
}
마지막으로 앱이 stop 될 때, MediaController와 MediaBrowser를 해제시켜주고, 앞서 말했듯 Future들도 해제시켜 준다
override fun onStop() {
super.onStop()
mediaController?.release()
mediaBrowser?.release()
MediaController.releaseFuture(mediaControllerFuture)
MediaBrowser.releaseFuture(mediaBrowserFuture)
}
위의 내용은 안드로이드 MediaController 공식 문서에서 찾을 수 있다.
class MainActivity : ComponentActivity() {
private var mediaController by mutableStateOf<MediaController?>(null)
private var mediaBrowser by mutableStateOf<MediaBrowser?>(null)
private lateinit var mediaControllerFuture: ListenableFuture<MediaController>
private lateinit var mediaBrowserFuture: ListenableFuture<MediaBrowser>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//MediaService 토큰 받기
val sessionToken = SessionToken(this, ComponentName(this, MediaService::class.java))
//토큰으로 MediaController의 future 생성
mediaControllerFuture = MediaController.Builder(this, sessionToken)
.buildAsync()
//토큰으로 MediaBrowser의 future 생성
mediaBrowserFuture = MediaBrowser.Builder(this, sessionToken)
.buildAsync()
//MediaController가 준비되면 저장
mediaControllerFuture.addListener({
mediaController = mediaControllerFuture.get()
}, MoreExecutors.directExecutor())
//MediaBrowser가 준비되면 저장
mediaBrowserFuture.addListener({
mediaBrowser = mediaBrowserFuture.get()
}, MoreExecutors.directExecutor())
enableEdgeToEdge()
setContent {
OffLineMusicTheme {
CompositionLocalProvider(
LocalMediaController.provides(mediaController),
LocalMediaBrowser.provides(mediaBrowser)
) {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
MusicScreen(modifier = Modifier.padding(innerPadding))
}
}
}
}
}
override fun onStop() {
super.onStop()
mediaController?.release()
mediaBrowser?.release()
MediaController.releaseFuture(mediaControllerFuture)
MediaBrowser.releaseFuture(mediaBrowserFuture)
}
}
이제 준비는 끝났다. MusicScreen이라는 곳에 LocalMediaController.current를 통해 MediaController를 받아오고 실행시켜 보자.
@Composable
fun MusicScreen(modifier: Modifier = Modifier) {
val localMediaController = LocalMediaController.current
val localMediaBrowser = LocalMediaBrowser.current
localMediaController?.addMediaItem(
MediaItem.Builder().setMediaId("1")
.setUri("https://storage.googleapis.com/exoplayer-test-media-0/play.mp3").build()
)
localMediaController?.play()
}

앱을 꺼도 백그라운드에서 잘 동작하는 모습을 볼 수 있다.