Managing audio focus Android


Managing audio focus

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

두 개 이상의 안드로이드 앱이 동시에 같은 출력 스트림에 오디오를 재생할 수 있다. 시스템은 모든 것을 믹스한다. 이 것은 기술적으로 인상적이지만 사용자에게는 반겨지지 않을 수 있다. 동시에 모든 음악 앱이 재생하는 것을 방지하기 위해 안드로이드는 오디오 포커스라는 개념을 도입한다. 한번에 오직 하나의 앱만이 오디오 포커스를 갖는 것이다.

앱이 오디오를 출력하기를 원할 때 오디오 포커스를 요청해야 한다. 포커스를 가지면 사운드를 재생할 수 있다. 그러나, 오디오 포커스를 취득한 후 재생이 끝나기 전까지 유지하지 못할 수 도 있다. 오디오 포커스를 점유하는동안 다른 앱이 포커스를 요청할 수 있기 때문이다. 만약 이런 상황이 발생하면 재생을 일시정지하거나 사용자에게 새로운 오디오 소스를 쉽게 듣게 하기 위해 볼륨을 낮춘다.

오디오 포커스는 협력적이다. 앱은 오디오 포커스 가이드라인에 알맞게 수행해야 하며 시스템은 이 규칙을 강제하지 않는다. 만약 앱이 오디오 포커스를 잃은 상태에서도 재생을 지속하고 싶다면 이를 막을 수는 없다. 이는 나쁜 사용자 경험으로서 이런 식으로 작동하는 앱은 사용자가 삭제할 가능성이 있다.

잘 작동하는 오디오 앱은 다음 일반 가이드라인에 따라 오디오 포커스를 관리해야 한다.

- 재생을 시작하기 전에 requestAudioFocus() 를 즉시 호출하고 AUDIOFOCUS_REQUEST_GRANTED 를 반환하는지 확인한다. 만약 앱이 이 가이드에 맞게 설계되면 requestAudioFocus() 는 미디어 세션의 onPlay() 콜백에서 호출되어야 한다.
- 다른앱이 오디오 포커스를 가지면 재생을 중지하거나 일시정지한다. 또는 볼륨다운 시킨다
- 재생을 멈추면 오디오 포커스를 없앤다

오디오 포커스는 실행중인 안드로이드의 버전에 따라 다르게 처리된다.

- 안드로이드 2.2 부터 오디오 포커스는 requestAudioFocus() 와 abandonAudioFocus() 로 관리한다. 앱은 반드시 AudioManager.OnAudioFocusChangeListener 를 등록하여 콜백을 받고 자체적인 오디오 레벨을 관리한다.
- 안드로이드 5.0 이나 이 후에서는 오디오 앱은 AudioAttributes 를 사용해 재생중인 오디오 앱의 형식을 나타낸다. 예를 들어 앱이 스피치를 재생하면 CONTENT_TYPE_SPEECH 를 지정한다.
- 안드로이드 8.0 이후는 AudioFocusRequest 파라미터를 받는 requestAudioFocus() 를 사용한다. AudioFocusRequest 는 앱의 오디오 컨텍스트와 기능성에 대한 정보를 포함한다. 시스템은 이 정보를 사용해 오디오 포커스를 얻고 잃음을 자동적으로 관리한다.

안드로이드 8.0 이후의 오디오 포커스

안드로이드 8.0 부터 requestAudioFocus() 를 호출할 때 반드시 AudioFocusRequest 파라미터를 제공한다. 오디오 포커스를 놓으려면 abandonAudioFocusRequest() 를 호출하며 이는 AudioFocusRequest 를 매개변수로 받는다. 같은 AudioFocusRequest 인스턴스이 포커스를 요청하거나 제거할 때 사용된다.

AudioFocusRequest 는 AudioFocusRequest.Builder 를 사용해 생성한다. 포커스 요청이 반드시 요청의 형식을 지정해야 함에 따라 형식은 빌더의 생성자에 포함된다. 빌더의 메소드를 사용해 요청의 다른 필드를 지정한다.

FocusGain필드는 필수사항이고 다른 필드는 선택적이다.


