From 8d2bc7d18290f1cb01ce9626d6411510f0f2e3d7 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 6 Apr 2020 21:47:12 +0100 Subject: [PATCH] Audio focus: Re-request audio focus if in a transient loss state This avoids cases where audio focus is never successfully acquired because another app is holding on to transient audio focus indefinitely. Issue: #7182 PiperOrigin-RevId: 305108528 --- RELEASENOTES.md | 41 +++++++++++++++++++ .../exoplayer2/demo/PlayerActivity.java | 2 + .../android/exoplayer2/AudioFocusManager.java | 40 +++++++----------- .../exoplayer2/AudioFocusManagerTest.java | 34 +++++++++++---- 4 files changed, 84 insertions(+), 33 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e178ffdf248..3c5bf13eaed 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -153,6 +153,47 @@ * FFmpeg extension: Add support for `x86_64` architecture. * Opus extension: Fix parsing of negative gain values ([#7046](https://github.com/google/ExoPlayer/issues/7046)). +* Add `SimpleExoPlayer.setWakeMode` to allow automatic `WifiLock` and `WakeLock` + handling ([#6914](https://github.com/google/ExoPlayer/issues/6914)). To use + this feature, you must add the + [WAKE_LOCK](https://developer.android.com/reference/android/Manifest.permission.html#WAKE_LOCK) + permission to your application's manifest file. +* Text: + * Catch and log exceptions in `TextRenderer` rather than re-throwing. This + allows playback to continue even if subtitle decoding fails + ([#6885](https://github.com/google/ExoPlayer/issues/6885)). + * 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)). + * 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. +* DRM: + * Fix playback of Widevine protected content that only provides V1 PSSH atoms + on API levels 21 and 22. + * Fix playback of PlayReady content on Fire TV Stick (Gen 2). +* DASH: + * Update the manifest URI to avoid repeated HTTP redirects + ([#6907](https://github.com/google/ExoPlayer/issues/6907)). + * Parse period `AssetIdentifier` elements. +* UI: Add an option to set whether to use the orientation sensor for rotation + in spherical playbacks + ([#6761](https://github.com/google/ExoPlayer/issues/6761)). +* Analytics: Fix `PlaybackStatsListener` behavior when not keeping history + ([#7160](https://github.com/google/ExoPlayer/issues/7160)). +* FFmpeg extension: Add support for `x86_64` architecture. +* Opus extension: Fix parsing of negative gain values + ([#7046](https://github.com/google/ExoPlayer/issues/7046)). ### 2.11.3 (2020-02-19) ### 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 173ffa02b5c..1f5576bb46e 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 @@ -38,6 +38,7 @@ 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.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; @@ -374,6 +375,7 @@ private void initializePlayer() { .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 86ed841f1a6..30da62b944f 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 @@ public void setAudioAttributes(@Nullable AudioAttributes audioAttributes) { */ @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 @@ public int updateAudioFocus(boolean playWhenReady, @Player.State int playbackSta 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 @@ private void handleAudioFocusChange(int focusChange) { (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 c6a6cf8c55c..8735840e7b0 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 @@ -171,7 +171,7 @@ public void setAudioAttributes_inStateEnded_requestsAudioFocus() { } @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(); @@ -191,17 +191,18 @@ public void updateAudioFocusFromIdleToBuffering_setsPlayerCommandPlayWhenReady() } @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 = @@ -209,9 +210,28 @@ public void updateAudioFocusFromPausedToPlaying_setsPlayerCommandPlayWhenReady() 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() @@ -235,7 +255,7 @@ public void updateAudioFocusFromReadyToIdle_abandonsAudioFocus() { @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() @@ -260,7 +280,7 @@ public void updateAudioFocusFromReadyToIdle_abandonsAudioFocus_v26() { @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); @@ -280,7 +300,7 @@ public void updateAudioFocusFromReadyToIdle_withoutHandlingAudioFocus_isNoOp() { @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);