무작정 앱만들기-6(ExoPlayer로 간단한 뮤직 플레이어를 만들어보자)

Aiden
21 min readAug 29, 2019

--

이번 포스팅의 최종 목표 화면

지난화에서는 어떤 작업을 했는가

지난화에서는

  1. Android 앱에 구글 맵을 띄우고,
  2. 지도데이터를 이용해 마커를 구글 맵에 추가했으며,
  3. 마커를 클릭할 때 나오는 InfoWindow을 커스텀 하였으며,
  4. InfoWindow에서 이미지 로드시 문제점을 해결하였다.

현재 만드는 앱에서 음악을 재생할 수 있는 기능이 필요하기 때문에, 간단하게 뮤직플레이어를 만들어보았다.

따라서 이번 포스팅에서는 현재 앱에서 사용되는 뮤직 플레이어를 구현하는 방법과 구현하면서 생각해야될 것들에 대해서 이야기 해보겠다.

뮤직플레이어는 사용자 행동에 따른 처리 사항과, 어떤 방식으로 만들 것이냐에 따라 많은 학습이 요구된다.

본인은 일단 최대한 간단하게 만드는 것이 목표이기 때문에, 기본적인 기능만을 갖춘 뮤직플레이어를 만들어 보겠다.

뮤직플레이어를 만들 때 MediaPlayer라는 Android native 라이브러리부터, 기타 여러 라이브러리를 사용할 수 있지만, 본인은 ExoPlayer라는 구글에서 만든 라이브러리를 사용하겠다.

사용 이유는 하단과 같다.

ExoPlayer supports features like Dynamic adaptive streaming over HTTP (DASH), SmoothStreaming and Common Encryption, which are not supported by MediaPlayer. It's designed to be easy to customize and extend.

한줄로 요약하면, MediaPlayer에서 지원하지 않는 기능들과 편하게 커스텀하고 확장할 수 있기 때문에 좋다는 것이다.

Android Studio 프로젝트에 ExoPlayer 세팅하기

상기 홈페이지는 ExoPlayer 공식 홈페이지이다. 친절히 설치하는 법부터 사용법까지 나와있으니, 이 포스팅에서 잘 되지 않는다면 공식 홈페이지를 참고하길 바란다.

일단 프로젝트 수준의 build.gradle 파일에

allprojects {
repositories {
google()
jcenter()
}
}

repositories에 google()과 jcenter()가 추가되었는지 확인한다.

이번에는 모듈 수준(앱)의 build.gradle 파일에 라이브러리를 추가해야한다.

implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'

dependencies에 위와 같이 넣으면 되는데, 저렇게 버전을 특정해주지 않으면 Android Studio가 warnning을 준다. 알아서 stable 버전으로 추천해주기 때문에 그 버전으로 추가하자.

우리는 간단하고 기본적인 기능과 UI 커스텀만 할 것이기 때문에 저 세개만 추가한 것이다. 추가적인 기능이 필요하다면 하단의 설명을 참고하자

  • exoplayer-core: Core functionality (required).
  • exoplayer-dash: Support for DASH content.
  • exoplayer-hls: Support for HLS content.
  • exoplayer-smoothstreaming: Support for SmoothStreaming content.
  • exoplayer-ui: UI components and resources for use with ExoPlayer.

추가로 본인이 Java 1.7 이하로 컴파일 하는 경우(default가 1.7) 모듈 수준(앱)의 build.gradle 파일에 android{} 섹션 하위에

compileOptions { 
targetCompatibility JavaVersion.VERSION_1_8
}

상단 문구를 추가해준다.

마지막으로 우리는 로컬에 음악파일을 저장하고 플레이 하는 방식이 아닌, 외부 url을 통해서 노래를 플레이 할 것이기 때문에 Menifest 파일에 인터넷 허용 코드를 넣어준다

<uses-permission android:name="android.permission.INTERNET" />

이제 ExoPlayer를 사용하기 위한 선행조건은 끝났다.

기본적인 뮤직 플레이어를 앱에 띄우기

