mirror of
https://github.com/samsonjs/media.git
synced 2026-03-30 10:15:48 +00:00
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:
parent
5a7dbae18c
commit
20cadd6e04
4 changed files with 134 additions and 64 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
Loading…
Reference in a new issue