devlog of ShinJe Kim

[Android] MediaSession이란?

|

이 글은 Android Developers의 Medium 글인 Understanding MediaSession안드로이드 공식문서를 공부하며 정리한 글입니다.

MediaSession이란 무엇인가?

Android Developers Medium에서 정의된 바는 아래와 같습니다.

MediaSession이란 outside actors가 애플리케이션의 미디어 플레이어를 제어할 수 있도록 해주는 미들웨어(middleware)입니다.

outside actors란? 애플리케이션의 외부에서 미디어를 제어하는 것들(“things”)을 “outside actors”라고 합니다.

MediaSession은 애플리케이션이 플레이의 상태(어떤 미디어가 load되었는지, 플레이어가 현재 중단되었는지 재생중인지 등)를 다른 interested parties나 애플리케이션의 내/외부로 report할 수 있도록 해줍니다.

MediaSession의 역할은 다음과 같습니다.

  • Playback Control

    앱 내의 UI나 외부의 actors(예: 미디어 버튼, 구글 어시스턴트 등)가 제어할 수 있는 단일 인터페이스(single interface)를 제공합니다.

  • State Sync

    현재의 재생 상태(playing, paused, stopped, etc)와 미디어 메타데이터(album art, song duration, song title, etc)를 외부의 actors와 애플리케이션 자체에 broadcast합니다.

사용자들은 애플리케이션의 UI를 사용하는 대신 구글 어시스턴트에 “음악 꺼줘”라고 말하며 음악을 끄고 싶을 수도 있습니다. 또한 앱의 UI를 사용하지 않고 헤드폰의 미디어키를 사용함으로써 음악을 재생/중단하고 싶을 수도 있습니다. 이를 구현하기 위해서는 outside actors(구글 어시스턴드, 헤드폰의 미디어키 등 애플리케이션의 내부 요소가 아닌 것들)가 애플리케이션 내부의 상태를 변화시켜야만 하는데, 이 때 MediaSession이 필요합니다.

여기서 주의해야 할 것은 MediaSession은 MediaPlayer의 역할을 하지 않는다는 것입니다. MediaSession은 앱의 MediaPlayer를 관리해주지 않습니다. 즉, MediaSession을 사용하더라도 MediaPlayer를 생성하고, load하고, 재생을 위해 prepare하는 등의 모든 작업을 (당연히) 해야 합니다.

일반적으로 하나의 애플리케이션에는 모든 재생을 통틀어 오직 하나의 MediaSession만 필요하지만, 미디어를 보다 세밀하게 제어하기 위해 여러개의 세션을 만들 수도 있습니다.

참고 문헌

[TIL] 2019-10-11

|

Today I Learned

안드로이드 서비스를 공부하고 있는데 이 Service라는 것이 대체 무엇인지 명확하게 이해가 되지 않는다. CTO님이 안드로이드 용어 공부를 할 때에는 운영체제에서의 정의와 안드로이드에서의 정의를 구분하여 이해하라고 말씀해주셨다. 그래서 각각의 정의를 찾아보았다. 사실 아직도 완벽히 머리에서 정리되지는 않는다.. 반복만이 살길이다

운영체제에서의 서비스란?

찾아본 정의를 모두 나열해보았다.

  • 운영체제에서 응용 프로그램을 실행할 때, 응용 프로그램이 필요로 하는 공통적인 기능들을 서비스라는 형태로 만들어 제공해주는 것. 프로세스, 메모리, 파일 등 플랫폼의 자원 관리도 해준다.
  • 시스템에서 동시에 실행되는 다른 프로세스들 간의 보호를 강화하는 것.
  • 근본적인 하드웨어를 통해서는 직접 제공되지 않는 새로운 기능성을 제공하는 것.

서버는 반드시 백그라운드 서비스로 만들어서 돌려야 한다. 만약 그렇지 않으면 터미널을 끄면 서버가 죽어버린다. 이를 방지하기 위해서는 키보드/모니터 등의 장치와 연결되지 않는 별도의 세션을 만들어야하는데 이렇게 만드는 것을 데몬화라고 한다.

운영체제에서의 세션이란 프로세스들 사이에서 통신을 하기 위해 메시지 교환을 통해 서로를 인식한 이후부터 통신을 마칠 때까지의 기간이라고 한다. 이 때, 세션이 끊기지 않는 서비스들을 데몬이라고 부르기도 하고 서비스라고 부르기도 한다(예: 키보드 입력이 끊기지 않는 것).

아래는 OS에서 제공하는 서비스 기능들이다.

  • User Interface
  • Program execution
  • I/O operation
  • File system manipulation
  • Communications
  • Error detection
  • Resource allocation
  • Accounting
  • Protection and Security

