diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b8074b8c86..a90207c865 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,6 +14,9 @@ * 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 diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index c100c853e7..5a577449fa 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.demo.Sample.UriSample; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -380,6 +381,7 @@ public class PlayerActivity extends AppCompatActivity .setTrackSelector(trackSelector) .build(); player.addListener(new PlayerEventListener()); + player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true); player.setPlayWhenReady(startAutoPlay); player.addAnalyticsListener(new EventLogger(trackSelector)); playerView.setPlayer(player); 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 86ed841f1a..30da62b944 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 @@ -158,10 +158,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ @PlayerCommand public int updateAudioFocus(boolean playWhenReady, @Player.State int playbackState) { - if (!shouldHandleAudioFocus(playbackState)) { - if (audioFocusState != AUDIO_FOCUS_STATE_NO_FOCUS) { - abandonAudioFocus(); - } + if (shouldAbandonAudioFocus(playbackState)) { + abandonAudioFocus(); return playWhenReady ? PLAYER_COMMAND_PLAY_WHEN_READY : PLAYER_COMMAND_DO_NOT_PLAY; } return playWhenReady ? requestAudioFocus() : PLAYER_COMMAND_DO_NOT_PLAY; @@ -174,33 +172,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return focusListener; } - private boolean shouldHandleAudioFocus(@Player.State int playbackState) { - return playbackState != Player.STATE_IDLE && focusGain == C.AUDIOFOCUS_GAIN; + private boolean shouldAbandonAudioFocus(@Player.State int playbackState) { + return playbackState == Player.STATE_IDLE || focusGain != C.AUDIOFOCUS_GAIN; } @PlayerCommand private int requestAudioFocus() { - int focusRequestResult; - - if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { - if (Util.SDK_INT >= 26) { - focusRequestResult = requestAudioFocusV26(); - } else { - focusRequestResult = requestAudioFocusDefault(); - } - audioFocusState = - focusRequestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED - ? AUDIO_FOCUS_STATE_HAVE_FOCUS - : AUDIO_FOCUS_STATE_NO_FOCUS; + if (audioFocusState == AUDIO_FOCUS_STATE_HAVE_FOCUS) { + return PLAYER_COMMAND_PLAY_WHEN_READY; } - - if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { + int requestResult = Util.SDK_INT >= 26 ? requestAudioFocusV26() : requestAudioFocusDefault(); + if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + audioFocusState = AUDIO_FOCUS_STATE_HAVE_FOCUS; + return PLAYER_COMMAND_PLAY_WHEN_READY; + } else { + audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS; return PLAYER_COMMAND_DO_NOT_PLAY; } - - return audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT - ? PLAYER_COMMAND_WAIT_FOR_CALLBACK - : PLAYER_COMMAND_PLAY_WHEN_READY; } private void abandonAudioFocus() { @@ -388,8 +376,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK) ? AudioFocusManager.VOLUME_MULTIPLIER_DUCK : AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT; - if (AudioFocusManager.this.volumeMultiplier != volumeMultiplier) { - AudioFocusManager.this.volumeMultiplier = volumeMultiplier; + if (this.volumeMultiplier != volumeMultiplier) { + this.volumeMultiplier = volumeMultiplier; playerControl.setVolumeMultiplier(volumeMultiplier); } } 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 5509d3ba0a..d5cf8890da 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 @@ -168,7 +168,7 @@ public class AudioFocusManagerTest { } @Test - public void updateAudioFocusFromIdleToBuffering_setsPlayerCommandPlayWhenReady() { + public void updateAudioFocus_idleToBuffering_setsPlayerCommandPlayWhenReady() { // Ensure that when playWhenReady is true while the player is IDLE, audio focus is only // requested after calling prepare (= changing the state to BUFFERING). AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); @@ -188,17 +188,18 @@ public class AudioFocusManagerTest { } @Test - public void updateAudioFocusFromPausedToPlaying_setsPlayerCommandPlayWhenReady() { - // Ensure that audio focus is not requested until playWhenReady is true. + public void updateAudioFocus_pausedToPlaying_setsPlayerCommandPlayWhenReady() { AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); audioFocusManager.setAudioAttributes(media); + // Audio focus should not be requested yet, because playWhenReady=false. assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull(); + // Audio focus should be requested now that playWhenReady=true. assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); ShadowAudioManager.AudioFocusRequest request = @@ -206,9 +207,28 @@ public class AudioFocusManagerTest { assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN); } + // See https://github.com/google/ExoPlayer/issues/7182 for context. + @Test + public void updateAudioFocus_pausedToPlaying_withTransientLoss_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 focus loss. + audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT); + + // Focus should be re-requested, rather than staying in a state of transient focus loss. + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + } + @Test @Config(maxSdk = 25) - public void updateAudioFocusFromReadyToIdle_abandonsAudioFocus() { + public void updateAudioFocus_readyToIdle_abandonsAudioFocus() { // Ensure that stopping the player (=changing state to idle) abandons audio focus. AudioAttributes media = new AudioAttributes.Builder() @@ -232,7 +252,7 @@ public class AudioFocusManagerTest { @Test @Config(minSdk = 26, maxSdk = TARGET_SDK) - public void updateAudioFocusFromReadyToIdle_abandonsAudioFocus_v26() { + public void updateAudioFocus_readyToIdle_abandonsAudioFocus_v26() { // Ensure that stopping the player (=changing state to idle) abandons audio focus. AudioAttributes media = new AudioAttributes.Builder() @@ -257,7 +277,7 @@ public class AudioFocusManagerTest { @Test @Config(maxSdk = 25) - public void updateAudioFocusFromReadyToIdle_withoutHandlingAudioFocus_isNoOp() { + public void updateAudioFocus_readyToIdle_withoutHandlingAudioFocus_isNoOp() { // Ensure that changing state to idle is a no-op if audio focus isn't handled. Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); @@ -277,7 +297,7 @@ public class AudioFocusManagerTest { @Test @Config(minSdk = 26, maxSdk = TARGET_SDK) - public void updateAudioFocusFromReadyToIdle_withoutHandlingAudioFocus_isNoOp_v26() { + public void updateAudioFocus_readyToIdle_withoutHandlingAudioFocus_isNoOp_v26() { // Ensure that changing state to idle is a no-op if audio focus isn't handled. Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);