Android Music 앱 만들기 (2) - MediaLibraryService

MoonHwi Han·2025년 1월 9일

개요

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

MediaLibraryService

MediaLibraryServiceMediaSessionService를 상속받고 있으며 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()

    }


    
}

MediaLibrarySession

미디어를 조작하거나 탐색하기 위한 정보가 들어있다.
생성하려면 Builder로 생성해야 하며, 필요한 파라미터는 Context, Player, MediaLibrarySession.Callback이다.

특히 MediaLibrarySession.Callback이 다른 점인데, 이 곳에 있는 onSearch같은 함수를 override하여 탐색을 구현할 수 있다.

onGetSession

MediaSessionService와 같이 필수로 override 해야하는 abstract function이다.
다만 이전과는 다르게 MediaSession이 아닌 MediaLibrarySession을 반환해야 한다.

onTaskRemoved

앱이 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문서와 같이 보면서 작성해야 한다.

STATE_END일 때 종료??

위의 코드에서 보면 player.playbackState == Player.STATE_ENDED의 조건으로 되어 있다.
하지만 Player.STATE_ENDED는 한 곡이 끝나도 호출되기 때문에 한곡 재생이나 반복 재생으로 세팅되어도 꺼지게 된다.

마침 playerrepeatMode를 받을 수 있고, 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()
    }
}


Activity 연결

Media3가 나름 최신 라이브러리이니, 나도 Compose로 UI를 만들어 Activity에 연결해 보려고 한다.

MediaLibraryService가 완성되었으니 이제 연결해보자.


MediaLibraryService는 MediaController, MediaBrowser 둘 다 만들 수 있다.

그러니 MediaControllerMediaBrowser를 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)

MediaControllerMediaBrowser는 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 될 때, MediaControllerMediaBrowser를 해제시켜주고, 앞서 말했듯 Future들도 해제시켜 준다

override fun onStop() {
    super.onStop()
    mediaController?.release()
    mediaBrowser?.release()
    MediaController.releaseFuture(mediaControllerFuture)
    MediaBrowser.releaseFuture(mediaBrowserFuture)
}

위의 내용은 안드로이드 MediaController 공식 문서에서 찾을 수 있다.

Activity전체 코드

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

결과

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

목록

Android Music 앱 만들기 (1) - 사전지식 및 구조

profile
기초 튼튼 개발자

0개의 댓글