그래서 운영 체제에서의 서비스가 한 마디로 뭐예요? 라고 누군가 나에게 물어본다면, 음.. 아직 잘 모르겠다ㅠ 한번에 완벽히 이해하는게 더 이상한 것일거야.. 집에 가서 운영체제 책 보고 다시 정리해야지.

안드로이드에서의 서비스란?

우리가 ‘안드로이드 서비스 개발해요’라고 말 할 때의 서비스는 기반 환경 및 플랫폼을 의미한다. 그렇다면 내가 지금 공부하고 있는 안드로이드 컴포넌트로서의 서비스는 무엇인가? 블로그에 열심히 정리하였지만 무어라 한마디로 정의하기가 어렵다(머릿속에서 정리가 안되었기 때문이겠지).

Service 공식 문서의 정의는 아래와 같다.

애플리케이션에서 백그라운드에서 실행해야 할 작업이 있을 때 시스템에 알려주며, 애플리케이션의 일부 기능을 다른 애플리케이션에 노출시킬 수 있는 기능을 제공한다.

Service Overview 공식 문서에서 정의하는 바는 아래와 같다. 조금 더 이해하기 쉬운 정의이다.

서비스란 백그라운드에서 오래 실행되는 작업을 수행하도록 하는 애플리케이션의 컴포넌트를 의미한다.

그럼 공식 문서에서 포그라운드(foreground)도 서비스의 한 유형이라고 설명하는건 뭐야? 라며 의문을 가졌었다. 여기서 말하는 포그라운드는 운영체제상의 포그라운드를 의미하는 것이 아니라 안드로이드 내에서 정의내린 포그라운드를 의미한다. 공식 문서의 정의를 살펴보자.

포그라운드

포그라운드 서비스는 사용자에게 잘 보이는 몇몇 작업을 수행합니다. 예를 들어 오디오 앱이라면 오디오 트랙을 재생할 때 포그라운드 서비스를 사용합니다. 포그라운드 서비스는 알림(Notification)을 표시해야 합니다. 포그라운드 서비스는 사용자가 앱과 상호작용하지 않을 때도 계속 실행됩니다.

그러니깐 실제로 백그라운드에서 동작하는 오디오 재생도 알림(Notification)을 표시하면 안드로이드에서는 포그라운드 서비스로 정의된다는 것이다.

이 정의를 머릿속에 각인시키면서 Service 공식문서를 정리한 내용을 반복하여 읽어보자.

[Android] Service란?

|

이 글은 안드로이드 공식문서를 공부하며 번역 및 요약한 글입니다.

Service

abstract class Service : ContextWrapper, ComponentCallbacks2

안드로이드에서의 서비스(Service)는 유저와 상호작용하지 않으면서 시간이 상대적으로 오래 걸리는 작업을 수행하거나, 다른 애플리케이션에서 사용할 기능을 제공하는 역할을 수행하는 애플리케이션 컴포넌트입니다. 각 서비스 클래스는 패키지의 AndroidManifest.xml 파일에 적절한 <service> 선언을 가지고 있어야합니다. 서비스는 Contenxt.startService()Context.bindService()로 시작할 수 있습니다.

서비스는 다른 애플리케이션 객체처럼 호스팅 프로세스의 메인 스레드에서 실행됩니다. 즉, 서비스가 CPU 집약적(MP3 재생 등)이거나 차단(네트워킹 등)을 하는 작업을 수행하려는 경우에는 해당 작업을 수행할 자체 스레드를 생성해야합니다. 이것과 관련된 더 많은 정보는 Processes and Threads에서 확인할 수 있습니다. IntentService 클래스는 서비스의 표준 구현으로 사용될 수 있으며, 작업이 완료되는 순간 자체적인 스레드를 가집니다.

서비스란 무엇인가?

서비스가 무엇인지에 대한 혼란스러움은 대부분 무엇이 서비스가 아닌지에 대한 것입니다.

  • 서비스는 별개의 프로세스가 아닙니다. 서비스 객체는 그 자체로 실행을 암시하거나 자체적인 프로세스를 가지지 않습니다; 특별히 지정된것이 아니라면, 애플리케이션이 실행되고 있는것과 동일한 스레드에서 실행됩니다.
  • 서비스는 스레드가 아닙니다. ANR(Application Not Responding) 에러를 피하기 위해 메인 스레드에서 작업을 수행하는 수단 자체는 아닙니다.

서비스 그 자체는 매우 간단하며, 아래의 두 가지 주요 기능을 가지고 있습니다.

  • 애플리케이션이 백그라운드에서 수행하려는 작업에 대해 시스템에 알려주는 기능(사용자가 애플리케이션가 직접적으로 상호작용하지 않더라도):

    이는 서비스 혹은 다른 사람이 명시적으로 중단(stop)할 때까지 시스템에서 서비스 작업을 예약하도록 요청하는 Context.startService() 호출에 해당합니다.

  • 애플리케이션의 일부 기능을 다른 애플리케이션에 노출시키는 기능:

    이는 Context.bindService() 호출에 해당하며, 이로 인해 서비스와 상호작용하기 위한 long-standing connection을 할 수 있습니다.

