기본적인 Media 앱 아키텍처
MediaController, MediaSession으로 구분해서 사용하는 구조
서로 미리 정의된 콜백을 통해 소통
MediaSession
플레이어 제어 및 상태 관리
- 플레이어와 직접적인 통신 및 제어
- 재생 상태 및 메타데이터 관리 (PlaybackStateCompat, MediaMetadataCompat)
- 플레이어 API의 캡슐화 (다른곳에서 플레이어 관련 API 접근 제한) / 플레이어는 세션에서만 호출하도록 권장
외부 미디어와의 통신 및 제어
- 외부 미디어 컨트롤 명령 수신 (물리 버튼, 음성, BT 등등)
- MediaSessionCompat.Callback을 통한 명령 처리
- 여러 MediaController과의 통신 지원 (Wear, Auto 등)
상태 정보 관리
- PlaybackStateCompat - 재생 상태 정보
- MediaMetadataCompat - 현재 재생 중인 미디어 정보
MediaSession의 상태를 나타내는 2개의 클래스 (PlaybackStateCompat & MediaMetadataCompat)
PlaybackStateCompat
플레이어의 현재 동작 상태 (현재 전송 상태 / 오류 코드 및 메세지 / 플레이어 위치 / 유효한 컨트롤러 작업 등등)
상태가 변경될 때마다 setPlaybackState 호출해 새로운 상태를 세션에 전달
MediaMetadataCompat
재생 중인 정보에 대한 설명 (아티스트, 앨범, 트랙 이름 / 트랙 길이 / 앨범 아트워크 등등)
상태가 변경될 때마다 setMetaData 호출해 새로운 상태를 세션에 전달
MediaController
UI와 MediaSession 사이의 인터페이스
- UI와 플레이어 로직의 분리
- 전송 제어 작업(transport control action)을 MediaSession으로 전달
- 단일 MediaSession과의 통신
기본적인 오디오 앱 구조
오디오 플레이어는 UI 없이도 백그라운드에서 재생될 수 있어야 한다
이를 위해
Activity(UI) 컴포넌트 + Service(Player) 컴포넌트 구성 (client / server 디자인)
2개의 컴포넌트로 나누어져 있어, 사용자가 다른 앱으로 전환할 때 서비스가 백그라운드에서 실행될 수 있다
client/server 디자인 → MediaBrowser / MediaBrowserService 사용
Server 구성
MediaBrowserService
- MediaBrowser가 포함된 앱에서 서비스를 검색하고 자체 mediaController를 생성해 mediaSession에 연결하고 플레이어를 제어할 수 있다
- 이런 방식으로 여러 기기에서(Wear OS, Android Auto) 호환 가능
MediaBrowserService 빌드
MediaSession 생성하고 콜백, 토큰 set
onGetRoot(), onLoadChildren() 통해 Client와의 연결 핸들링
- onGetRoot() → service에 접근하는 것을 조작
- client가 처음 연결할 때 호출
- client의 접근 권한을 확인
- null 리턴 시 연결 거부
- onLoadChildren() → client가 service의 콘텐츠 계층 구조 메뉴를 빌드, 표시할 수 있는 기능을 제공
- 클라이언트가 특정 ID의 하위 항목을 요청할 때 호출됨
- 계층 구조로 미디어 콘텐츠 제공
Client 구성
MediaController, MediaBrowser로 구성
MediaBrowser
- MediaBrowserService에 연결
- 연결 이후 MediaController 생성
- MediaBrowserService에 연결
- MediaBrowser 생성 시 MediaBrowserService, MediaBrowser 콜백 전달
- MediaBrowser에서 connect 호출하게 되면 MediaBrowserService 시작
- onGetRoot를 호출하고 결과에 따라 ConnectionCallback의 onConnected()나 onConnectionFailed 호출
- 성공하면 onLoadChildren 호출
- MediaController 생성
- onConnected()에서 MediaBrowserService의 session token을 가져와서 이를 이용해 MediaControllerCompat 생성
- MediaController에 UI를 연결하려면 컨트롤러의 TransportControls를 사용 (ex. clickListener에서 controller.transportControls.play() 호출)
- TransportControls는 MediaSession을 제어하기 위한 인터페이스
- 이를 호출하면 MediaSession의 Callback으로 전달
- UI에서는 현재 mediaSession의 상태(PlaybackState, metadata)를 보여주어야 하기 때문에 session과의 sync를 맞추기 위해 MediaControllerCallback 등록
- ControllerCallback은 MediaSession의 상태가 변경되었을 때 호출
MediaSessionCallback
콜백은 여러 API 함수를 호출해 Player 제어, 오디오 포커스 관리, 미디어 세션과 미디어 브라우저 서비스와 통신
Callback에서는
먼저 AudioFocus를 요청하고 granted 되면 다른 작업들을 실행하기를 권장
(isActive, startService, registerReceiver, startForeground 등등)
Callback에서의 작업
AudioFocus 관련
- onPlay시 requestFocus() 호출
- onStop시 abandonAudioFocus()
MediaSession 관련
- onPlay시 setActive(true) → 메타데이터 및 상태 업데이트
- onStop시 setActive(false)
Player 관련
- onPlay시 Player 시작
- onPause시 Player 일시중지
- onStop시 Player 중지
Notification 관련
- onPlay시 startForeground(notification)
- onPause시 stopForeground(false)
- onStop시 stopForeground(false)
BroadcastReceiver 관련
- onPlay시 BroadcastReceiver 등록
- onPause시 BroadcastReceiver 등록 취소
MediaButton
BT 헤드셋의 일시중지/재생 버튼 등 Android 기기 등의 하드웨어 버튼
사용자가 버튼을 누르면 Android는 keyEvent 생성 (버튼을 식별하는 키 코드 포함, KEYCODE_MEDIA로 시작하는 상수)
상황마다 미디어 버튼을 처리하는 방법이 다르다
버튼 이벤트가 처리되는 우선순위
- UI Activity가 보일 때 (Foreground)
- Activity가 숨겨져 있고 MediaSession이 active일 때 (Background + session Active)
- Activity가 숨겨져 있고 MediaSession이 inactive일 때 (Background + session inActive)
Foreground일 경우
onKeyDown() 에서 미디어 버튼 키 이벤트를 수신
Foreground에서 handle하지 않을 경우
Android는 이벤트를 처리할 수 있는 MediaSession을 찾는다
Android8.0 (26) 이상
시스템이 로컬에서 오디오를 재생한 MediaSession이 있는 마지막 앱을 찾는다
세션이 active면
- Android에서 직접 이벤트를 보냄
세션이 inactive면
- MediaButtonReceiver가 있다면 Android가 receiver로 이벤트를 보내서 이벤트를 받을 수 있도록 세션을 restart 시킨다
시스템은 세션에 ButtonReceiver가 없다면 버튼 이벤트를 삭제한다
Android 8.0 이전까지
Active MediaSession으로 이벤트 전달
여러 개의 active MediaSession이 있는 경우
Android는 재생 준비중인 (buffereing / connecting) , playing, paused 미디어 세션을 선택
우선순위
- PlaybackStateCompat.STATE_PLAYING
- PlaybackStateCompat.STATE_BUFFERING
- PlaybackStateCompat.STATE_PAUSED
- PlaybackStateCompat.STATE_STOPPED
active session이 없는경우
가장 최근 active였던 session을 찾아 이벤트 전달
mediaSession.isActive = false
false 였어도 다른 active session이 없을 시 가장 최근에 active였다면 이벤트를 받을 수도 있다
그럼 MediaSession이 Active일 경우 MediaButton 이벤트를 어떻게 받을 수 있을까?
Android 5.0 (API level 21) 이상
안드로이드가 onMediaButtonEvent()를 호출함으로써 자동으로 미디어 버튼 이벤트를 active mediaSession으로 보냄
MediaSessionCompat.Callback 콜백은 KeyEvent를 적절한 MediaSession Callback 메서드로 자동 변경
- MediaButtonReceiver가 이를 MediaSession 콜백으로 변환
- 서비스의 MediaSession에서 최종 처리
정리하면, MediaButton 이벤트가 핸들링 되는 경로는
- Android5.0 이상부터는 active한 session의 onMediaButtonEvent을 통해서
- Android5.0 이전까지는 MediaButtonReceiver를 통해 이벤트가 들어오면 intent를 MediaBrowserService의 onStartCommand로 포워딩
MediaButtonReceiver는 Android5.0 이전 이후 상관없이 사용되는 것
MediaButtonReceiver는 앱 부활 기능을 담당
MediaSession callback은 실행 중인 앱의 이벤트 처리를 담당
MediaButtonReceiver
- 앱이 종료된 상태
- Manifest에 등록된 MediaButtonReceiver는 앱이 완전히 종료된 상태에서도 시스템의 브로드캐스트를 받을 수 있음
- 이벤트를 받으면 서비스를 시작하여 앱을 "부활"시킬 수 있음 (onReceive() 함수에서)
- MediaSession이 inActive인 상태
- 시스템에 의해 앱이 kill된 상태
- 메모리 부족 등으로 시스템이 앱을 종료시켜도 Manifest에 등록된 Receiver는 살아있어 이벤트 받을 수 있다
InActive MediaSession을 재시작하기 위해 MediaButton을 사용
MediaSessionCompat을 사용한다면 시스템은 마지막 active session을 기억하고 있다가 media button event 발생 시 ACTINO_MEDIA_BUTTON Intent를 보내 session을 재시작하려고 시도한다
'안드로이드' 카테고리의 다른 글
[안드로이드] RxJava3CallAdapterFactory (0) | 2023.03.20 |
---|---|
[안드로이드] Retrofit 분석 (0) | 2023.03.11 |
[Android] Fragment ViewBinding 사용 시 주의할 점 (0) | 2023.02.12 |
[Android] Fragment Lifecycle (0) | 2023.02.12 |
[안드로이드] ListAdapter (0) | 2023.02.10 |