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 dc813ec commit 8d2bc7d
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 33 deletions.
41 changes: 41 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) ###

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
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 @@ -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();
Expand All @@ -191,27 +191,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 @@ -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()
Expand All @@ -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);
Expand All @@ -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);
Expand Down

0 comments on commit 8d2bc7d

Please sign in to comment.