이러한 이유로 인해 서비스 컴포넌트가 생성될 때 시스템이 실제로 하는 것은 컴포넌트를 인스턴스화하고 메인스레드에서 onCreate와 다른 적절한 콜백을 호출하는 것입니다. 보조 스레드(secondary thread)를 생성하는 것과 같은 적절한 동작을 구현하는 것은 서비스에 달려있습니다.

Service 자체는 아주 단순하기 때문에 만들고 싶은 상호작용을 원하는 만큼 간단하게 혹은 복잡하게 만들 수 있습니다: from treating it as a local Java object that you make direct method calls on (as illustrated by Local Service Sample), to providing a full remoteable interface using AIDL.

서비스의 유형

서비스에는 세 가지 유형이 있습니다

포그라운드(Foreground)

포그라운드 서비스는 사용자에게 보여지는 여러 작업을 수행합니다. 예를 들어 오디오 앱이라면 오디오 트랙을 재생할 때 포그라운드 서비스를 사용합니다. 포그라운드 서비스는 알림(Notificaiton)을 표시해야 합니다. 포그라운드 서비스는 사용자가 앱과 상호작용하지 않을 때에도 계속 실행됩니다.

백그라운드(Background)

백그라운드 서비스는 사용자에게 직접 보이지 않는 작업을 수행합니다. 예를 들어 어떠한 앱이 저장소를 압축하는 데 서비스를 사용했다면 이것은 대부분 백그라운드 서비스입니다.

참고: 앱이 API 레벨 26 이상을 대상으로 한다면 앱이 포그라운드에 있지 않을 때 시스템에서 백그라운드 서비스 실행에 대한 제한을 적용합니다. 이와 같은 경우에서는 대부분 앱이 예약된 작업을 사용해야 합니다.

바인드(Bound)

애플리케이션 구성 요소가 bindService()를 호출하여 해당 서비스에 바인딩되면 서비스가 바인딩됩니다. 바인딩된 서비스는 클라이언트-서버 인터페이스를 제공하여 구성 요소가 서비스와 상호작용하게 하며, 결과를 받을 수도 있고 심지어 이와 같은 작업을 여러 프로세스에 걸쳐 프로세스 간 통신(IPC)으로 수행할 수도 있습니다. 바인딩된 서비스는 또 다른 애플리케이션 구성 요소가 이에 바인딩되어 있는 경우에만 실행됩니다. 여러 개의 구성 요소가 서비스에 한꺼번에 바인딩될 수 있지만, 이 모든 것에서 바인딩이 해제되면 해당 서비스는 소멸됩니다.

기본 사항

서비스를 생성하기 위해서는 Service의 하위 클래스(혹은 이것의 기존 하위 클래스 중 하나)를 생성해야 합니다. 구현에서는 서비스 수명 주기의 주요 측면을 처리하는 콜백 메서드를 몇 가지 재정의해야 하며 서비스에 바인딩할 구성 요소에 대한 메커니즘을 제공해야 합니다(해당되는 경우). 서비스를 구현할 때 오버라이딩해야하는 가장 중요한 콜백 메서드는 아래와 같습니다.

onStartCommand()

시스템이 이 메서드를 호출하는 것은 또 다른 구성 요소(예: 액티비티)가 서비스를 시작하도록 요청하는 경우입니다. 이때 startService()를 호출하는 방법을 씁니다. 이 메서드가 실행되면 서비스가 시작되고 백그라운드에서 무한히 실행될 수 있습니다. 이것을 구현하면 서비스의 작업이 완료되었을 때 해당 서비스를 중단하는 것은 개발자 본인의 책임이며, 이때 stopSelf() 또는 stopService()를 호출하면 됩니다. 바인딩만 제공하고자 하는 경우, 이 메서드를 구현하지 않아도 됩니다.

onBind()

시스템은 다른 구성 요소가 해당 서비스에 바인딩되고자 하는 경우(예를 들어 RPC를 수행하기 위해)에도 이 메서드를 호출합니다. 이때 bindService()를 호출하는 방법을 사용합니다. 이 메서드를 구현할 때에는 클라이언트가 서비스와 통신을 주고받기 위해 사용할 인터페이스를 제공해야 합니다. 이때 IBinder를 반환하면 됩니다. 이 메서드는 항상 구현해야 하지만, 바인딩을 허용하지 않으려면 null을 반환해야 합니다.

onCreate()

시스템은 서비스가 처음 생성되었을 때(즉 서비스가 onStartCommand() 또는 onBind()를 호출하기 전에) 이 메서드를 호출하여 일회성 설정 절차를 수행합니다. 서비스가 이미 실행 중인 경우, 이 메서드는 호출되지 않습니다.