MethodNotes
setFocusGain()이 필드는 모든 요청에서 필수적이다. 이는 안드로이드8.0이전에서 requestAudioFocus()에서 사용된 durationHint 와 같은 값을 받는다. : AUDIOFOCUS_GAIN, AUDIOFOCUS_GAIN_TRANSIENT, AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, AUDIOFOCUS_GAIN_TRASIENT_EXCLUSIVE
setAudioAttributes()

AudioAttributes 는 앱의 유스케이스를 나타낸다. 시스템은 앱이 오디오 포커스를 얻거나 잃을 때 이 들을 확인한다. 속성은 스트림 형식의 개념을 대체한다. 안드로이드 8.0이후에는 볼륨 컨트롤이 아닌 다른 연산을 위한 스트림 형식은 디프리케이트되어다. 오디오 플레이어에서 사용하는 포커스 요청에서 같은 속성을 사용하도록 한다. (다음 테이블에서 보여진 예시에서 보여지듯)

AudioAttributes.Builder 를 사용하여 속성을 먼저 지정하고 이 메소드를 사용해 요청에 속성을 할당한다.

만약 지정되지 않으면 AudioAttributes는 AudioAttributes.USAGE_MEDIA가 된다.

setWillPauseWhenDucked()다른 앱이 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 로 포커스를 요청할 때 앱은 onAudioFocusChange() 콜백을 받지 못하는데 왜냐하면 시스템은 그 자체로 덕킹을 할 수 있기 때문이다. 볼륨 덕 대신 플레이백을 일시정지하려면 setWillPauseWhenDucked(true) 를 호출하고 automatic ducking에 설명되어 있듯 OnAudioFocusChangeListener 를 설정한다.
setAcceptsDelayedFocusGain()

오디오 포커스를 위한 요청은 다른 앱에의해 포커스가 잠겨질때 실패할 수 있다. 이 메소드는 delayed focus gain 을 활성화 하는데 이는 가용할 때 비동기적으로 포커스를 얻는 기능이다.

지연된 포커스 취득은 앱이 포커스가 허락되었는지를 알기위해 받을 콜백을 필요로 하므로 오디오 리퀘스트에서 AudioManager.OnAudioFocusChangeListener 를 지정할 때만 작동한다.

setOnAudioFocusChangeListener()

OnAudioFocusChangeListener는 요청에서 willPauseWhenDucked(true) 또는 setAcceptsDelayedFocusGain(true) 를 지정한 경우에만 필요하다.

리스너를 설정할 두 가지 방법이 있다: 핸들러 매개변수가 있는 것과 있지 않은 것이다. 핸들러는 리스너가 실행될 스레드이다. 만약 핸들러를 지정하지 않으면 핸들러는 메인 Looper를 사용하게 된다.

다음 예시는 AudioFocusRequest를 생성하기 위해 어떻게 AudioFocusRequest.Bulider를 사용하는지를 보여주고 오디오 포커스를 요청하고 제거한다.

mAudioManager= getSystemService(Context.AUDIO_SERVICE) as AudioManager
mFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
  setAudioAttributes(AudioAttributes.Builder().run {
    setUsage(AudioAttributes.USAGE_GAME)
    setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
    build()
  })
  setAcceptsDelayedFocusGain(true)
  setOnAudioFocusChangeListener(afChangeListener, mHandler)
  build()
}
mMediaPlayer = MediaPlayer()
val mFocusLock = Any()

var mPlaybackDelayed = false
var mPlaybackNowAuthorized = false

// ...
val res = mAudioManager.requestAudioFocus(mFocusRequest)
synchronized(mFocusLock) {
  mPlaybackNowAuthorized = when (res) {
    AudioManager.AUDIOFOCUS_REQUEST_FAILED -> false
    AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
      playbackNow()
      true
    }
    AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
      mPlaybackDelayed = true
      false
    }
    else -> false
  }
}

