Skip to content

Commit

Permalink
Audio focus: Re-request audio focus if in a transient loss state
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ojw28 committed Apr 7, 2020
1 parent 3c0e617 commit cc29798
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 33 deletions.
3 changes: 3 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
Expand Down Expand Up @@ -380,6 +381,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() {
Expand Down Expand Up @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,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();
Expand All @@ -188,27 +188,47 @@ 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 =
Shadows.shadowOf(audioManager).getLastAudioFocusRequest();
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()
Expand All @@ -232,7 +252,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()
Expand All @@ -257,7 +277,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);
Expand All @@ -277,7 +297,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);
Expand Down

0 comments on commit cc29798

Please sign in to comment.