Skip to content

Commit

Permalink
Audio focus: Restore full volume if focus is abandoned when ducked
Browse files Browse the repository at this point in the history
If we're in the ducked state and updateAudioFocus is called with a
new state for which focus is no longer required, we should restore
the player back to full volume.

Issue: #7182
PiperOrigin-RevId: 305232155
  • Loading branch information
ojw28 committed Apr 7, 2020
1 parent 5a7dbae commit 20cadd6
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 61 deletions.
12 changes: 7 additions & 5 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,16 +167,18 @@
* Allow missing hours and milliseconds in SubRip (.srt) timecodes
([#7122](https:/google/ExoPlayer/issues/7122)).
* Audio:
* Prevent case where another app spuriously holding transient audio focus
could prevent ExoPlayer from acquiring audio focus for an indefinite period
of time ([#7182](https:/google/ExoPlayer/issues/7182).
* Workaround issue that could cause slower than realtime playback of AAC on
Android 10 ([#6671](https:/google/ExoPlayer/issues/6671).
* Enable playback speed adjustment and silence skipping for floating point PCM
audio, via resampling to 16-bit integer PCM. To output the original floating
point audio without adjustment, pass `enableFloatOutput=true` to the
`DefaultAudioSink` constructor
([#7134](https:/google/ExoPlayer/issues/7134)).
* Workaround issue that could cause slower than realtime playback of AAC on
Android 10 ([#6671](https:/google/ExoPlayer/issues/6671).
* Fix case where another app spuriously holding transient audio focus could
prevent ExoPlayer from acquiring audio focus for an indefinite period of
time ([#7182](https:/google/ExoPlayer/issues/7182).
* Fix case where the player volume could be permanently ducked if audio focus
was released whilst ducking.
* Fix playback of WAV files with trailing non-media bytes
([#7129](https:/google/ExoPlayer/issues/7129)).
* Fix playback of ADTS files with mid-stream ID3 metadata.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,12 @@ public interface PlayerControl {
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
AUDIO_FOCUS_STATE_LOST_FOCUS,
AUDIO_FOCUS_STATE_NO_FOCUS,
AUDIO_FOCUS_STATE_HAVE_FOCUS,
AUDIO_FOCUS_STATE_LOSS_TRANSIENT,
AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK
})
private @interface AudioFocusState {}
/** No audio focus was held, but has been lost by another app taking it permanently. */
private static final int AUDIO_FOCUS_STATE_LOST_FOCUS = -1;
/** No audio focus is currently being held. */
private static final int AUDIO_FOCUS_STATE_NO_FOCUS = 0;
/** The requested audio focus is currently held. */
Expand All @@ -100,7 +97,7 @@ public interface PlayerControl {

private final AudioManager audioManager;
private final AudioFocusListener focusListener;
private final PlayerControl playerControl;
@Nullable private PlayerControl playerControl;
@Nullable private AudioAttributes audioAttributes;

@AudioFocusState private int audioFocusState;
Expand Down Expand Up @@ -165,6 +162,15 @@ public int updateAudioFocus(boolean playWhenReady, @Player.State int playbackSta
return playWhenReady ? requestAudioFocus() : PLAYER_COMMAND_DO_NOT_PLAY;
}

/**
* Called when the manager is no longer required. Audio focus will be released without making any
* calls to the {@link PlayerControl}.
*/
public void release() {
playerControl = null;
abandonAudioFocus();
}

// Internal methods.

@VisibleForTesting
Expand All @@ -183,10 +189,10 @@ private int requestAudioFocus() {
}
int requestResult = Util.SDK_INT >= 26 ? requestAudioFocusV26() : requestAudioFocusDefault();
if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
audioFocusState = AUDIO_FOCUS_STATE_HAVE_FOCUS;
setAudioFocusState(AUDIO_FOCUS_STATE_HAVE_FOCUS);
return PLAYER_COMMAND_PLAY_WHEN_READY;
} else {
audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS;
setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS);
return PLAYER_COMMAND_DO_NOT_PLAY;
}
}
Expand All @@ -200,7 +206,7 @@ private void abandonAudioFocus() {
} else {
abandonAudioFocusDefault();
}
audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS;
setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS);
}

private int requestAudioFocusDefault() {
Expand Down Expand Up @@ -325,60 +331,52 @@ private static int convertAudioAttributesToFocusGain(@Nullable AudioAttributes a
}
}

private void handleAudioFocusChange(int focusChange) {
// Convert the platform focus change to internal state.
private void setAudioFocusState(@AudioFocusState int audioFocusState) {
if (this.audioFocusState == audioFocusState) {
return;
}
this.audioFocusState = audioFocusState;

float volumeMultiplier =
(audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK)
? AudioFocusManager.VOLUME_MULTIPLIER_DUCK
: AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT;
if (this.volumeMultiplier == volumeMultiplier) {
return;
}
this.volumeMultiplier = volumeMultiplier;
if (playerControl != null) {
playerControl.setVolumeMultiplier(volumeMultiplier);
}
}

private void handlePlatformAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
setAudioFocusState(AUDIO_FOCUS_STATE_HAVE_FOCUS);
executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY);
return;
case AudioManager.AUDIOFOCUS_LOSS:
audioFocusState = AUDIO_FOCUS_STATE_LOST_FOCUS;
break;
executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY);
abandonAudioFocus();
return;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
if (willPauseWhenDucked()) {
audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT;
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || willPauseWhenDucked()) {
executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK);
setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT);
} else {
audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK;
setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK);
}
break;
case AudioManager.AUDIOFOCUS_GAIN:
audioFocusState = AUDIO_FOCUS_STATE_HAVE_FOCUS;
break;
default:
Log.w(TAG, "Unknown focus change type: " + focusChange);
// Early return.
return;
}

// Handle the internal state (change).
switch (audioFocusState) {
case AUDIO_FOCUS_STATE_NO_FOCUS:
// Focus was not requested; nothing to do.
break;
case AUDIO_FOCUS_STATE_LOST_FOCUS:
playerControl.executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY);
abandonAudioFocus();
break;
case AUDIO_FOCUS_STATE_LOSS_TRANSIENT:
playerControl.executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK);
break;
case AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK:
// Volume will be adjusted by the code below.
break;
case AUDIO_FOCUS_STATE_HAVE_FOCUS:
playerControl.executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY);
break;
default:
throw new IllegalStateException("Unknown audio focus state: " + audioFocusState);
Log.w(TAG, "Unknown focus change type: " + focusChange);
}
}