onDestroy()

시스템이 이 메서드를 호출하는 것은 서비스를 더 이상 사용하지 않고 소멸시킬 때입니다. 서비스는 스레드, 등록된 리스너 또는 수신기 등의 각종 리소스를 정리하기 위해 이것을 구현해야 합니다. 이는 서비스가 수신하는 마지막 호출입니다.

서비스 라이프사이클(Service Lifecycle)

서비스가 시스템에 의해 실행될 수 있는 두 가지 이유가 있습니다. 만약 Context.startService()를 호출한다면 시스템은 서비스를 가져와(필요하다면 서비스를 생성하여 onCreate 메소드를 호출할 것입니다) 클라이언트가 제공한 인자로 onStartCommand 메소드를 호출할 것입니다. 이 시점에서 서비스는 Context.stopService() 혹은 stopSelf()가 호출되기 전까지 계속 실행될 것입니다. Context.startService()에 대한 여러 호출은 중첩되지 않으므로 서비스가 시작된 횟수와는 관계 없이 Context.stopService() 혹은 stopSelf()가 호출되면 서비스는 중단(stop)될 것입니다; 그러나 서비스는 stopSelf(int) 메소드를 사용하여 start intent가 실행되기 전까지 서비스가 중단되지 않도록 할 수 있습니다.

시작된 서비스의 경우, onStartCommand()에서 리턴하는 값에 따라 실행할 수 있는 두 가지 주요 operation mode가 있습니다: START_STICKY는 필요에 의해 명시적으로 시작되고 중단되는 서비스에 사용됩니다. 반면에 START_NOT_STICKY 혹은 START_REDELIVER_INTENT는 전송된 명령을 처리하는 동안에만 서비스가 실행되어야 하는 경우에 사용됩니다.

클라이언트는 서비스에 지속적으로 연결하기 위해 Context.bindService()를 사용할 수 있습니다. 이는 마찬가지로 서비스가 아직 실행중이 아니지만(onCreate를 호출하는 동안) onStartCommand()를 호출하지 않은 경우에 서비스를 생성(create)합니다. 클라이언트는 서비스가 onBind 메소드에서 리턴하는 android.os.IBinder 객체를 수신하여 클라이언트가 다시 서비스를 호출할 수 있도록 해줍니다. 서비스는 연결(connection)이 설정되는 한 계속 실행될것입니다(서비스의 IBinder에 대한 참조를 클라이언트가 가지고 있는지의 여부와는 상관없습니다). 리턴된 IBinder는 일반적으로 aidl로 작성된(written in aidl) 복합 인터페이스(complex interface)로 리턴됩니다.

서비스는 시작될 수도 있고 서비스에 연결이 바인딩 될 수도 있습니다. 이 경우에 시스템은 서비스가 시작되거나, 혹은 Context.BIND_AUTO_CREATE와 하나 이상의 연결이 있는 한 서비스를 계속 실행하도록 합니다. 두 가지 상황 모두 발생하지 않으면 서비스의 onDestroy 메소드가 호출되고, 서비스가 효과적으로 종료(terminate)됩니다. onDestroy() 반환이 완료되면 모든 정리(스레드 중지, unregistering receivers)가 완료되어야만 합니다.

참고 문헌

[Android] 미디어 플레이어(MediaPlayer) 개요

|

이 글은 안드로이드 공식문서를 공부하며 번역한 글입니다.

MediaPlayer 클래스는 오디오 및 비디오 파일과 스트림 재생을 위해 쓰입니다. MediaPlayer 클래스의 메소드를 쓰는 방법은 VideoView에서 볼 수 있습니다.

상태 다이어그램(State Diagram)

오디오 및 비디오 파일 스트림 재생은 상태 머신(state machine)으로 관리됩니다. 아래의 다이어그램은 지원되는 재생 제어 작업(palyback control operations)으로 구동되는 수명주기(lifecycle)및 MediaPlayer 객체의 상태를 보여줍니다. 타원은 MediaPlayer 객체가 상주할 수 있는 상태를 나타냅니다. 아크(arc)는 객체의 상태 전환을 구동하는 재생 제어 작업을 나타냅니다. 아크(arc)에는 두 가지 유형이 있습니다. 단일 화살촉(single arrow head)을 가진 아크(arc)는 동기 함수 호출을 나타내고, 이중 화살촉(double arrow head)이 있는 아크(arc)는 비동기식 함수 호출을 나타냅니다.

mediaplayer-state-diagram

The basics

안드로이드 프레임워크에서 소리와 영상을 재생하기 위해 사용되는 클래스는 아래와 같습니다.

  • MediaPlayer

    소리와 영상을 재생하기 위한 기본 API입니다.

  • AudioManager

    기기의 오디오 소스와 오디오 출력을 관리하는 클래스입니다.

