두 개 이상의 Android 앱이 동일한 출력 스트림으로 동시에 오디오를 재생할 수 있습니다. 시스템이 모든 것을 혼합합니다. 이는 기술적으로 인상적이지만 사용자에게는 매우 좋지 않을 수 있습니다. 동시에 모든 음악 앱이 재생되지 않도록 Android는 오디오 포커스라는 아이디어를 도입합니다. 한 번에 하나의 앱만 오디오 포커스를 유지할 수 있습니다.
앱이 오디오를 출력해야 하는 경우 오디오 포커스를 요청해야 합니다. 포커스가 있는 앱은 사운드를 재생할 수 있습니다. 그러나 오디오 포커스를 획득한 후 재생을 완료할 때까지 오디오 포커스를 유지하지 못할 수 있습니다. 다른 앱에서 포커스를 요청할 수 있으며 이 경우 내 앱에서 보유한 오디오 포커스를 다른 앱에서 선점하게 됩니다. 이 경우 내 앱은 사용자가 새로운 오디오 소스를 더 쉽게 들을 수 있도록 볼륨을 낮추거나 재생을 일시중지해야 합니다.
오디오 포커스는 상호 협력합니다. 앱은 오디오 포커스 가이드라인을 준수하도록 권장되지만 시스템에서는 규칙을 적용하지 않습니다. 앱이 오디오 포커스를 잃은 후에도 큰 소리로 계속 재생하려고 한다면 아무것도 이를 방지할 수 없습니다. 이는 좋지 않은 경험이므로 사용자가 이런 방식으로 오작동하는 앱을 제거할 가능성이 큽니다.
제대로 작동하는 오디오 앱은 다음과 같은 일반 가이드라인에 따라 오디오 포커스를 관리해야 합니다.
- 재생을 시작하기 직전에
requestAudioFocus()
를 호출하여AUDIOFOCUS_REQUEST_GRANTED
가 반환되는지 확인합니다. 이 가이드에서 설명하는 대로 앱을 디자인하는 경우 미디어 세션의onPlay()
콜백에서requestAudioFocus()
호출이 실행되어야 합니다. - 다른 앱이 오디오 포커스를 받으면 재생을 중지 또는 일시중지하거나 볼륨을 낮춥니다.
- 재생이 중지되면 오디오 포커스를 포기합니다.
오디오 포커스는 실행 중인 Android의 버전에 따라 다르게 처리됩니다.
- Android 2.2(API 레벨 8)부터 앱은
requestAudioFocus()
및abandonAudioFocus()
를 호출하여 오디오 포커스를 관리합니다. 앱은 콜백을 수신하고 자체 오디오 레벨을 관리하기 위해 두 호출 모두와 함께AudioManager.OnAudioFocusChangeListener
를 등록해야 합니다. - Android 5.0(API 레벨 21) 이상을 타겟팅하는 앱의 경우 오디오 앱은
AudioAttributes
를 사용하여 앱이 재생하는 오디오의 유형을 설명해야 합니다. 예를 들어 음성을 재생하는 앱은CONTENT_TYPE_SPEECH
를 지정해야 합니다. - Android 8.0(API 레벨 26) 이상을 실행하는 앱은
AudioFocusRequest
매개변수를 사용하는requestAudioFocus()
메서드를 사용해야 합니다.AudioFocusRequest
에는 앱의 오디오 컨텍스트 및 기능에 관한 정보가 포함되어 있습니다. 시스템은 이 정보를 사용하여 오디오 포커스의 획득과 손실을 자동으로 관리합니다.
Android 8.0 이상에서 오디오 포커스
Android 8.0(API 레벨 26)부터는 requestAudioFocus()
를 호출할 때 AudioFocusRequest
매개변수를 제공해야 합니다.
오디오 포커스를 해제하려면 AudioFocusRequest
를 인수로 사용하는 abandonAudioFocusRequest()
메서드를 호출합니다. 포커스를 요청하고 포기할 때 동일한 AudioFocusRequest
인스턴스를 사용해야 합니다.
AudioFocusRequest
를 만들려면 AudioFocusRequest.Builder
를 사용합니다. 포커스 요청은 항상 요청 유형을 지정해야 하므로 유형은 빌더의 생성자에 포함됩니다. 요청의 다른 필드를 설정하려면 빌더의 메서드를 사용합니다.
FocusGain
필드는 필수사항이고 다른 필드는 모두 선택사항입니다.
메서드 | 참고사항 |
---|---|
setFocusGain()
|
이 필드는 모든 요청에서 필수사항입니다. Android 8.0 이전 requestAudioFocus() 호출에 사용된 durationHint 와 동일한 값을 갖습니다(AUDIOFOCUS_GAIN , AUDIOFOCUS_GAIN_TRANSIENT , AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 또는 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE ).
|
setAudioAttributes()
|
AudioAttributes 는 앱의 사용 사례를 설명합니다. 시스템은 앱이 오디오 포커스를 획득하거나 손실할 때 이를 관찰합니다. 속성은 스트림 유형의 개념에 우선합니다. Android 8.0(API 레벨 26) 이상에서는 볼륨 컨트롤 이외의 작업에 관한 스트림 유형이 지원되지 않습니다. 오디오 플레이어에서 사용하는 포커스 요청에서 동일한 속성을 사용합니다(이 표 이후에 나오는 예 참조).
지정되지 않은 경우 |
setWillPauseWhenDucked()
|
다른 앱이 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 으로 포커스를 요청하면 포커스가 있는 앱은 일반적으로 onAudioFocusChange() 콜백을 받지 않습니다. 시스템이 스스로 볼륨 낮추기를 할 수 있기 때문입니다. 볼륨을 낮추는 대신 재생을 일시중지해야 하는 경우 자동 볼륨 낮추기에 설명된 대로 setWillPauseWhenDucked(true) 를 호출하고 OnAudioFocusChangeListener 를 생성 및 설정합니다.
|
setAcceptsDelayedFocusGain()
|
다른 앱에서 포커스를 잠그면 오디오 포커스 요청이 실패할 수 있습니다. 이 메서드는 사용 가능할 때 비동기로 포커스를 획득하는 기능인 포커스 획득 지연을 사용 설정합니다.
오디오 요청에서도 |
setOnAudioFocusChangeListener()
|
요청에서 willPauseWhenDucked(true) 또는 setAcceptsDelayedFocusGain(true) 도 지정하는 경우에만 OnAudioFocusChangeListener 가 필요합니다.
리스너를 설정하는 두 가지 방법, 즉 핸들러 인수를 사용하는 방법과 사용하지 않는 방법이 있습니다. 핸들러는 리스너가 실행되는 스레드입니다. 핸들러를 지정하지 않으면 기본 |
다음 예는 AudioFocusRequest.Builder
를 사용하여 AudioFocusRequest
를 빌드하는 방법 및 오디오 포커스를 요청하고 포기하는 방법을 보여줍니다.
Kotlin
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() } mediaPlayer = MediaPlayer() val focusLock = Any() var playbackDelayed = false var playbackNowAuthorized = false // ... 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 } } // ... 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) { resumeOnFocusGain = true playbackDelayed = false } pausePlayback() } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { // ... pausing or ducking depends on your app } } }
자바
audioManager = (AudioManager) Context.getSystemService(Context.AUDIO_SERVICE); playbackAttributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_GAME) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(); focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(playbackAttributes) .setAcceptsDelayedFocusGain(true) .setOnAudioFocusChangeListener(afChangeListener, handler) .build(); mediaPlayer = new MediaPlayer(); final Object focusLock = new Object(); boolean playbackDelayed = false; boolean playbackNowAuthorized = false; // ... int res = audioManager.requestAudioFocus(focusRequest); synchronized(focusLock) { if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { playbackNowAuthorized = false; } else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { playbackNowAuthorized = true; playbackNow(); } else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) { playbackDelayed = true; playbackNowAuthorized = false; } } // ... @Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: if (playbackDelayed || resumeOnFocusGain) { synchronized(focusLock) { playbackDelayed = false; resumeOnFocusGain = false; } playbackNow(); } break; case AudioManager.AUDIOFOCUS_LOSS: synchronized(focusLock) { resumeOnFocusGain = false; playbackDelayed = false; } pausePlayback(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: synchronized(focusLock) { resumeOnFocusGain = true; playbackDelayed = false; } pausePlayback(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // ... pausing or ducking depends on your app break; } } }
자동 볼륨 낮추기
Android 8.0(API 레벨 26)에서 다른 앱이 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
로 포커스를 요청하면 시스템은 앱의 onAudioFocusChange()
콜백을 호출하지 않은 채 볼륨을 낮추고 복원할 수 있습니다.
자동 볼륨 낮추기는 음악 및 동영상 재생 앱에서 허용되는 동작이지만 오디오 북 앱과 같이 음성 콘텐츠를 재생할 때는 유용하지 않습니다. 이 경우에는 앱의 일시중지가 대신 필요합니다.
볼륨 낮추기 요청 시 볼륨을 줄이는 대신 일시중지를 원하는 경우 원하는 일시중지/다시 시작 동작을 구현하는 onAudioFocusChange()
콜백 메서드로 OnAudioFocusChangeListener
를 만듭니다.
setOnAudioFocusChangeListener()
를 호출하여 리스너를 등록하고 setWillPauseWhenDucked(true)
를 호출하여 시스템이 자동 볼륨 낮추기 대신 콜백을 사용하도록 지시합니다.
포커스 획득 지연
전화 통화 중과 같이 다른 앱에 의해 포커스가 '잠겨' 있기 때문에 시스템이 오디오 포커스 요청을 허용할 수 없는 경우가 있습니다. 이 경우 requestAudioFocus()
는 AUDIOFOCUS_REQUEST_FAILED
를 반환합니다. 이러한 일이 발생하면 앱이 포커스를 획득하지 못한 것이므로 오디오 재생을 계속해서는 안 됩니다.
setAcceptsDelayedFocusGain(true)
메서드는 앱이 비동기적으로 포커스 요청을 처리하도록 허용합니다. 이 플래그를 설정하면 포커스가 잠겨 있을 때 만들어진 요청이 AUDIOFOCUS_REQUEST_DELAYED
를 반환합니다. 오디오 포커스를 잠근 조건이 더 이상 존재하지 않는 경우(예: 전화 통화 종료) 시스템은 보류 중인 포커스 요청을 허용하고 onAudioFocusChange()
를 호출하여 앱에 알립니다.
포커스 획득 지연을 처리하려면 원하는 동작을 구현하는 onAudioFocusChange()
콜백 메서드로 OnAudioFocusChangeListener
를 만들고 setOnAudioFocusChangeListener()
를 호출하여 리스너를 등록해야 합니다.
Android 8.0 이전의 오디오 포커스
requestAudioFocus()
를 호출할 때는 현재 포커스가 있고 재생 중인 다른 앱에 적용할 수 있는 지속 시간 힌트를 지정해야 합니다.
- 예측 가능한 미래에 오디오를 재생하고자 하며(예: 음악 재생) 이전 오디오 포커스 보유 앱이 재생을 중지할 것으로 예상되는 경우 영구적인 오디오 포커스(
AUDIOFOCUS_GAIN
)를 요청합니다. - 단기간만 오디오를 재생할 것이고 이전 보유 앱이 재생을 일시중지할 것으로 예상되는 경우 일시적인 포커스(
AUDIOFOCUS_GAIN_TRANSIENT
)를 요청합니다. - 단기간만 오디오를 재생할 것이므로 이전 포커스 소유 앱이 오디오 출력을 '낮추기만' 하면 계속 재생해도 괜찮음을 나타내려면 볼륨 낮추기와 함께 일시적인 포커스(
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
)를 요청합니다. 두 오디오 출력 모두 오디오 스트림에 혼합됩니다. 볼륨 낮추기는 들을 수 있는 운전 경로 등 간헐적으로 오디오 스트림을 사용하는 앱에 특히 적합합니다.
requestAudioFocus()
메서드에도 AudioManager.OnAudioFocusChangeListener
가 필요합니다. 이 리스너는 미디어 세션을 소유하고 있는 동일한 활동 또는 서비스에서 만들어야 합니다. 그러면 다른 앱이 오디오 포커스를 획득하거나 포기할 때 내 앱이 수신하는 onAudioFocusChange()
콜백이 구현됩니다.
다음 스니펫은 STREAM_MUSIC
스트림에 관한 영구적인 오디오 포커스를 요청하며 오디오 포커스에서의 후속 변경을 처리하기 위해 OnAudioFocusChangeListener
를 등록합니다. (변경 리스너에 관한 내용은 오디오 포커스 변경에 응답에서 설명합니다.)
Kotlin
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 }
자바
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); AudioManager.OnAudioFocusChangeListener afChangeListener; ... // Request audio focus for playback int result = audioManager.requestAudioFocus(afChangeListener, // Use the music stream. AudioManager.STREAM_MUSIC, // Request permanent focus. AudioManager.AUDIOFOCUS_GAIN); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { // Start playback }
재생을 마치면 abandonAudioFocus()
를 호출합니다.
Kotlin
audioManager.abandonAudioFocus(afChangeListener)
자바
// Abandon audio focus when playback complete audioManager.abandonAudioFocus(afChangeListener);
이는 포커스가 더 이상 필요하지 않음을 시스템에 알리고 관련된 OnAudioFocusChangeListener
의 등록을 취소합니다. 일시적인 포커스를 요청한 경우에는 일시중지되거나 볼륨을 낮춘 앱에 계속 재생하거나 볼륨을 복원할 수 있다고 알리게 됩니다.
오디오 포커스 변경에 응답
앱은 오디오 포커스를 획득하면 다른 앱이 직접 오디오 포커스를 요청할 경우 이를 해제할 수 있어야 합니다. 이 경우 앱은 requestAudioFocus()
호출 시 지정된 AudioFocusChangeListener
에서 onAudioFocusChange()
메서드 호출을 수신합니다.
onAudioFocusChange()
에 전달된 focusChange
매개변수는 발생하는 변경의 종류를 나타냅니다. 이는 포커스를 획득하는 앱에서 사용되는 지속 시간 힌트에 해당합니다. 앱은 이에 적절히 대응해야 합니다.
- 일시적인 포커스 손실
- 포커스 변경이 일시적인 경우(
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
또는AUDIOFOCUS_LOSS_TRANSIENT
) 앱은 볼륨을 낮추거나(자동 볼륨 낮추기를 사용하지 않는 경우) 재생을 일시중지하되 동일한 상태를 유지해야 합니다.오디오 포커스가 일시적으로 손실된 동안 오디오 포커스를 계속해서 모니터링하여 포커스를 다시 획득할 때 정상적인 재생을 다시 시작할 수 있도록 준비해야 합니다. 차단 앱이 포커스를 포기하면 콜백(
AUDIOFOCUS_GAIN
)이 수신됩니다. 이때 볼륨을 정상 수준으로 복원하거나 재생을 다시 시작할 수 있습니다. - 영구적인 포커스 손실
- 오디오 포커스 손실이 영구적이면(
AUDIOFOCUS_LOSS
) 다른 앱이 오디오를 재생하는 것입니다. 앱은AUDIOFOCUS_GAIN
콜백을 수신하지 않을 것이므로 재생을 즉시 일시중지해야 합니다. 재생을 다시 시작하려면 사용자가 알림 또는 앱 UI에서 재생 전송 컨트롤을 누르는 등 명시적인 작업을 실행해야 합니다.
다음 코드 스니펫은 OnAudioFocusChangeListener
및 onAudioFocusChange()
콜백을 구현하는 방법을 보여줍니다. 오디오 포커스가 영구적으로 손실될 경우 중지 콜백 지연을 위해 Handler
가 사용됩니다.
Kotlin
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 Handler handler = new Handler(); AudioManager.OnAudioFocusChangeListener afChangeListener = new AudioManager.OnAudioFocusChangeListener() { public void onAudioFocusChange(int focusChange) { if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { // Permanent loss of audio focus // Pause playback immediately mediaController.getTransportControls().pause(); // Wait 30 seconds before stopping playback handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30)); } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { // Pause playback } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { // Lower the volume, keep playing } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { // Your app has been granted audio focus again // Raise volume to normal, restart playback if necessary } } };
핸들러는 다음과 같은 Runnable
을 사용합니다.
Kotlin
private var delayedStopRunnable = Runnable { mediaController.transportControls.stop() }
자바
private Runnable delayedStopRunnable = new Runnable() { @Override public void run() { getMediaController().getTransportControls().stop(); } };
사용자가 재생을 다시 시작할 때 지연된 중지가 시작되지 않도록 하려면 상태 변경에의 응답으로 mHandler.removeCallbacks(mDelayedStopRunnable)
를 호출합니다. 예를 들어 콜백의 onPlay()
, onSkipToNext()
등에서 removeCallbacks()
를 호출합니다. 서비스에서 사용하는 리소스를 정리할 때 onDestroy()
콜백에서도 이 메서드를 호출해야 합니다.