diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0bfe39b1ce..5ecaeb9568 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java b/library/core/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java index 30da62b944..5aeca440ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java @@ -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)); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index e940be00a2..b09cdace9d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -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) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java index 8735840e7b..f05d001adf 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java @@ -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 =