매너페스트 선언하기(Manifest declarations)

애플리케이션에서 MediaPlayer를 사용하기 위해서는 매너페스트 파일에 적절한 선언을 해주어야 합니다.

  • Internet Permission - 스트림 네트워크 기반의 MediaPlayer를 사용한다면 애플리케이션이 네트워크 접근을 요청해야만 합니다.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

MediaPlayer 사용하기(Using MediaPlayer)

미디어 프레임워크의 가장 중요한 컴포넌트 중 하나는 MediaPlayer 클래스입니다. 이 클래스의 객체는 최소한의 세팅으로 오디오와 비디오 모두를 가져오고(fetch), 디코딩하고(decode), 재생(play)할 수 있습니다. 이 클래스는 아래와 같은 여러 종류의 미디어를 지원합니다.

  • 로컬 리소스(Local resources)
  • 내부의 URI(Internal URIs, such as one you might obtain from a Content Resolver)
  • 외부의 URL(External URLs (streaming))

Supported Media Formats 페이지에서 안드로이드에서 지원하는 미디어 형식을 확인할 수 있습니다.

아래는 로컬의 raw 리소스(당신의 앱의 res/raw/ 디렉토리에 위치한 리소스)를 재생하도록 하는 예시 코드입니다.

var mediaPlayer: MediaPlayer? = MediaPlayer.create(context, R.raw.sound_file_1)
mediaPlayer?.start() // prepare()를 호출할 필요가 없습니다; create()가 같은 역할을 해줍니다.

위의 예시에서 “raw” 리소스는 시스템이 특정한 방식으로 파싱할 필요가 없는 파일입니다. 하지만 이 리소스의 컨텐츠가 raw 오디오여서는 안됩니다. 리소스의 컨텐츠는 안드로이드에서 지원되는 양식에 따라 적절한 방식으로 인코딩되고 포맷팅 된 미디어 파일이어야합니다.

아래는 로컬 시스템의 URI에서(예를 들어 Content Resolver를 통해 얻은) 미디어를 재생하는 예제 코드입니다.

val myUri: Uri = .... // 이곳에서 Uri를 초기화합니다.
val mediaPlayer: MediaPlayer? = MediaPlayer().apply {
    setAudioStreamType(AudioManager.STREAM_MUSIC)
    setDataSource(applicationContext, myUri)
    prepare()
    start()
}

HTTP 스트리밍을 통해 리모트 URL로부터 미디어를 재생하는 방법은 아래와 같습니다.

val url = "http://........" // 이곳에 URL을 입력합니다.
val mediaPlayer: MediaPlayer? = MediaPlayer().apply {
    setAudioStreamType(AudioManager.STREAM_MUSIC)
    setDataSource(url)
    prepare() // 오랜 시간이 걸릴 수도 있습니다! (buffering 혹은 기타 등등)
    start()
}

중요: 온라인상의 미디어 파일을 스트림하기 위한 URL을 전달한다면, 해당 파일은 progressive download가 가능한 파일이어야만 합니다.

주의: setDataSource()를 사용할때에는 IllegalArgumentExceptionIOException을 catch하거나 pass하도록 해야합니다. 참조하고있는 해당 파일이 존재하지 않을 수도 있기 때문입니다.

비동기 준비(Asynchronous preparation)

MediaPlayer를 사용할 때에 주의해야할 점이 몇 가지 있습니다. 예를 들면, prepare() 호출은 미디어 데이터를 가져오고 디코딩하는 과정을 포함할 수 있기때문에 실행하는 데에 시간이 많이 걸릴 수 있습니다. 따라서 이와 같이 실행하는데에 시간이 많이 걸릴 수 있는 메서드는 절대 애플리케이션의 UI 스레드에서 호출하면 안됩니다. 만약 UI 스레드에서 시간이 오래 걸리는 메서드를 호출한다면, 해당 메서드가 리턴될때까지 UI가 멈춰있게되며 이는 매우 나쁜 사용자 경험을 제공합니다. 또한 이는 ANR(Application Not Responding) 에러를 발생시킬 수도 있습니다. 만약 당신의 리소스가 빨리 다운로드 될 것이라고 기대할지라도, UI 응답이 1/10초 이상 걸리는 것은 일시적으로 정지된다는 것을 감지할 수 있는 정도이며 이는 사용자에게 앱이 느리다는 인상을 줄 수 있습니다.

이를 방지하기 위해서는 MediaPlayer를 위한 별도의 스레드를 만들어서 작업이 완료되었을 때 메인스레드에 알려주어야합니다. 이 로직을 직접 작성할 수도 있지만 안드로이드 프레임워크에서 제공하는 prepareAsync()라는 메서드를 사용하면 간편하게 세팅할 수 있습니다. 이 메서드는 백그라운드에서 미디어 준비를 시작하고 바로 리턴합니다. 미디어 준비가 완료되면 setOnPreparedListener()를 통해 구성된 MediaPlayer.OnPreparedListeneronPrepared() 메서드가 호출됩니다.

상태 관리하기(Managing state)

MediaPlayer에서 기억해야 할 또 다른 중요한 것은 미디어 플레이어는 상태 베이스(state-based)라는 것입니다. 즉, MediaPlayer는 내부적으로 ‘상태’를 가지고 있으며 특정 operation은 플레이어가 특정 상태일때만 유효하므로, 코드를 작성할 때 유의해야 합니다. 만약 잘못된 상태일 때 operation을 수행하려 한다면 시스템이 예외를 발생시키거나 바람직하지 않은 동작을 발생시킬 수도 있습니다.

MediaPlayer 클래스의 공식 문서에는 한 상태에서 다른 상태로 이동할 어떤 메서드가 MediaPlayer를 움직이는지 보여주는 상태 다이어그램이 나와있습니다. 예를 들면, 새로운 MediaPlayer를 생성할때에 MediaPlayer는 Idle 상태에 있습니다. 이 시점에서 setDataSource()를 호출하고 초기화하여 initialized 상태로 옮겨가야합니다. 이후에는, prepare() 혹은 prepareAsync() 메서드를 사용하여 MediaPlayer를 준비해야합니다. MediaPlayer의 준비가 완료되면 Prepared 상태에 돌입하게 되는데, 이는 미디어를 재생하기 위한 start()를 호출할 수 있다는 의미입니다. 이 시점에서, 다이어그램이 보여주듯이, start(), pause(), seekTo()와 같은 메서드를 호출하여 Started, Paused, PlaybackCompleted의 상태들 사이를 이동할 수 있습니다. 하지만 한 번 stop()을 호출하고 나면 MediaPlayer를 다시 준비하기 전까지는 start()를 호출할 수 없습니다.

MediaPlayer 객체와 상호작용하는 코드를 작성할 때에는 항상 상태 다이어그램을 염두에 두세요. 잘못된 상태에서 메서드를 호출하는 것은 자주 발생하는 버그의 원인입니다.

서비스에서 MediaPlayer 사용하기(Using MediaPlayer in a service)

애플리케이션이 스크린에서 보이지 않을때에도 백그라운드에서 앱이 동작하도록 만들고 싶다는 것은, 사용자가 다른 애플리케이션과 상호작용 할 때에도 당신의 애플리케이션의 미디어가 재생되게 하고 싶다는 것입니다. 그렇게 하기 위해서는 우선 Service를 시작한 다음 여기에서 MediaPlayer 인스턴스를 제어해야만 합니다. MediaPlayer를 MediaBrowserServiceCompat에 임베드하여 다른 액티비티의 MediaBrowserCompat과 상호작용 하도록 해야합니다.

이 때 클라이언트/서버 설정에 주의해야합니다. 백그라운드 서비스에서 실해중인 플레이어가 시스템의 다른 부분과 어떻게 상호작용 하는지에 대한 기대(expectations)가 있습니다. 만약 당신의 애플리케이션이 이러한 기대를 충족하지 못한다면 사용자에게 좋지 않은 경험을 줄 수 있습니다. 자세한 내용은 Building an Audio App을 참조하시면 됩니다.

이 섹션에서는 서비스 내부에서 MediaPlayer를 구현할 때 이를 관리하기 위한 지침에 대해 다룹니다.

비동기로 실행하기(Running asynchronously)

우선, Activity처럼, Service에서의 모든 작업은 기본적으로 싱글 스레드로 이뤄집니다. 즉, 동일한 애플리케이션 내에서 액티비티와 서비스를 실행한다면 디폴트로 동일한 스레드(메인 스레드)를 사용하게될 것입니다. 따라서 서비스는 들어오는 intent를 빠르게 처리해야하며, intent에 응답할 때 긴 계산(lengthy computations)을 수행하지 않아야 합니다. 무거운 작업이나 호출 차단(blocking calls)이 예상되는 경우에는 직접 구현하거나 프레임워크에서 제공하는 비동기 프로세스를 사용하여 비동기적으로 처리해야합니다.

예를 들어, MediaPlayer를 메인 스레드에서 사용하는 경우에는, 준비가 완료된 후 재생을 시작하기 위한 알림을 받기 위해서는 prepare() 대신 prepareAsync()를 사용해야하며, MediaPlayer.OnpreparedListener를 구현해야만합니다.

아래는 예제 코드입니다.

private const val ACTION_PLAY: String = "com.example.action.PLAY"

class MyService: Service(), MediaPlayer.OnPreparedListener {

