Commit 8d2bc7d1 by olly Committed by Oliver Woodman

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
parent dc813eca
...@@ -153,6 +153,47 @@ ...@@ -153,6 +153,47 @@
* FFmpeg extension: Add support for `x86_64` architecture. * FFmpeg extension: Add support for `x86_64` architecture.
* Opus extension: Fix parsing of negative gain values * Opus extension: Fix parsing of negative gain values
([#7046](https://github.com/google/ExoPlayer/issues/7046)). ([#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) ### ### 2.11.3 (2020-02-19) ###
......
...@@ -38,6 +38,7 @@ import com.google.android.exoplayer2.PlaybackPreparer; ...@@ -38,6 +38,7 @@ import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer; 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.demo.Sample.UriSample;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
...@@ -374,6 +375,7 @@ public class PlayerActivity extends AppCompatActivity ...@@ -374,6 +375,7 @@ public class PlayerActivity extends AppCompatActivity
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.build(); .build();
player.addListener(new PlayerEventListener()); player.addListener(new PlayerEventListener());
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
player.setPlayWhenReady(startAutoPlay); player.setPlayWhenReady(startAutoPlay);
player.addAnalyticsListener(new EventLogger(trackSelector)); player.addAnalyticsListener(new EventLogger(trackSelector));
playerView.setPlayer(player); playerView.setPlayer(player);
......
...@@ -158,10 +158,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -158,10 +158,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/ */
@PlayerCommand @PlayerCommand
public int updateAudioFocus(boolean playWhenReady, @Player.State int playbackState) { public int updateAudioFocus(boolean playWhenReady, @Player.State int playbackState) {
if (!shouldHandleAudioFocus(playbackState)) { if (shouldAbandonAudioFocus(playbackState)) {
if (audioFocusState != AUDIO_FOCUS_STATE_NO_FOCUS) { abandonAudioFocus();
abandonAudioFocus();
}
return playWhenReady ? PLAYER_COMMAND_PLAY_WHEN_READY : PLAYER_COMMAND_DO_NOT_PLAY; return playWhenReady ? PLAYER_COMMAND_PLAY_WHEN_READY : PLAYER_COMMAND_DO_NOT_PLAY;
} }
return playWhenReady ? requestAudioFocus() : PLAYER_COMMAND_DO_NOT_PLAY; return playWhenReady ? requestAudioFocus() : PLAYER_COMMAND_DO_NOT_PLAY;
...@@ -174,33 +172,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -174,33 +172,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return focusListener; return focusListener;
} }
private boolean shouldHandleAudioFocus(@Player.State int playbackState) { private boolean shouldAbandonAudioFocus(@Player.State int playbackState) {
return playbackState != Player.STATE_IDLE && focusGain == C.AUDIOFOCUS_GAIN; return playbackState == Player.STATE_IDLE || focusGain != C.AUDIOFOCUS_GAIN;
} }
@PlayerCommand @PlayerCommand
private int requestAudioFocus() { private int requestAudioFocus() {
int focusRequestResult; if (audioFocusState == AUDIO_FOCUS_STATE_HAVE_FOCUS) {
return PLAYER_COMMAND_PLAY_WHEN_READY;
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;
} }
int requestResult = Util.SDK_INT >= 26 ? requestAudioFocusV26() : requestAudioFocusDefault();
if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { 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 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() { private void abandonAudioFocus() {
...@@ -388,8 +376,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -388,8 +376,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
(audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK) (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK)
? AudioFocusManager.VOLUME_MULTIPLIER_DUCK ? AudioFocusManager.VOLUME_MULTIPLIER_DUCK
: AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT; : AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT;
if (AudioFocusManager.this.volumeMultiplier != volumeMultiplier) { if (this.volumeMultiplier != volumeMultiplier) {
AudioFocusManager.this.volumeMultiplier = volumeMultiplier; this.volumeMultiplier = volumeMultiplier;
playerControl.setVolumeMultiplier(volumeMultiplier); playerControl.setVolumeMultiplier(volumeMultiplier);
} }
} }
......
...@@ -171,7 +171,7 @@ public class AudioFocusManagerTest { ...@@ -171,7 +171,7 @@ public class AudioFocusManagerTest {
} }
@Test @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 // Ensure that when playWhenReady is true while the player is IDLE, audio focus is only
// requested after calling prepare (= changing the state to BUFFERING). // requested after calling prepare (= changing the state to BUFFERING).
AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
...@@ -191,17 +191,18 @@ public class AudioFocusManagerTest { ...@@ -191,17 +191,18 @@ public class AudioFocusManagerTest {
} }
@Test @Test
public void updateAudioFocusFromPausedToPlaying_setsPlayerCommandPlayWhenReady() { public void updateAudioFocus_pausedToPlaying_setsPlayerCommandPlayWhenReady() {
// Ensure that audio focus is not requested until playWhenReady is true.
AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build();
Shadows.shadowOf(audioManager) Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
audioFocusManager.setAudioAttributes(media); audioFocusManager.setAudioAttributes(media);
// Audio focus should not be requested yet, because playWhenReady=false.
assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY)) assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY);
assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull(); assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull();
// Audio focus should be requested now that playWhenReady=true.
assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY))
.isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY);
ShadowAudioManager.AudioFocusRequest request = ShadowAudioManager.AudioFocusRequest request =
...@@ -209,9 +210,28 @@ public class AudioFocusManagerTest { ...@@ -209,9 +210,28 @@ public class AudioFocusManagerTest {
assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN); 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 @Test
@Config(maxSdk = 25) @Config(maxSdk = 25)
public void updateAudioFocusFromReadyToIdle_abandonsAudioFocus() { public void updateAudioFocus_readyToIdle_abandonsAudioFocus() {
// Ensure that stopping the player (=changing state to idle) abandons audio focus. // Ensure that stopping the player (=changing state to idle) abandons audio focus.
AudioAttributes media = AudioAttributes media =
new AudioAttributes.Builder() new AudioAttributes.Builder()
...@@ -235,7 +255,7 @@ public class AudioFocusManagerTest { ...@@ -235,7 +255,7 @@ public class AudioFocusManagerTest {
@Test @Test
@Config(minSdk = 26, maxSdk = TARGET_SDK) @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. // Ensure that stopping the player (=changing state to idle) abandons audio focus.
AudioAttributes media = AudioAttributes media =
new AudioAttributes.Builder() new AudioAttributes.Builder()
...@@ -260,7 +280,7 @@ public class AudioFocusManagerTest { ...@@ -260,7 +280,7 @@ public class AudioFocusManagerTest {
@Test @Test
@Config(maxSdk = 25) @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. // Ensure that changing state to idle is a no-op if audio focus isn't handled.
Shadows.shadowOf(audioManager) Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
...@@ -280,7 +300,7 @@ public class AudioFocusManagerTest { ...@@ -280,7 +300,7 @@ public class AudioFocusManagerTest {
@Test @Test
@Config(minSdk = 26, maxSdk = TARGET_SDK) @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. // Ensure that changing state to idle is a no-op if audio focus isn't handled.
Shadows.shadowOf(audioManager) Shadows.shadowOf(audioManager)
.setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment