봄날은 갔다. 이제 그 정신으로 공부하자

오디오 포커스 관리 - part6 본문

학습

오디오 포커스 관리 - part6

길재의 그 정신으로 공부하자 2021. 1. 8. 11:01

이번 글에서는 오디오 출력을 제어하는 방식에 대해 설명합니다.

 

* 참고 사이트:

   https://developer.android.com/guide/topics/media-apps/audio-focus

 

두 개 이상의 Android 앱이 동일한 출력 스트림으로 동시에 오디오를 재생할 수 있습니다. 이때 시스템은 오디오를 혼합하여 출력합니다.

동시에 오디오가 혼합되어 출력되는 것은 사용자에게 좋지 않으므로 동시에 모든 음악 앱이 재생되지 않도록 Android는 오디오 포커스라는 아이디어를 도입하여 한 번에 하나의 앱만 오디오 포커스를 유지할 수 있도록 하였습니다.

 

앱이 오디오를 출력해야 하는 경우 오디오 포커스를 요청해야 합니다. 포커스가 있는 앱은 사운드를 재생할 수 있습니다. 그러나 오디오 포커스를 획득한 후 재생을 완료할 때까지 오디오 포커스를 유지하지 못할 수 있습니다. 다른 앱에서 포커스를 요청할 수 있으며 이 경우 내 앱에서 보유한 오디오 포커스는 다른 앱에서 선점하게 됩니다. 이 경우 내 앱은 사용자가 새로운 오디오 소스를 더 쉽게 들을 수 있도록 볼륨을 낮추거나 재생을 일시중지해야 합니다.

 

오디오 포커스는 상호 협력합니다. 앱은 오디오 포커스 가이드라인을 준수하도록 권장되지만 시스템에서는 규칙을 적용하지 않습니다. 앱이 오디오 포커스를 잃은 후에도 큰 소리로 계속 재생하려고 한다면 아무것오 이를 방지 할 수 없지만 이러한 앱은 사용자가 직접 삭제할 가능성이 큽니다.

따라서 제대로 작동하는 오디오 앱은 다음과 같은 일반 가이드라인에 따라 오디오 포커스를 관리해야 합니다.

   - 재생을 시작하기 적전에 requestAudioFocus()를 호출하여 AUDIOFOCUS_REQUEST_GRANTED가 반환되는지 확인합니다. 

   - 다른 앱이 오디오 포커스를 받으면 재생을 중지 또는 일시중지하거나 볼륨을 낮춥니다.

   - 재생이 중지되면 오디오 포커스를 포기합니다.

 

오디오 포커스는 실행 중인 Android의 버전에 따라 다르게 처리됩니다.

   - Android 2.2(API Level 8)부터 앱은 requestAudioFocus() 및 abandonAudioFocus()를 호출하여 오디오 포커스를 관리합니다. 

      앱은 콜백을 수신하고 자체 오디오 레벨을 관리하기 위해 두 호출 모두와 함께 AudioManager.OnAudioFocusChangeListener를 등록

      해야 합니다.

   - Android 5.0(API Level 21)이상을 타겟팅으로 하는 앱의 경우 오디오 앱은 AudioAttributes를 사용하여 앱이 재생하는 오디오의 유형

      을 설명해야 합니다.

      예를 들어 음성을 재생하는 앱은 CONTENT_TYPE_SPEECH를 지정해야 합니다.

   - Android 8.0(API Level 26)이상을 실행하는 앱은 AudioFocusRequest 매개변수를 사용하는 requestAudioFocus() 메서드를

     사용해야 합니다.

     AndroidFocusRequest에는 앱의 오디오 컨텍스트 및 기능에 관한 정보가 포함되어 있습니다. 시스템은 이 정보를 사용하여 오디오

     포커스의 획득과 손실을 자동으로 관리합니다.

 

Android 8.0이상에서 오디오 포커스

Android 8.0(API Level 26)부터느 requestAudioFocus()를 호출할 때 AudioFocusRequest 매개변수를 제공해야 합니다. 오디오 포커스를 해제하려면 AudioFocusRequest를 인수로 사용하는 abandonAudioFocusRequest() 메서드를 호출합니다. 포커스를 요청하고 포기할 때 동일한 AudioFocusRequest 인스턴스를 사용해야 합니다.

 

