오디오 포커스 관리

두 개 이상의 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) 이상에서는 볼륨 컨트롤 이외의 작업에 관한 스트림 유형이 지원되지 않습니다. 오디오 플레이어에서 사용하는 포커스 요청에서 동일한 속성을 사용합니다(이 표 이후에 나오는 예 참조).

AudioAttributes.Builder를 사용하여 속성을 먼저 지정한 다음 이 메서드를 사용하여 요청에 속성을 할당합니다.

지정되지 않은 경우 AudioAttributes의 기본값은 AudioAttributes.USAGE_MEDIA입니다.

setWillPauseWhenDucked() 다른 앱이 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK으로 포커스를 요청하면 포커스가 있는 앱은 일반적으로 onAudioFocusChange() 콜백을 받지 않습니다. 시스템이 스스로 볼륨 낮추기를 할 수 있기 때문입니다. 볼륨을 낮추는 대신 재생을 일시중지해야 하는 경우 자동 볼륨 낮추기에 설명된 대로 setWillPauseWhenDucked(true)를 호출하고 OnAudioFocusChangeListener를 생성 및 설정합니다.
setAcceptsDelayedFocusGain() 다른 앱에서 포커스를 잠그면 오디오 포커스 요청이 실패할 수 있습니다. 이 메서드는 사용 가능할 때 비동기로 포커스를 획득하는 기능인 포커스 획득 지연을 사용 설정합니다.

오디오 요청에서도 AudioManager.OnAudioFocusChangeListener를 지정한 경우에만 포커스 획득 지연이 작동합니다. 포커스가 허용되었음을 알려면 앱이 콜백을 수신해야 하기 때문입니다.

setOnAudioFocusChangeListener() 요청에서 willPauseWhenDucked(true) 또는 setAcceptsDelayedFocusGain(true)도 지정하는 경우에만 OnAudioFocusChangeListener가 필요합니다.

리스너를 설정하는 두 가지 방법, 즉 핸들러 인수를 사용하는 방법과 사용하지 않는 방법이 있습니다. 핸들러는 리스너가 실행되는 스레드입니다. 핸들러를 지정하지 않으면 기본 Looper와 연결된 핸들러가 사용됩니다.

다음 예는 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에서 재생 전송 컨트롤을 누르는 등 명시적인 작업을 실행해야 합니다.

다음 코드 스니펫은 OnAudioFocusChangeListeneronAudioFocusChange() 콜백을 구현하는 방법을 보여줍니다. 오디오 포커스가 영구적으로 손실될 경우 중지 콜백 지연을 위해 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() 콜백에서도 이 메서드를 호출해야 합니다.