    private var mMediaPlayer: MediaPlayer? = null

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        ...
        val action: String = intent.action
        when(action) {
            ACTION_PLAY -> {
                mMediaPlayer = ... // 여기서 MediaPlayer를 초기화합니다.
                mMediaPlayer?.apply {
                    setOnPreparedListener(this@MyService)
                    prepareAsync() // 메인스레드를 block 하지 않기 위해 비동기를 준비합니다.
                }
            }
        }
        ...
    }

    /** MediaPlayer가 준비되었을 때 호출됩니다.*/
    override fun onPrepared(mediaPlayer: MediaPlayer){
        mediaPlayer.start()
    }
}

비동기 오류 처리하기(Handling asynchronous errors)

동기로 작업할때에는 일반적으로 오류는 에러 코드로 알 수 있습니다. 하지만 비동기를 사용한다면 애플리케이션에서 에러를 적절히 알 수 있는지 확인해야만 합니다. MediaPlayer의 경우, MediaPlayer.OnErrorListener를 구현하고 MediaPlayer 인스턴스에 설정함으로써 이를 해결할 수 있습니다.

class MyService : Service(), MediaPlayer.OnErrorListener {
    private var mediaPlayer: MediaPlayer? = null

    fun iniMediaPlayer() {
        // 여기에서 MediaPlayer를 초기화합니다.
        mediaPlayer?.setOnErrorListener(this)
    }

    override fun onError(mp: MediaPlayer, wha t: Int, extra: Int): Boolean {
        // ... react appropriately ...
        //  MediaPlayer가 오류 상태로 접어들었음로 리셋해야합니다.
    }
}

오류가 발생했을 때, MediaPlayerError 상태로 접어들었다는 것을 기억하는 것이 중요합니다(MediaPlayer 클래스의 전체 상태 다이어그램을 참조하세요). 오류 상태가 되었다면 미디어 플레이어를 다시 사용하기 전에 꼭 리셋해주어야합니다.

Using wake locks

백그라운드에서 미디어를 재생하는 애플리케이션을 설계할 때, 서비스를 실행하는 동안 기기(device)가 절전 모드(sleep)로 전환될 수 있습니다. 안드로이드 시스템은 배터리 사용량을 최소화하려 하기 때문에, 기기가 잠자고 있을 때에는 CPU와 와이파이 하드웨어를 포함하여 불필요한 기능들을 끄도록 되어있습니다. 그러나 음악 재생이나 스트리밍 서비스를 만들려면, 이러한 시스템이 재생을 방해하지 않도록 하고 싶을 것입니다.

위와 같은 상황에서 당신의 서비스가 계속 실행되도록 보장하려면 “wake locks”를 사용해야합니다. wake lock은 핸드폰이 idle 상태이더라도 애플리케이션에서 특정 기능을 사용할 수 있도록 시스템이 특정 신호를 보내는 방식입니다.

주의: wake lock은 기기의 배터리 수명을 상당히 단축시키기때문에, 정말 필요할때에만 최소한으로 사용하고 유지해야합니다.

CPU가 MediaPlayer를 재생하고 있는 동안에도 실행되고 있다는 것을 보장하려면, MediaPlayer를 초기화할 때 setWakeMode() 메소드를 호출하면 됩니다. 이렇게 하면, MediaPlayer는 재생하는 동안 지정된 잠금을 유지하고 일시중지(pause) 중단(stop)되면 잠금을 해제합니다.

mediaPlayer = MediaPlayer().apply {
    // 여기에서 기타 다른 것들을 초기화합니다.
    setWakeMode(applicationContext, PowerManager.PARTIAL_WAKE_LOCK)
}

하지만, 위의 예제를 통해 얻어진 wake lock은 CPU가 꺠어있도록 하는 것만을 보장합니다. 만약 네트워크를 통해 미디어를 스트리밍하고 와이파이를 사용한다면, WifiLock을 유지하고 싶을 것입니다. 이 또한 직접 구현해야합니다. 따라서 원격 URL(remote URL)을 사용하여 MediaPlayer을 준비할때에는 와이파이 잠금을 create하고 acquire 해야합니다. 아래는 예제 코드입니다.

val wifiManager = getSystemService(Context.WIFI_SERVICE) as WifiManager
val wifiLock: WifiManager.WifiLock = 
    wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock")

wifiLock.acquire()

미디어를 일시정지(pause) 혹은 중단(stop)하거나, 네트워크가 더 이상 필요하지 않을 때에는 아래와 같이 잠금을 해제해야합니다:

wifiLock.release()

Performing cleanup