AudioFocusRequest를 만들려면 AudioFocusRequest.Builder를 사용합니다. 포커스 요청은 항상 요청 유형을 지정해야 하므로 유형은 빌더의 생성자에 포함됩니다. 요청의 다른 필드를 설정하려면 빌더의 메서드를 사용합니다.

FocusGain 필드만 필수사항이고 다른 필드는 모두 선택사항 입니다.

 

아래 코드 예시는 AudioFocusRequest.Builder를 사용하여 AudioFocusRequest를 빌드하는 방법 및 오디오 포커스를 요청하고 포기하는 방법을 설명합니다.

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
            }
        }
    }

    

자동 볼륨 낮추기

Android 8.0(API Level 26)에서 다른 앱이 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK로 포커스를 요청하면 시스템은 앱의 onAudioFocusChange() 콜백을 호출하지 않은 채 볼륨을 낮추고 복원할 수 있습니다.

 

자동 볼륨 낮추기는 음악 및 동영상 재생 앱에서 허용되는 동작이지만 오디오북앱과 같이 음성 콘텐츠를 재생할 때는 유용하지 않습니다. 이경우에는 앱의 일시중지가 대신 필요합니다.

 

볼륨 낮추기 요청 시 볼륨을 줄이는 대신 일시 중지를 원하는 경우 원하는 일시중지/다시 시작 동작을 구현하는 onAudioFocusChange() 콜백 메서드로 onAudioFocusChangeListener를 만듭니다.

setOnAudioFocusChangeListener()를 호출하여 리스너를 등록하고 setWillPauseWhenDucked(true)를 호출하여 자동 볼륨 낮추기 대신 콜백을 사용하도록 지시합니다.

 

포커스 획득 지연

전화 통화 중과 같이 다른 앱에 의해 포커스가 “잠겨” 있기 때문에 시스템이 오디오 포커스 요청을 허용할 수 없는 경우가 있습니다. 이 경우 requestAudioFocus()는 AUDIOFOCUS_REQUEST_FAILED를 반환합니다. 이러한 일이 발생하면 앱이 포커스를 획득하지 못한 것이므로 오디오 재생을 계속해서는 안됩니다.

 

setAcceptsDelayedFocusGain(true) 메서드는 앱이 비동기적으로 포커스 요청을 처리하도록 허용합니다. 이 플래그를 설정하면 포커스가 잠겨 있을 때 만들어진 요청이 AUDIOFOCUS_REQUEST_DELAYED를 반환합니다. 오디오 포커스를 잠근 조건이 더 이상 존재하지 않는 경우, 시스템은 보류 중인 포커스 요청을 허용하고 onAudioFocusChange()를 호출하여 앱에 알립니다.

 

포커스 획득 지연을 처리하려면 원하는 동작을 구현하는 onAudioFocusChange() 콜백 메서드로 OnAudioFocusChangeListener를 만들고 setOnAudioFoucsChangeListener()를 호출하여 리스너를 등록해야 합니다.

 

Android 8.0 미만에셔의 오디오 포커스 처리

requestAudioFocus()를 호출할 때는 현재 포커스가 있고 재생 중인 다른 앱에 적용할 수 있는 지속 시간 힌트를 지정해야 합니다.

   - 예측 가능한 미래에 오디오를 재생하고자 하며(예: 음악 재생) 이전 오디오 포커스 보유 앱이 재생을 중지할 것으로 예상되는 경우

      영구적인 오디오 포커스(AUDIOFOCUS_GAIN)를 요청합니다.

   - 단기간만 오디오를 재생할 것이고(예: 경고 음성 출력) 이전 보유 앱이 재생을 일시중지할 것으로 예상되는 경우 일시적인 포커스

      (AUDIOFOCUS_GAIN_TRANSIENT)를 요청합니다.

   - 단기간만 오디오를 재생할 것이므로 이전 포커스 소유 앱이 오디오 출력을 ‘낮추기만’하면 계속 재생해도 괜찮음을 나타내려면 볼륨

      낮추기와 함께 일시적인 포커스(AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)를 요청합니다. 두 오디오 출력 모두 오디오 스트림에

     혼합됩니다. 볼륨 낮추기는 들을 수 있는 운전 경로 등 간헐적으로 오디오 스트림을 사용하는 앱에 적합합니다.

 

