[android] AudioFocus 관리하기
-
2개 이상의 앱이 audio 를 하나의 stream 으로 동시에 재생할 수 있다.
시스템은 이들을 믹스한다. 이는 유저에게 소음이 될 수 있다.
이를 예방하기 위해서 "Audio Focus" 라는 개념을 도입했고, 하나의 앱만 audio focus 를 가질 수 있다.
-
앱이 audio output 을 하려면, audio focus 를 요청해야 한다.
focus 를 획득하면 소리를 재생할 수 있다.
audio focus 를 획득한 후에 재생을 끝마칠 때까지 focus 를 유지하지 못 할 수 있다.
다른 앱이 focus 를 요청하여 취득하는 경우가 그 경우이다.
Focus 를 잃으면 재생을 중단하거나, 볼륨을 줄이는 작업 등을 통해 유저가 새로운 audio 를 잘 들을 수 있게 협조해야 한다.
-
Android 12 (API Level 31) 전에는 audio focus 는 시스템에 의해 관리되지 않았다.
그래서 앱이 audio focus 가이드 라인을 준수하도록 권장하였다.
Android 11 (API Level 30) 이하에서 앱이 audio focus 를 잃은 후에도 계속 무언가를 크게 재생한다면, 시스템이 이를 막지 않지 않았다.
하지만 이는 나쁜 UX 를 초래하여 유저가 앱을 삭제할 수도 있다.
-
올바르게 작동하는 오디오 앱은 다음 일반적인 가이드 라인을 준수해야 한다.
1. 재생 전에 requestAudioFocus() 를 가급적 빨리 호출하고, AUDIOFOCUS_REQUEST_GRANTED 를 획득한다. media session 의 onPlay() callback 에서 호출 하는 것이 좋다.
2. 다른 앱이 audio focus 를 얻으면 (내가 audio focus 를 잃으면) 재생을 중단(stop_하거나 포즈시키거나, 볼륨을 줄인다.
3. 재생이 끝나면 audio focus 를 놓아준다. 유저가 재생을 "pause" 시킨거라면 꼭 focus 를 놓아줄 필요는 없다.
4. AudioAttribute 는 앱이 재생하는 오디오 타입을 명시한다. 예를 들어 음성을 재생한다면 CONTENT_TYPE_SPEECH 를 명시해준다.
-
Android 12 (API Level 31) or later
Audio focus 가 system 에 의해 관리된다. 다른 앱에서 audio focus 를 얻어가면 시스템에서 focus 를 잃은 재생을 fade out 시킨다. 그리고 수신통화가 있을 때 오디오 재생을 묵음시킨다.
-
Android 8.0 (API Level 26) through Android 11 (API Level 30)
Audio focus 가 system 에 의해 관리되지 않는다. 하지만 Android 8.0 (API Level 26) 에서 몇몇 변화가 있었다.
-
Android 7.1 (API Level 25) and lower
Audio focus 는 system 에 의해 관리되지 않는다. requestAudioFocus() 와 abandonAudioFocus() 를 사용하여 audio focus 를 관리한다. AudioManager.OnAudioFocusChangeListener 를 등록해서 callback 을 받는다.
Audio focus in Android 12 and higher
-
audio focus 를 사용하는 미디어나 게임 앱은 audio focus 를 잃은 후에 오디오를 재생해서는 안 된다.
Android 12 (API Level 31) 이상에서는 system 이 이를 강제한다.
다른 앱이 focus 를 가지고 재생을 하고 있는데, 내 앱이 focus 를 요청하면 system 이 다른 앱의 재생을 fade out 시킨다. 추가적으로 fade out 은 한 앱에서 다른 앱으로의 부드러운 전환을 제공한다.
-
fade out 동작은 다음 조건이 만족되면 발생한다.
1. 현재 재생중인 앱이 다음 조건 모두를 만족한다.
1. AudioAttributes.USAGE_MEDIA, AudioAttributes.USAGE_GAME attribute 를 가짐.
2. audio focus 를 AUDIOFOCUS_GAIN 으로 요청 했음.
3. 재생하는 content type 이 AudioAttributes.CONTENT_TYPE_SPEECH 가 아님.
2. 새로 focus 를 요청하는 앱이 AUDIOFOCUS_GAIN 로 focus 를 요청함.
위 조건이 만족하면 system 이 현재 재생중인 앱의 audio 를 fade out 시키고, fade out 이 끝나면 그 앱에 focus loss 를 알려준다. 그 앱은 다시 audio focus 를 요청할 때까지 묵음 상태로 남아 있는다.
* Exsting audio focus behaviors
** Automatic ducking
-
자동 볼륨 줄이기(automatic ducking)는 Anroid 8.0 (API Level 26) 에 도입되었다.
System 이 ducking 을 구현했기 때문에 개발자가 이를 구현할 필요가 없어졌다.
automatic ducking 은 audio 알림이 플레이 하는 앱으로부터 focus 를 얻었을 때도 발생한다.
알림은 ducking 경사의 끝에 맞춰서 시작한다.
-
Automatic ducking 은 다음 조건이 만족되었을 대 발생한다.
1. 현재 재생중인 앱이 아래 조건 모두를 만족시킴
1. audio focus 를 AUDIOFOCUS_GAIN 으로 요청 했음.
2. 재생하는 content type 이 AudioAttributes.CONTENT_TYPE_SPEECH 가 아님.
3. 앱이 AudioFocusRequest.Builder.setWillPauseWhenDucked(true) 를 지정하지 않은 경우
2. focus 를 요청하는 앱이 AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 으로 focus 를 요청한 경우
-
ducking 으로 focus 를 뺐긴 앱은 다시 focus 를 얻을 때 unduck 이 된다.
ducking 으로 focus 를 뺐긴 경우에는 알림을 받지 못한다.
** Mute current audio playback for incoming phone calls
-
몇몇 앱은 전화 중 오디오 재생을 올바르게 컨트롤하지 못하고 있다.
이 상황이 발생하면 유저는 통화를 위해 해당 앱을 찾아서 음소가 시키거나 종료시킨다.
이를 방지하기 위해 system 은 수신 전화가 왔을 때 다른 앱에서 사용중인 audio 를 음소거 시킨다.
이 기능은 수신전화를 받았고, 앱이 다음 조건을 만족시킬 때 작동한다.
1. 앱이 AudioAttributes.USAGE_MEDIA 또는 AudioAttributes.USAGE_GAME 속성을 사용할 때
2. 앱이 성공적으로 focus 를 획득하고 재생하고 있는 경우
이 경우 전화가 종료될 때까지 재생은 멈춘다.
그러나 앱이 통화중 다시 재생을 시작하면, 재생은 가능하다. 이는 유저의 의도적 액션이라 간주하기 때문이다.
Audio focus in Android 8.0 through Android 11
-
Android 8.0 (API Level 26, O) 이상에서는 requestAudioFocus() 함수를 호출할 때 반드시 AudioFocusRequest 를 함께 전달해야 한다.
AudioFocusRequest 에는 audio context, capability 등의 정보가 들어간다.
System 은 이 정보를 바탕으로 audio focus 의 획득과 상실을 자동으로(?) 관리한다.
Audio focus 를 놓아주기 위해서는 동일하게 AudioFocusRequest 를 인자로 받는 abandonAudioFocusRequest() 를 호출해주어야 한다.
이들에 전달되는 AudioFocusRequest 객체는 동일한 것이어야 한다. (should)
-
AudioFocusRequest 를 생성하기 위해서는 AudioFocusRequest.Builder 를 사용한다.
Focus 요청은 반드시 항상 요청 type 을 명시해야 하므로, 이는 생성자에 전달되어야 한다.
FocusGain field 는 꼭 지정되어야 하고, 나머지는 optional 하다.
-
setFocusGain() : 8.0 이전의 durationHint 에 적용되던 값과 동일한 값을 설정해주는 필수 설정값이다. AUDIOFOCUS_GAIN, AUDIOFOCUS_GAIN_TRANSIENT, AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 가 사용된다.
-
setAudioAttributes() : AudioAttributes 는 use case 를 기입한다. 시스템이 이 정보를 보고 audio focus 의 획득과 상실을 관리한다. Atttibutes 는 stream type 의 명시를 대체한다.
Android 8.0 (API Level 26, O) 이상에서 볼륨 컨트롤 이외의 stream type 은 deprecate 되었다. audio player 에서 사용하는 focus request 와 동일한 attributes 를 사용해야 한다.
AudioAttributes.Builder 를 사용하여 생성한다.
기본값은 AudioAttributes.USAGE_MEDIA 이다.
-
setWillPauseWhenDucked() : 다른 앱이 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 에 대한 focus 를 요청하면, 이 앱은 onAudioFocusChange callback 을 받지 않는다. 왜냐하면 시스템이 알아서 duck 을 할 수 있기 때문이다.
볼륨을 줄이는 대신 재생을 멈추고 싶다면, setWillPauseWhenDucked(true) 를 호출해주며, OnAudioFocusChangeListener 를 달아주어야 한다.
-
setAcceptDelayedFocusGain() : 다른 앱이 focus 를 잠궈놓으면 audio focus 요청이 실패할 수 있다. 이 함수는 지연된 focus gain 을 가능하게 한다. 비동기적으로 focus 획득이 가능할 때 얻는다는 이야기이다.
delayed focus gain 은 AudioManager.OnAudioFocusChangeListener 를 주었을 때만 작동한다.
-
setOnAudioFocusChangeListener() : willPauseWhenDucked(true) 또는 setAcceptsDelayedFocusGain(true) 를 주었을 때만 유효하다.
두가지 설정 방법이 있는데 하나는 handler 를 함께 주는 것이고, 다른 하나는 없이 주는 것이다.
handler 를 주는 경우 해당 thread 에서 listener 가 불린다.
만약 주지 않으면 main Looper 가 사용된다.
-
// initializing variables for audio focus and playback management
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
setAudioAttributes(AudioAttributes.Builder().run {
setUsage(AudioAttributes.USAGE_GAME)
setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
build()
})
setAcceptsDelayedFocusGain(true)
setOnAudioFocusChangeListener(afChangeListener, handler)
build()
}
val focusLock = Any()
var playbackDelayed = false
var playbackNowAuthorized = false
// requesting audio focus and processing the response
val res = audioManager.requestAudioFocus(focusRequest)
synchronized(focusLock) {
playbackNowAuthorized = when (res) {
AudioManager.AUDIOFOCUS_REQUEST_FAILED -> false
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
playbackNow()
true
}
AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
playbackDelayed = true
false
}
else -> false
}
}
// implementing OnAudioFocusChangeListener to react to focus changes
override fun onAudioFocusChange(focusChange: Int) {
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN ->
if (playbackDelayed || resumeOnFocusGain) {
synchronized(focusLock) {
playbackDelayed = false
resumeOnFocusGain = false
}
playbackNow()
}
AudioManager.AUDIOFOCUS_LOSS -> {
synchronized(focusLock) {
resumeOnFocusGain = false
playbackDelayed = false
}
pausePlayback()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
synchronized(focusLock) {
// only resume if playback is being interrupted
resumeOnFocusGain = isPlaying()
playbackDelayed = false
}
pausePlayback()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
// ... pausing or ducking depends on your app
}
}
}
* Automatic ducking
-
Android 8.0 (API Level 26, O) 에서는 다른 앱이 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 에 대한 focus 를 요청하면, 시스템이 onAudioFocusChange() callback 호출 없이 자동으로 볼륨을 줄이거나 원상복구하거나 할 수 있다.
Automatic ducking 은 음악 또는 비디오 재생 앱에서 수용할 만한 동작이지만, audio book 과 같은 대화형 음성에 대해서는 유효하지 않다. 후자의 경우는 duck 대신 재생을 중단하는 것이 좋다.
만약 duck 대신 재생 중단을 하고 싶다면 OnAudioFocusChangeListener 의 onAudioFocusChange() callback 을 구현해서, pause/resume 을 control 해야 한다.
setOnAudioFocusChangeListener() 를 사용해 listener 를 등록하고, setWillPauseWhenDucked(true) 를 설정하여 automatic duck 보다는 callback 을 받는 것을 원한다고 알려야 한다.
* Delayed focus gain
-
다른 앱에서 focus 를 lock 하고 있는 경우 시스템은 audio focus 를 요청하는 또 다른 앱에게 focus 를 줄 수 없다.
이 경우 requestAudioFocus() 에 대한 return 으로 AUDIOFOCUS_REQEUST_FAILED 가 전달된다.
이렇게 audio focus 를 얻지 못한 경우 audio 를 재생하면 안 된다.
setAcceptsDelayedFocusGain(true) 를 통해 focus 를 비동기적으로 얻을 수 있다.
이 경우 requestFocus() 함수에 대한 return 은 AUDIOFOCUS_REQUEST_DELAYED 가 된다.
audio focus 를 획득할 수 있는 상황이 되면, 예를 들어 전화 종료와 같은, 시스템은 줄 서 있는 focus request 를 체크해서 적합한 녀석의 onAudioFocusChange() 함수를 호출해준다.
delayed gain focus 를 위해서는 OnAudioFocusChangeListener 를 만들고 onAudioFocusChange() 를 구현해주어야 한다.
Audio focus in Anddroid 7.1 and lower
-
requestAudioFocus() 를 호출할 때 duration hint 를 반드시 지정해서 다른 앱이 이 조건을 존중할 수 있도록 해야 한다.
- 영구적인 audio focus 를 위해서는 AUDIOFOCUS_GAIN 을 요청해야 한다.
- 일시적인 audio focus 를 위해서는 AUDIOFOCUS_GAIN_TRANSIENT 를 요청한다.
- ducking 을 하는 일시적은 audio focus 를 위해서는 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 을 요청한다. 이 경우 기존 focus 를 가지고 있는 친구는 볼륨이 낮아지면서 내용물은 mix 가 된다.
- ducking 은 audio stream 을 간헐적으로 사용하는 네비게이션 등의 앱에 대해 유용하다.
-
reqeustAudioFocus() 함수는 AudioManager.OnAudioFocusChangeListener 를 요구한다.
이 녀석은 media session 을 소유하는 activity 또는 service 에서 생성되어야만 한다.
onAudioFocusChange() 를 구현해서 app 이 audio focus 의 획득과 상실상태를 받아 동작을 조절해야 한다.
abandonAudioFocus() 를 호출할 때 동일한 OnAudioFocusChangeListener 를 전달해야 한다.
-
transient focus 를 요청한 경우, callback 을 통해 audio focus 에 대해 paused 혹은 ducked 되었는지를 알려준다.
-
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
lateinit var afChangeListener AudioManager.OnAudioFocusChangeListener
...
// Request audio focus for playback
val result: Int = audioManager.requestAudioFocus(
afChangeListener,
// Use the music stream.
AudioManager.STREAM_MUSIC,
// Request permanent focus.
AudioManager.AUDIOFOCUS_GAIN
)
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// Start playback
}
Responding to an audio focus change
-
앱이 audio focus 를 가지고 있을 때, 다른 앱이 audio focus 를 요청하면 놓아줄 수 있어야 한다.
AudioFocusChangeListener 의 onAudioFocusChange 함수가 불리는 때가 다른 앱이 audio focus 를 가져갈 때이다.
-
onAudioFocusChange 의 focusChange param 은 focus 를 획득할 때 전달한 duration hint 값에 해당하는 변화이다.
* Transient loss of focus
-
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 또는 AUDIOFOCUS_LOSS_TRANSIENT 의 경우 앱에서 볼륨을 줄이거나(automatic ducking에 의존하지 않는 경우) 재생을 멈추어야 한다.
단시간 audio focus 상실 동안, audio focus 의 변화를 계속 감시해서 다시 focus 를 얻었을 때 재생을 이어갈 수 있도록 준비해야 한다.
다시 focus 를 얻으면 AUDIOFOCUS_GAIN이 전달되며, 이 때 다시 볼륨을 올리거나, 재생을 계속하면 된다.
* Permanent loss of focus
-
audio focus 를 완전히 상실한 경우 AUDIOFOCUS_LOSS 이 전달된다.
이 경우 오디오 재생을 바로 멈춰야 하며, 추가 조치 없이는 AUDIOFOCUS_GAIN 에 대한 callback 을 받지 못한다.
다시 재생하기 위해서는 유저로부터 명시적 액션(다시 재생 버튼을 누름)을 받아야만 한다.
-
private val handler = Handler()
private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_LOSS -> {
// Permanent loss of audio focus
// Pause playback immediately
mediaController.transportControls.pause()
// Wait 30 seconds before stopping playback
handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30))
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
// Pause playback
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
// Lower the volume, keep playing
}
AudioManager.AUDIOFOCUS_GAIN -> {
// Your app has been granted audio focus again
// Raise volume to normal, restart playback if necessary
}
}
}
private var delayedStopRunnable = Runnable {
mediaController.transportControls.stop()
}
참고 : https://developer.android.com/guide/topics/media-apps/audio-focus
끝