23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
Android 앱을 개발할 때 우리는 CPU, 메모리, 배터리, 네트워크 등 다양한 자원을 신중하게 다룹니다.
성능을 최적화하고 사용자 경험을 향상시키기 위해 리소스를 아끼고, 효율적으로 사용하는 것은 개발자의 기본 소양이죠.
우리가 멀티미디어 앱을 개발할 때 고려해야할 중요한 리소스가 있습니다.
바로 오디오에 대한 권한, 즉 Audio Focus입니다. 오디오 포커스는 시스템이 관리하는 공유 리소스입니다.
하나의 앱이 독점할 수도 있고, 다른 앱과 양보하며 사용할 수도 있습니다. 이를 무시하거나 무분별하게 다루면 사용자 경험은 순식간에 망가질 수 있습니다.
이 글에서 우리는 Audio Focus를 하나의 소중한 리소스로 보고, 이를 올바르게 요청하고 반납하며, 다른 앱과 어떻게 공존할 수 있을지를 살펴보겠습니다.
Audio Focus는 단순한 권장사항이 아닙니다. 이를 무시하고 오디오 재생이나 녹음을 수행할 경우, 사용자는 다음과 같은 혼란스러운 경험을 겪을 수 있습니다.
다음은 Android 앱 개발시 발생할 수 있는 대표적인 사례들입니다.
사용자는 음악 스트리밍 앱을 실행한 상태에서 팟캐스트나 라디오 앱을 켭니다.
라디오 앱이 Audio Focus를 요청하지 않고 오디오를 재생합니다.
결과: 두 앱의 소리가 겹쳐 재생되며 사용자 경험이 망가집니다.
사용자가 음악을 듣고 있는 중에 전화가 옵니다.
음악 앱이 재생을 중지하지 않고 계속 재생됩니다.
결과: 전화 상대방의 목소리와 음악이 섞여 들리며 통화 품질이 저하됩니다.
내비게이션 앱이 음성 안내를 시작합니다.
음악 앱은 계속 재생되고 있습니다.
결과: 중요한 안내를 사용자가 놓칠 수 있습니다.
사용자가 음성 메모 앱을 실행해 녹음을 시작합니다.
배경에서 동작 중인 음악 앱이나 라디오 앱이 여전히 재생 중입니다.
결과: 원하지 않은 소리가 녹음될 수 있습니다.
앞서 소개한 시나리오처럼 사용자가 원하지 않는 경험을 피하고, 바람직한 경험을 제공하기 위해 AudioFocus를 어떻게 활용할 수 있는지 살펴보겠습니다.
앞에서 우리는 Audio Focus에 대한 처리 없이 오디오 재생 앱을 개발할 경우에 발생할 수 있는 이슈를 살펴 보았습니다.
이를 해결하기 위해서 Android는 Audio Focus기능을 제공합니다.
다음 그림은 Android 앱이 Audio Focus를 사용하여 서로 다른 앱들이 오디오를 재생하는 일반적인 케이스를 보여줍니다.
음악 앱이 AudioManager에게 AudioFocus를 요청합니다.
AudioManager는 음악 앱의 AudioFocus 요청을 승인(Grant) 합니다.
음악 앱은 음악을 재생합니다.
라디오 앱이 AudioManager에게 AudioFocus를 요청합니다.
AudioManager는 라디오 앱의 AudioFocus 요청을 승인(Grant) 합니다.
AudioManager는 현재 AudioFocus를 가지고 있는 음악 앱에게 AudioFocus를 잃었음(LOSS)을 알립니다.
음악 앱은 현재 재생 중인 음악을 일시정지 혹은 정지합니다.
라디오 앱은 재생을 시작합니다.
라디오 앱은 재생이 끝나면 사용한 AudioFocus를 반환합니다.
지금부터 위에서 사용된 요소들에 대해 간단히 살펴보겠습니다.
AudioManager : Audio Focus를 요청하거나 해제할 때 사용하는 Android Audio 관리를 위한 핵심 시스템 서비스 클래스
AudioFocusRequest : Audio Focus 요청에 대한 상세를 가진 클래스
AudioManager.OnAudioFocusChangeListener : Audio Focus 상태가 변경될때 호출받을 Callback 인터페이스
AudioManager.requestAudioFocus(AudioFocusRequest): Int : Audio Focus를 획득 요청하며, 획득 유무 결과를 반환
focusGain: AudioFocusRequest 생성 시, 필수로 필요합니다. focus를 요청 시, 어떤 용도&목적을 가지는지를 의미합니다.
focusGain 종류 | 설명 |
|---|---|
AUDIOFOCUS_GAIN | 사용시간이 길거나 예상되지 않을 경우, 다른 앱에게는 영구적인 정지를 요청하고 싶은 경우 |
AUDIOFOCUS_GAIN_TRANSIENT | 짧게 사용하는 경우 |
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK | 짧게 사용하며 다른 앱에게 Ducking(Volume down)을 요청하고 싶은 경우 |
AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE | 짧게 사용하며 다른 앱에게 Mute혹은 정지 등을 요청하고 싶은 경우 |
AudioManager.abandonAudioFocus(AudioFocusRequest): Int
획득한 Audio Focus를 해제, 해제 결과를 반환(실제 시스템에 문제가 있어 예외가 발생하는 경우를 제외하면, 항상 성공)
직전에 AudioFocus를 가지고 있던(현재는 AUDIOFOCUS_LOSS_TRANSIENT_XXX 상태) AudioFocusRequest로 Focus를 넘겨준다.
(nAudioFocusChange(AUDIOFOCUS_GAIN) 호출)
AudioManager.OnAudioFocusChangeListener.onAudioFocusChange(int focusChange: Int) : Focus 상태가 변경되었을 때 호출됨.
focusChange : 변경된 focus 상태
focusChange 종류 | 설명 |
|---|---|
AUDIOFOCUS_LOSS | 타 앱의 AUDIOFOCUS_GAIN 요청에 의해 Focus를 완전히 잃은 경우 |
AUDIOFOCUS_LOSS_TRANSIENT | 타 앱의 AUDIOFOCUS_GAIN_TRANSIENT or AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 요청에 의해 일시적으로 Focus를 잃은 경우 |
AUDIOFOCUS_LOSS_TRANSIENT_MAY_DUCK | 타 앱의 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 요청에 의해 일시적으로 Focus를 잃은 경우 |
AUDIOFOCUS_GAIN | * AUDIOFOCUS_LOSS(_XXX) 상태에서 Focus를 가진 앱이 Focus를 반환한 경우 * AudioFocus 요청이 Delay된 이후 획득한 경우 |
AudioManager를 통해 Audio Focus를 요청한다고 해서 항상 성공하는 것은 아닙니다.
시스템은 다양한 이유로 요청을 거부할 수 있으며, 이 경우 반환값으로 AUDIOFOCUS_REQUEST_FAILED가 전달됩니다.
이러한 실패는 흔치 않지만, 앱이 안정적으로 오디오를 재생하려면 반드시 실패 케이스에 대한 예외처리 필요합니다.
1. 특별한 권한을 가진 앱이 Audio Focus를 사용 중인 경우
우리는 앞서 이 상황을 전화 시나리오에서 확인했습니다. 전화는 Android 시스템에서 특수하게 처리되는 오디오 사용 케이스입니다.
통화가 시작되면(ringtone이 울리면), 시스템은 내부적으로 Audio Focus를 잠금(Lock) 상태로 전환하며, 일반 앱의 Audio Focus 요청을 거부합니다.
이러한 처리는 Android의 오디오 포커스 정책에 따라 구현되어 있으며, 자세한 흐름은 Android 오픈소스 코드(Android Open Source Project, AOSP)를 통해 확인할 수 있습니다.
그렇다면 우리 앱도 Audio Focus를 잠금 상태로 점유할 수는 없을까요?
결론부터 말씀드리자면, Android는 일반 앱에게 그런 권한을 허용하지 않습니다.
AudioManager 클래스를 살펴보면, AUDIOFOCUS_FLAG_LOCK이라는 플래그가 존재하지만, 이 값은 숨김(hide) 처리되어 있어 일반 앱에서는 접근할 수 없습니다.
이 플래그는 AudioPolicy를 통해 제어되며, 주로 제조사 시스템 앱이나 특수 권한을 가진 앱에서만 사용됩니다.
즉, 일반적인 서드파티 앱이 Audio Focus를 강제로 잠그는 동작은 시스템 차원에서 허용되지 않도록 설계되어 있습니다.
이는 다양한 앱들이 공존하는 Android 생태계에서 오디오 자원의 공정한 사용을 보장하기 위한 정책입니다.
2. 잘못된 AudioFocus 요청
AudioFocusRequest를 올바르게 설정하지 경우:
허용되지 않은 값들을 설정한 경우
설정한 값이 적절하지 않은 경우
개발과정에서 쉽게 발견하고 수정할 수 있습니다.
3. 시스템에서 너무 많은 AudioFocus를 사용 중인 경우
드문 경우지만, 여러 앱이 동시에 실행 중인 상황에서 Audio Focus를 요청한 뒤 해제하지 않으면, 시스템 내부에 요청이 누적될 수 있습니다.
이렇게 정상적으로 반납되지 않은 Focus 요청이 계속 쌓이면, 결국 시스템은 새로운 Audio Focus 요청을 거부하게 됩니다.
아래는 관련코드와 함께, Audio Focus 요청이 거부되었을 때 시스템에서 출력된 실제 로그 메시지입니다.
// MediaFocusControl.java
/**
* Arbitrary maximum size of audio focus stack to prevent apps OOM'ing this process.
*/
private static final int MAX_STACK_SIZE = 100;
if (mFocusStack.size() > MAX_STACK_SIZE) {
Log.e(TAG, "Max AudioFocus stack size reached, failing requestAudioFocus()");
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}2025-05-15 10:59:32.904 2369-5239 MediaFocusControl pid-2369 E Max AudioFocus stack size reached, failing requestAudioFocus()네. 없습니다. 설명에서 본 것처럼 다른 앱이 AUDIOFOCUS_GAIN_TRANSIENT or AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 로 Focus를 요청하면
현재 Focus를 가진 앱은 AUDIOFOCUS_LOSS_TRANSIENT 상태만 전달 받습니다.
그럼 AUDIOFOCUS_GAIN_TRANSIENT or AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 는 무엇이 다른 걸까요?
다음코드를 통해서 우리는 현재 Focus가 Exclusive이고 Audio Recording 중이면 Notification 소리를 내지 않는 것을 확인할 수 있습니다.
이런 코드 일부를 통해서, 시스템 내부에서 소리를 재생해야하는 경우를 차단하고 있는 것을 알 수 있습니다.
// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/services/core/java/com/android/server/audio/AudioService.java#14917
/**
* @see AudioManager#shouldNotificationSoundPlay(AudioAttributes)
*/
@android.annotation.EnforcePermission(QUERY_AUDIO_STATE)
public boolean shouldNotificationSoundPlay(@NonNull final AudioAttributes aa) {
super.shouldNotificationSoundPlay_enforcePermission();
Objects.requireNonNull(aa);
// don't play notifications if the stream volume associated with the
// AudioAttributes of the notification record is 0 (non-zero volume implies
// not silenced by SILENT or VIBRATE ringer mode)
final int stream = AudioAttributes.toLegacyStreamType(aa);
final boolean mutingFromVolume = getStreamVolume(stream) == 0;
if (mutingFromVolume) {
Slog.i(TAG, "shouldNotificationSoundPlay false: muted stream:" + stream
+ " attr:" + aa);
return false;
}
// don't play notifications if there is a user of GAIN_TRANSIENT_EXCLUSIVE audio focus
// and the focus owner is recording
final int uid = mMediaFocusControl.getExclusiveFocusOwnerUid();
if (uid == -1) { // return value is -1 if focus isn't GAIN_TRANSIENT_EXCLUSIVE
return true;
}
// is the owner of GAIN_TRANSIENT_EXCLUSIVE focus also recording?
final boolean mutingFromFocusAndRecording = mRecordMonitor.isRecordingActiveForUid(uid);
if (mutingFromFocusAndRecording) {
Slog.i(TAG, "shouldNotificationSoundPlay false: exclusive focus owner recording "
+ " uid:" + uid + " attr:" + aa);
return false;
}
return true;
}Android를 개발하다보면 문서만으로는 어떤 기능을 하는지, 어떻게 동작하는지 이해되지 않는 부분을 종종 접할 수 있습니다.
다행히 Android는 그 동안 오픈소스로 제공되어, 대부분의 동작을 실제 코드로 직접 확인할 수 있습니다.
문서만으로 이해되지 않는 부분이 있다면, 코드에서 그 답을 찾아보는건 어떨까요?
Audio Focus에 대한 처리는 단순히 있으면 좋은 부가기능이 아니라, 앱의 기본 동작 안정성과 사용자 경험을 지키기 위한 필수 설계 요소입니다.
음악 플레이어, 녹음 , 음성 안내 등 앱이 소리를 재생하거나 들을 가능성이 있다면 Audio Focus를 관리해야합니다.
지금까지 우리는 AudioFocus를 처리하지 않으면 어떤 문제가 발생할 수 있는지 그리고 어떻게 올바르게 사용하는지에 대해 알아보았습니다.
지금 개발 중인 앱에 Audio Focus 처리가 빠져 있다면, 지금 한 번 반영해보는 것은 어떨까요?
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.