일단 프로젝트를 만들었으면 기본적으로 MainActivity가 생성이 될 것이고 그에 따른 xml 파일도 만들어질 것이다. MainActivity의 xml 파일로 이동하자

<com.google.android.exoplayer2.ui.PlayerControlView
android:id="@+id/main_pcv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" />

root로 사용하는 layout 하위에 위처럼 PlayerControlView를 넣어준다. 위의 코드는 ConstraintLayout 하위에 넣은 코드이다. ConstraintLayout 하위의 저 코드를 넣어주면 하단에 PlayerControlView를 preview 화면에서 확인할 수 있을 것이다. MainActivity 코드로 가보자.

MainActivity의 코드는 위와 같이 구성되면 된다. 코드를 간단히 설명해보겠다. player를 전역 변수로 선언한 이유는 추후에 Android Life Cycle에 맞춰서 player를 다뤄야하기 때문이다. 그리고 우리가 플레이할 샘플 노래의 url을 전역변수로 선언해 주었다.

이후 onCreate 함수에 initializePlayer 함수를 추가하고, onCreated 함수 하단에 private으로 initializePlayer 함수를 정의해준다.

initializePlayer에서는 player가 null인지 체크하여, null인 경우 새로운 SimpleExoPlayer 객체를 만들어주고, 우리가 MainActivity xml파일에 만든 main_pcv라는 PlayerControlView에 player를 넣어준다.

이후 DefaultHttpDataSourceFactory를 사용해서 DataSourceFactory 객체를만들어 주었는데, DefaultDataSourceFactory를 사용해도 된다. 다만 차이점은 DefaultHttpDataSourceFactory가 Android의 HttpURLConnection을 사용한다는 점이 다를 뿐이다. DefaultHttpDataSourceFactory나 DefaultDataSourceFactory 모두 BaseDataSource를 상속받은 DataSource를 생성한다.

이후 ProgressiveMediaSource를 이용해서 mediaSource를 만들어주는데, 하단의 사용하는 protocol에 따라서 다른 것을 사용해주면 된다.

자 마지막으로 player의 prepare 함수를 통해서 mediaSource를 실행시키게 준비시키면 된다.

이제 앱을 실행시켜 보자(반드시 가상 머신이 아닌 실 기기에서 실행시킬 것)

어떤가? 하단에는 플레이어가 있고, 실행 버튼을 누르면 노래가 나올 것이다. 근데 몇가지의 문제를 발견할 수 있다.

  1. 플레이어가 몇초후에 사라질 것이고
  2. 홈버튼을 눌러 앱을 나가보아도 노래는 여전히 나올 것이다.

문제를 해결해보자!

플레이어가 사라지지 않게 유지시키는 방법

이것은 너무나 간단하다. 하단의 코드를 initializePlayer 함수에 하단의 코드를 추가하면 된다.

main_pcv.showTimeoutMs = 0

앱이 화면에 떠 있지 않을 때 노래를 끄는 방법

자 이문제는 조금 더 복잡하다. 일단 Android Life Cycle에 대한 이해가 필요하기 때문이다. 앱이 실행됨을 감지하는 onStart, onResume 때는 initializePlayer 함수를 호출한다. 앱이 화면 최우선에서 멀어지는 것을 감지하는 onStop, onPause 때는 곧 작성할 releasePlayer 함수를 호출하여 player를 멈출 것이다.

자 이제 MainActivity 코드로 돌아가보자. 전역변수에 아래 코드를 추가시켜준다.

private var playbackPosition = 0L
private var currentWindow = 0
private var playWhenReady = true

위의 변수들을 추가한 이유는, 재생 중간에 홈버튼을 눌러서 앱을 나갈 때, 지금 재생중인 곡의 정보들을 저장하기 위해서 필요하기 때문이다.

playbackPosition은 현재 곡의 재생 시간을, currentWindow는 재생목록을 사용할 때 현재의 재생곡 순번을, playWhenReady은 재생을 자동으로 할 것인지 안 할 것인지에 대한 정보를 저장할 것이다.