이전에도 언급했듯이, MediaPlayer 객체는 상당히 많은 양의 시스템 자원을 소모합니다. 그렇기때문에 딱 필요한 만큼만 유지하고 작업을 완료했으면 release()를 호출해야합니다. 가비지 컬렉터는 메모리에는 민감하지만 다른 미디어 관련 자원의 부족에는 민감하지 않기 때문에 가비지 컬렉터가 MediaPlayer를 회수하는 데에는 시간이 많이 걸릴 수 있습니다. 따라서 시스템상의 가비지 컬렉션에 의존하기보다는 명시적으로 위의 cleanup 메소드를 호출하는 것이 중요합니다. 따라서 이러한 상항에서는 서비스를 사용할 때 항상 onDestroy() 메소드를 오버라이딩하여 MediaPlayer를 release해야 합니다.

class MyService : Service() {

    private var mediaPlayer: MediaPlayer? = null
    // ...

    override fun onDestroy(){
        super.onDestroy()
        mediaPlayer?.release()
    }
}

서비스를 종료할 때에 MediaPlayer를 release하는 것 이외에도 항상 release를 할 수 있는 기회를 찾아야합니다. 예를 들자면, 미디어를 일정 시간 동안 재생할 수 없다고 기대되는 경우(예: 오디오 포커스를 잃은 경우)에는 현재 존재하는 MediaPlayer를 release하고 나중에 다시 새로 create해야 합니다. 반면, 만약 아주 짧은 시간동안만 재생을 중단할 것 같은 경우에는 다시 새로 create하고 prepare하는 오버헤드를 방지하기 위해 MediaPlayer를 계속 유지해야 합니다.

Digital Rights Management (DRM)

이 부분은 추후에 추가하도록 하겠습니다.

암호화된 미디어 처리하기(Handling encrypted media)

안드로이드 8.0(API 레벨 26) 부터 MediaPlayer는 Common Encryption Scheme(CENC)와 HLS sample-level encrypted media(METHOD=SAMPLE-AES)를 기본 스트림 유형 H.264와 AAC로 decrypt할 수 있게 되었습니다. Full-segment encrypted media(METHOD=AES-128)는 이전에도 지원되었습니다.

콘텐트리졸버에서 미디어 가져오기(Retrieving media from a ContentResolver)

미디어 재생 애플리케이션에서 유용한 기능 중 하나는 사용자의 디바이스에 있는 음악을 가져올 수 있는 기능입니다. ContentResolver 쿼리를 이용하여 외부의 미디어를 가져올 수 있습니다.

val resolver: ContentResolver = contentResolver
val uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
val cursor: Cursor? = resolver.query(uri, null, null, null, null)
when {
    cursor == null -> {
        // 쿼리가 실패했을때. 여기에서 에러를 처리하세요.
    }
    !cursor.moveToFirst() -> {
        // 디바이스에 미디어가 없을 때.
    }
    else -> {
        val titleColumn: Int = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media.TITLE)
        val idColumn: Int = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media._ID)
        do {
            val thisId = cursor.getLong(idColumn)
            val thisTitle = cursor.getString(titleColumn)
            // ...process entry...
        } while (cursor.moveToNext())
    }
}
cursor?.close()

이를 MediaPlayer와 함께 사용하려면 아래의 코드처럼 할 수 있습니다.

val id: Long = /* retrieve it from somewhere */
val contentUri: Uri =
    ContentUris.withAppendedId(android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id )

mediaPlayer = MediaPlayer().apply {
    setAudioStreamType(AudioManager.STREAM_MUSIC)
    setDataSource(applicationContext, contentUri)
}

// ...prepare and start...

샘플 코드(Sample code)

android-SimpleMediaPlayer 코드 샘플은 독립적인 플레이어를 만드는 방법을 보여줍니다. BasicMediaDecoderDeviceOwner 샘플은 이 페이지에서 다루는 API 사용법을 보여줍니다.

참고문헌

[TIL] 2019-10-10

|

Today I Learned

  • progressive download란? 아래는 위키피디아의 정의이다.

    progressive download란 서버에서 클라이언트로 디지털 미디어 파일을 전송하는 것을 의미한다. 일반적으로 컴퓨터에서 HTTP 프로토콜을 사용하여 실행한다. 소비자는 미디어 다운로드가 완료되기 이전에 미디어를 재생한다. 스트리밍 미디어(streaming media)와 progressive download의 중요한 차이점은 디지털 미디어에 접근하는 엔드 유저(최종 사용자)가 어떤 방식으로 디지털 미디어를 수신하고 저장하는지이다.

    미디어 플레이어의 프로그레시브 다운로드 재생은 온전하게 파일의 헤더에 위치한 메타데이터와 웹서버에서 다운로드된 디지털 미디어 데이터 파일의 로컬 버퍼에 의해 이루어진다. 지정된 양의 데이터가 로컬 재생 장치에 사용 가능하게 되는 시점에, 미디어 재생이 시작된다. 이 지정된 크기의 버퍼는 콘텐츠 생산자에 의해 파일의 인코더 설정에 내장되고, 미디어 플레이어에서 부과하는 추가적인 버퍼 설정에 의해 강화된다.