requestAudioFocus() 메서드에도 AudioManager.OnAudioFocusChangeListener가 필요합니다. 이 리스너는 미디어 세션을 소유하고 있는 동일한 활동 또는 서비스에서 만들어야 합니다. 그러면 다른 앱이 오디오 포커스를 획득하거나 포기할 때 내 앱이 수신하는 onAudioFocusChange() 콜백이 구현됩니다.

 

아래 코드 예시는 STREAM_MUSIC 스트림에 관한 영구적인 오디오 포커스를 요청하며 오디오 포커스에서의 후속 변경을 처리하기 위해 OnAudioFocusChangeListener를 등록합니다.

    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
    }

 

재생이 완료되면 abandonAudioFocus()를 호출합니다.

audioManager.abandonAudioFocus(afChangeListener)

 

이는 포커스가 더이상 필요하지 않음을 시스템에 알리고 관련된 OnAudioFocusChangeListener의 등록을 취소합니다. 일시적인 포커스를 요청한 경우에는 일시적인 포커스를 요청한 경우에는 일시중지되거나 볼륨을 낮춘 앱에 계속 재생하거나 볼륨을 복원할 수 있다고 알리게 됩니다.

 

오디오 포커스 변경 응답

앱은 오디오 포커스를 획득하면 다른 앱이 직접 오디오 포커스를 요청할 경우 이를 해제할 수 있어야 합니다. 이 경우 앱은 requestAudioFocus() 호출 시 지정된 AudioFocusChangeListener에서 onAudioFocusChange() 메서드 호출을 수신합니다.

 

onAudioFocusChange()에 전달된 focusChange 매개변수는 발생하는 변경의 종류를 나타냅니다. 이는 포커스를 획득하는 앱에서 사용되는 지속 시간 힌트에 해당하므로 앱은 이에 적절히 대응해야 합니다.

 

일시적인 포커스 손실

포커스 변경이 일시적인 경우(AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK or AUDIOFOCUS_LOSS_TRANSIENT)앱은 볼륨을 낮추거나(자동 볼륨 낮추기를 사용하지 않는 경우) 재생을 일시중지하되 동일한 상태를 유지해야 합니다.

 

오디오 포커스가 일시적으로 손실된 동안 오디오 포커스를 계속해서 모니터링하여 포커스를 다시 획득할 때 정상적인 재생을 다시 시작할 수 있도록 준비해야 합니다. 차단 앱이 포커스를 포기하면 콜백(AUDIOFOCUS_GAIN)이 수신됩니다. 이때 볼륨을 정상적인 수준으로 복원하거나 재생을 다시 시작할 수 있습니다.

 

영구적인 포커스 손실

오디오 포커스 손실이 영구적이면(AUDIOFOCUS_LOSS) 다른 앱이 오디오를 재생하는 것 입니다. 앱은 AUDIOFOCUS_GAIN 콜백을 수신하지 못할 것이므로 재생을 즉시 일시중지해야합니다. 재색을 다시 시작하려면 사용자가 알림 또는 앱의 UI에서 재생 전송 컨틀롤을 누르는 등의 명시적인 작업을 실행해야 합니다.

 

아래 코드 예제는 OnAudioFocusChangeListener 및 onAudioFocusChange() 콜백을 구현하는 방법을 보여줍니다. 오디오 포커스가 영구적으로 손실될 경우 중지 콜백 지연을 위해 Handler가 사용됩니다.

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
            }
        }
    }

 

핸들러는 아래와같은 Runnable을 사용합니다.

private var delayedStopRunnable = Runnable {
    mediaController.transportControls.stop()
}

 

사용자가 재생을 다시 시작할 지연된 중지가 시작되지 않도록 하려면 상태 변경에의 응답으로 mHandler.removeCallbacks(mDelayedStopRunnable) 호출합니다. 예를 들어 콜백의 onPause(), onSkipToNext() 등에서 removeCallbacks() 호출합니다. 서비스에서 사용하는 리소스를 정리할 onDestory()콜백에서도 메서드를 호출해야 합니다.

'학습' 카테고리의 다른 글

Android 12를 타겟으로 빌드하기  (0) 2022.02.08
android 12 변경 사항 정리  (1) 2021.12.31
오디오 출력 Handling - part5  (0) 2021.01.07
Media button 응답 처리 - part4  (0) 2021.01.06
동영상 앱 빌드 - part3  (0) 2021.01.04
Comments