private fun releasePlayer() {
player?.let {
playbackPosition = it.currentPosition
currentWindow = it.currentWindowIndex
playWhenReady = it.playWhenReady
it.release()
player = null
}
}

이후 위와같이 releasePlayer 함수를 추가해준다. 이곳에서 위에서 만든 전역변수에 현재 재생곡의 재생시점, 재생목록의 현재곡 순번, 자동으로 실행할 것인지 말것인지 여부를 저장해주고, player의 release 함수를 호출하고 player를 null로 만든다.

자 이제 Android Life Cycle에 따라 작동할 수 있게, onStart, onResume, onStop, onPause 등의 함수들을 override하여 initializePlayer와 releasePlayer 함수를 호출해준다.

override fun onResume() {
super.onResume()
initializePlayer()
}

override fun onRestart() {
super.onRestart()
initializePlayer()
}

override fun onPause() {
super.onPause()
releasePlayer()
}

override fun onStop() {
super.onStop()
releasePlayer()
}

그리고 onResume 때 initializePlaye 함수가 호출되니 onCreate 함수에 있던 initializePlayer 함수를 제거해준다.

마지막으로 initializePlayer 함수에 내용을 추가해주어야한다.

player!!.prepare(mediaSource)
player!!.seekTo(currentWindow, playbackPosition)
player!!.playWhenReady = playWhenReady

prepare 함수 밑에 seekTo 함수로 재생목록상 현재곡의 순번과, 재생곡의 현재 위치를 설정해주고, playWhenReady에 자동으로 실행되게 끔 설정해주면된다.

최종적으로 완성된 MainActivity 코드는 하단과 같다.

한발짝 더 나아가보자

자 그러나 우리 코드는 여전히 문제를 가지고 있다.

  1. 멜론이나 벅스 등의 다른 음악 플레이어를 들으면서 우리 앱을 실행시켜 노래를 재생시켜보면 중복으로 노래가 나올 것이며
  2. 노래 실행중에 이어폰을 빼면 노래가 꺼지지 않을 것이다.

위의 두개는 우리가 뮤직 플레이어 앱을 사용하면서 기본으로 기대하는 기능들이다. 지금부터는 위 두 문제의 해결 방법에 대해서 이야기해보겠다.

노래 중복 문제 해결하기

노래 중복 문제는 audio focus 처리를 어떻게 해주느냐에 따라서 처리가 가능하다. 위의 링크는 audio focus 처리에 관련한 공식 문서이니 내 설명이 부족하다면 더 찾아보도록 하자.

audio focus는 다수의 앱에서 audio를 사용하는 경우 동시 출력되는 것을 통제하기 위해서 고안된 방법이다. 오로지 한개의 앱만 한번에 audio focus를 유지할 수 있다.

audio를 출력해야하는 경우, audio focus를 요청해야한다. audio focus를 가지고 있으면 audio가 출력되게 만들면 된다. 근데 다른 앱의 요청에 의해서 audio focus를 넘겨줄 때가 있을 것이다. 그때는 audio focus를 잃은 앱의 audio의 출력을 중지하거나, 음량을 줄여야 할 것이다.

사실 audio focus는 시스템이 강제할 수 있는 부분이 아니다. 단지 앱들이 그렇게 해줬으면 하길 바라는 가이드 라인일 뿐이다. 어떠한 앱이 audio focus를 잃었음에도 불구하고 audio 출력을 큰 음량으로 유지한다고 해도, 시스템적으로 막을 방법은 없다. 하지만 이러한 앱은 사용자들이 불편을 느낄테고, 결국 앱 삭제의 결과를 초래할 것이다. 따라서 audio focus 처리를 가이드라인으로 권고 하고 있는 것이다.