// ...
override fun onAudioFocusChange(focusChange: Int) {
  when (focusChange) {
    AudioManager.AUDIOFOCUS_GAIN ->
      if (mPlaybackDelayed || mResumeOnFocusGain) {
        synchronized(mFocusLock) {
          mPlaybackDelayed = false
          mResumeOnFocusGain = false
        }
        playbackNow()
      }
    AudioManager.AUDIOFOCUS_LOSS -> {
        synchronized(mFocusLock) {
          mResumeOnFocusGain = false
          mPlaybackDelayed = false
        }
        pausePlayback()
      }
    AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
       synchronized(mFocusLock) {
          mResumeOnFocusGain = true
          mPlaybackDelayed = false
        }
        pausePlayback()
      }
    AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
      // .. pausing or ducking depends on your app
     }
  }
}

자동 더킹

안드로이드 8.0에서 다른 앱이 AUDIOFOCUS_GAIN_TRASIENT_MAY_DUCK으로 포커스를 요청하면 앱의 onAudioFocusChange() 콜백을 호출하지 않고 덕과 볼륨 되살리기를 할 수 있다.

자동 더킹이 음악과 비디오 재생 앱에서 허용가능한 작용이라면 오디오 북 앱과 같은 말로된 컨텐트를 재생하는데 유용하지 않다. 이 런 경우 앱은 대신에 일시정지를 하는 것이 좋다.

만약 볼륨을 줄이는대신 덕이 요청될 때 앱을 일시정지하고 싶으면 요구되는 onAudioFocusChange() 콜백 메소드에 일시정지/다시시작 작용을 구현하는 OnAudioFocusChangeListener를 생성한다. setOnAudioFocusChangeListener() 는 리스너를 등록하고 setWillPauseWhenDucked(true)를 호출해 시스템에게 자동 더킹보다는 자신의 콜백을 사용하도록 설정한다.

지연된 포커스 게인

간혹 시스템은 오디오 포커스를 승인하지 못하는데, 포커스가 폰 콜과 같이 다른 앱에 의해 잠긴 상태일때 그렇다. 이 경우에, requestAudioFocus()는 AUDIOFOCUS_REQUEST_FAILED를 반환한다. 이 상황에서는 포커스를 받지 못했으므로 오디오 플레이백을 처리하지 않아야 한다.

setAcceptsDelayedFocusGain(true) 메소드는 포커스를 비동기적으로 요청할 때 사용하는 메소드이다. 이 플래그가 설정되면 포커스가 락되었을 때 요청하면 AUDIOFOCUS_REQUEST_DELAYED가 반환된다. 전화통화 종료와 같이 잠겨진 오디오포커스가 없으면 시스템은 지연된 포커스 요청을 승인하고 앱에 알리기 위해 onAudioFocusChange()를 호출한다.

지연된 포커스를 다루려면 원하는 작용을 구현하기 위한 onAudioFocusChange() 콜백과 함께 OnAudioFocusChangeListener를 생성하고 setOnAudioFocusChangeListener() 를 호출함으로서 리스너를 등록해야 한다.

안드로이드 8.0 이전에서 오디오 포커스

requestAudioFocus() 를 호출할 때 듀레이션 힌트를 지정하여 현재 포커스를 가지고 재생중인 다른앱에서 사용할 수 있도록 한다.
- 앞으로 오디오를 플레이할 계획이 있다면 영구적 오디오 포커스 (AUDIOFOCUS_GAIN)을 요청하여 오디오 포커스의 이전 홀더가 재생을 멈추게한다
- 이전 홀더가 재생을 일시정지하게 짧은 시간에만 오디오를 재생하려면 트랜지언트 포커스를 요청한다. (AUDIOFOCUS_GAIN_TRANSIENT)
- 이전 포커스 오너가 오디오 아웃풋을 덕하면 계속 플레이해도 괜찮고 오디오가 짧은 시간에만 재생되기를 원한다면 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK . 양쪽 오디오 출력은 오디오 스트림에 믹스된다. 더킹은 특별히 오디블 드라이빙 디렉션과 같은 간헐적 오디오 스트림에 적절하다.

requestAudioFocus() 메소드는 또한 AudioManager.OnAudioFocusChangeListener 를 요청한다. 이 리스너는 미디어 세션을 가진 액티비티나 서비스에서 생성되어야한다. 이는 다른 앱에서 오디오 포커스를 취득이나 해제 시 앱이 받을 수 있는 콜백 함수 onAudioFocusChange()를 구현한다.

