Audio focus: Restore full volume if focus is abandoned when ducked

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
This commit is contained in:
olly 2020-04-07 12:45:18 +01:00 committed by Oliver Woodman
parent 5a7dbae18c
commit 20cadd6e04
4 changed files with 134 additions and 64 deletions

View file

@ -167,16 +167,18 @@
* Allow missing hours and milliseconds in SubRip (.srt) timecodes
([#7122](https://github.com/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://github.com/google/ExoPlayer/issues/7182).
* Workaround issue that could cause slower than realtime playback of AAC on
Android 10 ([#6671](https://github.com/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://github.com/google/ExoPlayer/issues/7134)).
* Workaround issue that could cause slower than realtime playback of AAC on
Android 10 ([#6671](https://github.com/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://github.com/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://github.com/google/ExoPlayer/issues/7129)).
* Fix playback of ADTS files with mid-stream ID3 metadata.

View file

@ -75,15 +75,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@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. */
@ -100,7 +97,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final AudioManager audioManager;
private final AudioFocusListener focusListener;
private final PlayerControl playerControl;
@Nullable private PlayerControl playerControl;
@Nullable private AudioAttributes audioAttributes;
@AudioFocusState private int audioFocusState;
@ -165,6 +162,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
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
@ -183,10 +189,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
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;
}
}
@ -200,7 +206,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} else {
abandonAudioFocusDefault();
}
audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS;
setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS);
}
private int requestAudioFocusDefault() {
@ -325,63 +331,55 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
}
private void handleAudioFocusChange(int focusChange) {
// Convert the platform focus change to internal state.
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS:
audioFocusState = AUDIO_FOCUS_STATE_LOST_FOCUS;
break;
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;
} else {
audioFocusState = 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);
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) {
this.volumeMultiplier = volumeMultiplier;
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:
executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY);
abandonAudioFocus();
return;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || willPauseWhenDucked()) {
executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK);
setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT);
} else {
setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK);
}
return;
default:
Log.w(TAG, "Unknown focus change type: " + focusChange);
}
}
private void executePlayerCommand(@PlayerCommand int playerCommand) {
if (playerControl != null) {
playerControl.executePlayerCommand(playerCommand);
}
}
// Internal audio focus listener.
private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener {
@ -393,7 +391,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override
public void onAudioFocusChange(int focusChange) {
eventHandler.post(() -> handleAudioFocusChange(focusChange));
eventHandler.post(() -> handlePlatformAudioFocusChange(focusChange));
}
}
}

View file

@ -1565,9 +1565,9 @@ public class SimpleExoPlayer extends BasePlayer
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) {

View file

@ -229,6 +229,54 @@ public class AudioFocusManagerTest {
.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() {
@ -318,6 +366,28 @@ public class AudioFocusManagerTest {
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
@ -367,7 +437,7 @@ public class AudioFocusManagerTest {
}
@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();
@ -385,7 +455,7 @@ public class AudioFocusManagerTest {
@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 =
@ -411,7 +481,7 @@ public class AudioFocusManagerTest {
@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 =