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
This commit is contained in:
parent
3c0e617837
commit
cc29798d9b
@ -14,6 +14,9 @@
|
|||||||
* Allow missing hours and milliseconds in SubRip (.srt) timecodes
|
* Allow missing hours and milliseconds in SubRip (.srt) timecodes
|
||||||
([#7122](https://github.com/google/ExoPlayer/issues/7122)).
|
([#7122](https://github.com/google/ExoPlayer/issues/7122)).
|
||||||
* Audio:
|
* 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
|
* Workaround issue that could cause slower than realtime playback of AAC on
|
||||||
Android 10 ([#6671](https://github.com/google/ExoPlayer/issues/6671).
|
Android 10 ([#6671](https://github.com/google/ExoPlayer/issues/6671).
|
||||||
* Enable playback speed adjustment and silence skipping for floating point PCM
|
* Enable playback speed adjustment and silence skipping for floating point PCM
|
||||||
|
@ -39,6 +39,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.drm.DefaultDrmSessionManager;
|
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
@ -380,6 +381,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;
|
|||||||
*/
|
*/
|
||||||
@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;
|
|||||||
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;
|
|||||||
(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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,7 +168,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();
|
||||||
@ -188,17 +188,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 =
|
||||||
@ -206,9 +207,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()
|
||||||
@ -232,7 +252,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()
|
||||||
@ -257,7 +277,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);
|
||||||
@ -277,7 +297,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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user