다음 스닙샷은 STREAM_MUSIC 스트림에서 오디오 포커스를 요청하고 오디오 포커스에서 변화를 다루기 위한 OnAudioFocusChangeListener 를 등록한다. (체인지 리스너는 Responding to an audio focus change에서 다룬다.)

mAudioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
lateinit var afChangeListener : AudioManager.OnAudioFocusChangeListener

...

// 재생을 위해 오디오 포커스 요청
val result: Int = mAudioManager.requestAudioFocus(afChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
}

끝날 때 abandonAudioFocus() 를 호출한다.

mAudioManager.abandonAudioFocus(afChangeListener)

이는 시스템에게 포커스가 더이상 필요치 않고 연계된 OnAudioFocusChangeListener도 등록해제한다. 만약 트랜지언트 포커스를 요청했다면 앱에게 일시정지나 덕드를 알리고 재생을 지속하거나 볼륨을 복원한다.

오디오 포커스 변경에 반응하기

앱이 오디오 포커스를 취득할 때 다른 앱이 오디오 포커스를 요청할 때 릴리즈 할 수 있어야 한다. 이것이 발생하면 requestAudioFocus()에서 할당한 AudioFocusChangeListener의 onAudioFocusChange() 메소드가 호출된다.

focusChange 파라미터가 onAudioFocusChange() 에 건내어지고 이는 발생중인 변화의 종류를 가리킨다. 포커스를 취득하는 앱에서 사용된 듀레이션 힌트에 연계된다. 앱은 적절히 반응해야 한다.

포커스의 트랜지언트 소실
만약 포커스 변화가 트랜지언트이면 (AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 또는 AUDIOFOCUS_LOSS_TRANSIENT) 앱은 덕(자동 더킹에 의존하지 않으면)이나 일지정지를하며 그렇지 않으면 현 상태를 유지한다.
오디오 포커스의 트랜지언트 소실중에 오디오 포커스 변화를 계속 감지해야하며 포커스를 다시 얻었을 때의 일반 재생 재시작에 준비해야한다. 블로킹 앱이 포커스를 해제했을 때 AUDIOFOCUS_GAIN 콜백을 얻게된다. 이 상황에서 볼륨을 일반레벨로 복원하거나 재생을 다시 시작한다.

포커스의 영구적 소실
만약 오디오 포커스 소실이 영구적 (AUDIOFOCUS_LOSS) 이면 다른 앱이 오디오를 재생하는 것이다. 즉시 재생을 일시정지한다. 이는 AUDIOFOCUS_GAIN 콜백이 발생하지 않는다. 재생을 다시시작하려면 알림의 재생 트랜스포트 컨트롤이나 앱 UI를 누르는 것과 같은 사용자의 명시적 액션이 필요하다.

다음 코드 스닙샷은 OnAudioFocusChangeListener와 onAudioFocusChange()를 어떻게 구현하는지 보여준다. Handler 는 오디오 포커스의 영구적 소실에서 중지 콜백을 지연하기 위해 필요하다.

private val mHandler = Handler()
private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
  when (focusChange) {
    AudioManager.AUDIOFOCUS_LOSS -> {
      // 영구적 오디오 포커스 소실
      // 즉시 재생을 일시정지
      mediaController.transportControls.pause()
      // 재생을 중지하기 위해 30초 대기
      mHandler.postDelayed(mDelayedStopRunnable, TimeUnit.SECONDS.toMillis(30))
    }
    AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
      // 재생 일시정지
    }
    AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
      // 낮은 볼륨, 재생은 지속
    }
    AudioManager.AUDIOFOCUS_GAIN -> {
      // 오디오 포커스를 다시 얻음
      // 볼륨을 표준 상태로 복원, 필요한 경우 재생 재시작
    }
  }
}

핸들러는 이렇게 생긴 Runnable을 사용한다.

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

사용자가 재생을 재시작한 경우에 대비해 지연된 중지가 방해하지 않도록 mHandler.removeCallbacks(mDelayedStopRunnable) 을 상태변화 반응에서 처리한다. 예를 들어 콜백의 onPlay()에서 removeCallbacks()를 호출해준다. 또한 이 함수를 서비스의 리소스를 제거할 때의  onDestroy() 콜백에서도 호출해준다.



덧글

댓글 입력 영역