audio focus를 처리할 수 있는 방법은 min sdk version이 api level 26이상이냐 아니냐로 구분할 수 있다. 근데 과연 min sdk를 26이상으로 잡아놓고 있는 곳이 있을지가 의문이다. 그래서 26 미만이라고 가정하고 26미만일 때 어떻게 처리해야할지를 설명하겠다(밑의 방식대로 하면 deprecated라고 뜨지만, 그렇다고 해서 min sdk를 26 이상으로 올릴 수는 없기 때문에 이해하길 바란다)

하단의 함수를 추가하자.

상단의 코드를 설명하면, 시스템의 AudioManager를 가져와서, audio focus가 변화함에 따라 현재 노래를 중단시킬지, 재생시킬지를 결정한다.

위에서 상정한 경우는 3가지인데 더 있으니 참고 문서를 한번 볼 것을 강력히 권고한다.

  1. AudioManager.AUDIOFOCUS_LOSS : 알수 없는 기간동안 audio focus를 뺏긴 경우
  2. AudioManager.AUDIOFOCUS_LOSS_TRANSIENT : 일시적으로 audio focus를 뺏긴 경우
  3. AudioManager.AUDIOFOCUS_GAIN : audio focus를 얻은 경우

자 그리고 initializePlayer 함수에 setAudioFocus()를 넣어주자

최종적인 MainActivity 코드는 하단과 같을 것이다.

이제 멜론 같은 음악 플레이어에서 노래를 하나 틀어보고, 우리 앱을 켜보자. 그러면 정상적으로 멜론의 노래가 중지되고 우리 노래가 나오는 것을 확인할 수 있을 것이다.

이어폰을 뺐을 때 노래 중지 시키기

자 이제 이어폰을 뺐을 때, 노래를 중지시킬 수 있는 방법에 대해서 적어보겠다.

위 내용이 audio의 output이 달라졌을 때, 대응하는 방법에 대한 공식 문서이니 필요하다면 참고하길 바란다.

이어폰이 빠지든 , 블루투스 기기가 연결되었다가 해제되는 경우 등 갑자기 음악 소리가 핸드폰 스피커로 연결되어서 나온다면, 주변사람들에게 피해를 입히거나, 본인이 난처해질 수 있는 상황이 있을 것이다.

이를 막기 위해서는

  1. BroadcastReceiver를 만들어서 시스템 변화(audio output의 변화)를 감지하고 처리 코드를 추가
  2. 만든 BroadcastReceiver를 시스템에 등록하고, 사용하지 않을 때 시스템에서 제거

BroadcastReceiver는 시스템에 특정 이벤트를 감지할 수 있는 컴포넌트이다.

inner class MediaReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == intent.action) {
player?.playWhenReady = false
}
}
}

일단 MainActivity안에 BroadcastReceiver를 상속받은 MediaReceiver라는 이름의 inner class를 만들어 준다.

그리고 그 내부의 함수인 onReceive를 override 해준다. 이는 시스템이 보낸 이벤트(혹은 본인이 이벤트를 만들어서 보낼 경우)를 받았을 경우 호출되는 함수이다. intent의 action값이 디바이스에서 소리가 나게 되었다는 의미의AudioManager.ACTION_AUDIO_BECOMING_NOISY와 같을 경우를 우리는 처리하면된다. 이때 player의 플레이를 중지시키는 코드를 넣었다.

private val mediaReceiver = MediaReceiver()

이후 MainActivity의 전역변수로 mediaReceiver를 선언하고 만들어준 inner class인 MediaReceiver를 생성한다.

그리고 Android Life Cycle에 따라 onResume에는

registerReceiver(mediaReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY))

위 registerReceiver 함수를 호출해 준다.

unregisterReceiver(mediaReceiver)

그리고 onPause에는 위처럼 receiver를 해제해주는 함수를 호출한다.

최종 코드는 아래와 같을 것이다.

플레이어 커스터마이징 하기

자 대망의 마지막 과정이다. 우리의 플레이어는 지금 ExoPlayer가 제공하는 기본 ui를 사용하고 있다. 이걸 간단히 커스텀해보겠다.