float volumeMultiplier =
(audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK)
? AudioFocusManager.VOLUME_MULTIPLIER_DUCK
: AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT;
if (this.volumeMultiplier != volumeMultiplier) {
this.volumeMultiplier = volumeMultiplier;
playerControl.setVolumeMultiplier(volumeMultiplier);
private void executePlayerCommand(@PlayerCommand int playerCommand) {
if (playerControl != null) {
playerControl.executePlayerCommand(playerCommand);
}
}

Expand All @@ -393,7 +391,7 @@ public AudioFocusListener(Handler eventHandler) {

@Override
public void onAudioFocusChange(int focusChange) {
eventHandler.post(() -> handleAudioFocusChange(focusChange));
eventHandler.post(() -> handlePlatformAudioFocusChange(focusChange));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1565,9 +1565,9 @@ public void stop(boolean reset) {
public void release() {
verifyApplicationThread();
audioBecomingNoisyManager.setEnabled(false);
audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_IDLE);
wakeLockManager.setStayAwake(false);
wifiLockManager.setStayAwake(false);
audioFocusManager.release();
player.release();
removeSurfaceCallbacks();
if (surface != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,54 @@ public void updateAudioFocus_pausedToPlaying_withTransientLoss_setsPlayerCommand
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
}

@Test
public void updateAudioFocus_pausedToPlaying_withTransientDuck_setsPlayerCommandPlayWhenReady() {
AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
audioFocusManager.setAudioAttributes(media);

assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);

// Simulate transient ducking.
audioFocusManager
.getFocusListener()
.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f);

// Focus should be re-requested, rather than staying in a state of transient ducking. This
// should restore the volume to 1.0.
assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
assertThat(testPlayerControl.lastVolumeMultiplier).isEqualTo(1.0f);
}

@Test
public void updateAudioFocus_abandonFocusWhenDucked_restoresFullVolume() {
AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
audioFocusManager.setAudioAttributes(media);

assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);

// Simulate transient ducking.
audioFocusManager
.getFocusListener()
.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f);

// Configure the manager to no longer handle audio focus.
audioFocusManager.setAudioAttributes(null);

// Focus should be abandoned, which should restore the volume to 1.0.
assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
assertThat(testPlayerControl.lastVolumeMultiplier).isEqualTo(1.0f);
}

@Test
@Config(maxSdk = 25)
public void updateAudioFocus_readyToIdle_abandonsAudioFocus() {
Expand Down Expand Up @@ -318,6 +366,28 @@ public void updateAudioFocus_readyToIdle_withoutHandlingAudioFocus_isNoOp_v26()
assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull();
}

@Test
public void release_doesNotCallPlayerControlToRestoreVolume() {
AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
audioFocusManager.setAudioAttributes(media);

assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);

// Simulate transient ducking.
audioFocusManager
.getFocusListener()
.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f);

audioFocusManager.release();

// PlaybackController.setVolumeMultiplier should not have been called to restore the volume.
assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f);
}

@Test
public void onAudioFocusChange_withDuckEnabled_volumeReducedAndRestored() {
// Ensure that the volume multiplier is adjusted when audio focus is lost to
Expand Down Expand Up @@ -367,7 +437,7 @@ public void onAudioFocusChange_withPausedWhenDucked_sendsCommandWaitForCallback(
}

@Test
public void onAudioFocusChange_withTransientLost_sendsCommandWaitForCallback() {
public void onAudioFocusChange_withTransientLoss_sendsCommandWaitForCallback() {
// Ensure that the player is commanded to pause when audio focus is lost with
// AUDIOFOCUS_LOSS_TRANSIENT.
AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
Expand All @@ -385,7 +455,7 @@ public void onAudioFocusChange_withTransientLost_sendsCommandWaitForCallback() {

@Test
@Config(maxSdk = 25)
public void onAudioFocusChange_withAudioFocusLost_sendsDoNotPlayAndAbandondsFocus() {
public void onAudioFocusChange_withFocusLoss_sendsDoNotPlayAndAbandonsFocus() {
// Ensure that AUDIOFOCUS_LOSS causes AudioFocusManager to pause playback and abandon audio
// focus.
AudioAttributes media =
Expand All @@ -411,7 +481,7 @@ public void onAudioFocusChange_withAudioFocusLost_sendsDoNotPlayAndAbandondsFocu

@Test
@Config(minSdk = 26, maxSdk = TARGET_SDK)
public void onAudioFocusChange_withAudioFocusLost_sendsDoNotPlayAndAbandondsFocus_v26() {
public void onAudioFocusChange_withFocusLoss_sendsDoNotPlayAndAbandonsFocus_v26() {
// Ensure that AUDIOFOCUS_LOSS causes AudioFocusManager to pause playback and abandon audio
// focus.
AudioAttributes media =
Expand Down

0 comments on commit 20cadd6

Please sign in to comment.