우리가 해야할 일은

  1. custom할 플레이어 xml 파일을 만들고
  2. MainActivity xml파일의 ExoPlayer PlayerControlView에 연결해주면된다.

일단 custom할 xml파일을 custom_music_player.xml의 이름으로 다음과 같이 만들어준다.

위의 코드 10번째 줄을보면 android:id=”@id/exo_progress” 와 같이 id값을 넣는 부분이 약간 다른것을 볼 수 있다. 저렇게 넣는 방식으로 하면, exo player의 기능을 저 뷰에 넣어준다는 것이다.

<item name="exo_ad_overlay" type="id"/>
<item name="exo_artwork" type="id"/>
<item name="exo_buffering" type="id"/>
<item name="exo_content_frame" type="id"/>
<item name="exo_controller" type="id"/>
<item name="exo_controller_placeholder" type="id"/>
<item name="exo_duration" type="id"/>
<item name="exo_error_message" type="id"/>
<item name="exo_ffwd" type="id"/>
<item name="exo_next" type="id"/>
<item name="exo_overlay" type="id"/>
<item name="exo_pause" type="id"/>
<item name="exo_play" type="id"/>
<item name="exo_position" type="id"/>
<item name="exo_prev" type="id"/>
<item name="exo_progress" type="id"/>
<item name="exo_repeat_toggle" type="id"/>
<item name="exo_rew" type="id"/>
<item name="exo_shuffle" type="id"/>
<item name="exo_shutter" type="id"/>
<item name="exo_subtitles" type="id"/>
<item name="exo_vr" type="id"/>

위 리스트는 ExoPlayer에서 제공하는 id 값들이다. 이름을 참고해서 필요한 기능들을 연결해 주도록 하자.

다시 코드로 돌아오면, DefaultTimeBar는 음악 재생 시간 관련 뷰이다.

하단의 두 TextView는 노래 제목과 가수이름이다. 원하는 데로 넣어도 되고, 나중에 코드에서 setText와 같은 함수를 이용해서 동적으로 바꾸어도 된다.

그 밑에 있는 LinearLayout은 플레이를 컨트롤하는 버튼들을 넣었다. 뒤로감기, 재생, 일시정지, 빨리감기 총 4개이다. 여기에서도 DefaultTimeBar의 id처럼 특이하게 exo 플레이어의 id값들을 넣어준다.

추가적으로 style속성이 보일 것이다. 본인이 재생이나 빨리감기 등 기능 버튼에 대한 디자인이 있다면 그걸 쓰면되고, 없으면 위 코드처럼 ExoPlayer에서 제공하는 기본적인 디자인을 사용하면 된다.

저렇게 만들면 재생버튼과 일시정지 버튼이 둘다 있을 것 처럼 보이지만, 재생버튼이 활성화되어있을 때는 일시정지 버튼이 없어지고, 일시정지 버튼이 활성화 되면 재생버튼이 사라진다. 그래서 버튼은 언제나 3개만 나온다.

마지막으로 activity_main.xml 파일로 간다. PlayerControlView에

app:controller_layout_id="@layout/custom_music_cotroller"

위 속성을 추가해준 뒤, 앱을 실행시켜보자. 그러면 커스텀된 player가 잘 나오는 것을 확인할 수 있을 것이다.

마무리

이번 포스팅은

  1. ExoPlayer를 프로젝트에 설정하는 방법과
  2. ExoPlayer로 간단한 뮤직 플레이어를 만들고,
  3. 뮤직 플레이어를 개발할 때 생기는 여러 문제에 대한 대처법과
  4. 뮤직 플레이어를 커스터마이징하는 방법

총 4가지를 다루어 보았다.

사실 제대로 된 뮤직 플레이어를 만드려면 더 많은 것들을 개발하고, 예외처리도 더 많이 해야하며, 구조적으로도 많은 분리가 필요하다.

오늘 포스팅된 예제를 토대로 더 발전시킨 뮤직 플레이어를 만들 수 있으면 좋겠다.

--

--

Aiden

안드로이드 개발자(개인 공부용도의 블로그)