Move audio focus management to ExoPlayerImplInternal

This ensures all AudioManager calls are moved off the main
thread.

Having the audio focus management on the playback thread
also allows future improvements like requesting audio focus
only just before the player becomes ready (which is recommended
but not currently done by ExoPlayer).

PiperOrigin-RevId: 730962299
(cherry picked from commit 19c7b2127568e05b829efe2d9943be04657cefd1)
This commit is contained in:
tonihei 2025-02-25 11:24:11 -08:00 committed by oceanjules
parent 9052313245
commit fc4112beee
5 changed files with 370 additions and 168 deletions

View File

@ -27,6 +27,7 @@ import android.content.Context;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@ -137,10 +138,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* Constructs an AudioFocusManager to automatically handle audio focus for a player.
*
* @param context The current context.
* @param eventHandler A {@link Handler} to for the thread on which the player is used.
* @param eventLooper A {@link Looper} for the thread on which the audio focus manager is used.
* @param playerControl A {@link PlayerControl} to handle commands from this instance.
*/
public AudioFocusManager(Context context, Handler eventHandler, PlayerControl playerControl) {
public AudioFocusManager(Context context, Looper eventLooper, PlayerControl playerControl) {
this.audioManager =
Suppliers.memoize(
() ->
@ -148,7 +149,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
(AudioManager)
context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE)));
this.playerControl = playerControl;
this.eventHandler = eventHandler;
this.eventHandler = new Handler(eventLooper);
this.audioFocusState = AUDIO_FOCUS_STATE_NOT_REQUESTED;
}

View File

@ -169,7 +169,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
private final ComponentListener componentListener;
private final FrameMetadataListener frameMetadataListener;
private final AudioBecomingNoisyManager audioBecomingNoisyManager;
private final AudioFocusManager audioFocusManager;
@Nullable private final StreamVolumeManager streamVolumeManager;
private final WakeLockManager wakeLockManager;
private final WifiLockManager wifiLockManager;
@ -351,6 +350,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
PlayerId playerId = new PlayerId(builder.playerName);
internalPlayer =
new ExoPlayerImplInternal(
applicationContext,
renderers,
secondaryRenderers,
trackSelector,
@ -408,8 +408,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
new AudioBecomingNoisyManager(
builder.context, playbackLooper, builder.looper, componentListener, clock);
audioBecomingNoisyManager.setEnabled(builder.handleAudioBecomingNoisy);
audioFocusManager = new AudioFocusManager(builder.context, eventHandler, componentListener);
audioFocusManager.setAudioAttributes(builder.handleAudioFocus ? audioAttributes : null);
if (builder.suppressPlaybackOnUnsuitableOutput) {
suitableOutputChecker = builder.suitableOutputChecker;
@ -443,7 +441,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
videoSize = VideoSize.UNKNOWN;
surfaceSize = Size.UNKNOWN;
internalPlayer.setAudioAttributes(audioAttributes);
internalPlayer.setAudioAttributes(audioAttributes, builder.handleAudioFocus);
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_ATTRIBUTES, audioAttributes);
sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_SCALING_MODE, videoScalingMode);
sendRendererMessage(
@ -523,10 +521,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void prepare() {
verifyApplicationThread();
boolean playWhenReady = getPlayWhenReady();
@AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, Player.STATE_BUFFERING);
updatePlayWhenReady(playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playerCommand));
if (playbackInfo.playbackState != Player.STATE_IDLE) {
return;
}
@ -804,9 +798,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void setPlayWhenReady(boolean playWhenReady) {
verifyApplicationThread();
@AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState());
updatePlayWhenReady(playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playerCommand));
updatePlayWhenReady(playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
}
@Override
@ -1007,7 +999,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void stop() {
verifyApplicationThread();
audioFocusManager.updateAudioFocus(getPlayWhenReady(), Player.STATE_IDLE);
stopInternal(/* error= */ null);
currentCueGroup = new CueGroup(ImmutableList.of(), playbackInfo.positionUs);
}
@ -1032,7 +1023,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
wakeLockManager.setStayAwake(false);
wifiLockManager.setStayAwake(false);
audioFocusManager.release();
if (suitableOutputChecker != null) {
suitableOutputChecker.disable();
}
@ -1474,13 +1464,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
listener -> listener.onAudioAttributesChanged(newAudioAttributes));
}
internalPlayer.setAudioAttributes(audioAttributes);
internalPlayer.setAudioAttributes(audioAttributes, handleAudioFocus);
audioFocusManager.setAudioAttributes(handleAudioFocus ? newAudioAttributes : null);
boolean playWhenReady = getPlayWhenReady();
@AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState());
updatePlayWhenReady(playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playerCommand));
listeners.flushEvents();
}
@ -1538,7 +1523,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
return;
}
this.volume = volume;
sendVolumeToInternalPlayer();
internalPlayer.setVolume(volume);
float finalVolume = volume;
listeners.sendEvent(EVENT_VOLUME_CHANGED, listener -> listener.onVolumeChanged(finalVolume));
}
@ -2745,31 +2730,15 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
}
private void sendVolumeToInternalPlayer() {
float scaledVolume = volume * audioFocusManager.getVolumeMultiplier();
internalPlayer.setVolume(scaledVolume);
}
private void updatePlayWhenReady(
boolean playWhenReady,
@AudioFocusManager.PlayerCommand int playerCommand,
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY;
boolean playWhenReady, @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
@PlaybackSuppressionReason
int playbackSuppressionReason = computePlaybackSuppressionReason(playWhenReady, playerCommand);
int playbackSuppressionReason = computePlaybackSuppressionReason(playWhenReady);
if (playbackInfo.playWhenReady == playWhenReady
&& playbackInfo.playbackSuppressionReason == playbackSuppressionReason
&& playbackInfo.playWhenReadyChangeReason == playWhenReadyChangeReason) {
return;
}
updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
playWhenReady, playWhenReadyChangeReason, playbackSuppressionReason);
}
private void updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
boolean playWhenReady,
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason,
@PlaybackSuppressionReason int playbackSuppressionReason) {
pendingOperationAcks++;
// Position estimation and copy must occur before changing/masking playback state.
PlaybackInfo newPlaybackInfo =
@ -2791,21 +2760,15 @@ import java.util.concurrent.CopyOnWriteArraySet;
/* repeatCurrentMediaItem= */ false);
}
@PlaybackSuppressionReason
private int computePlaybackSuppressionReason(
boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) {
if (playerCommand == AudioFocusManager.PLAYER_COMMAND_WAIT_FOR_CALLBACK) {
return Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS;
private @PlaybackSuppressionReason int computePlaybackSuppressionReason(boolean playWhenReady) {
if (suitableOutputChecker != null
&& !suitableOutputChecker.isSelectedOutputSuitableForPlayback()) {
return Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT;
}
if (suitableOutputChecker != null) {
if (playWhenReady && !suitableOutputChecker.isSelectedOutputSuitableForPlayback()) {
return Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT;
}
if (!playWhenReady
&& playbackInfo.playbackSuppressionReason
== PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
return Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT;
}
if (playbackInfo.playbackSuppressionReason
== Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS
&& !playWhenReady) {
return Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS;
}
return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
}
@ -2925,16 +2888,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (isSelectedOutputSuitableForPlayback) {
if (playbackInfo.playbackSuppressionReason
== Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
playbackInfo.playWhenReady,
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
Player.PLAYBACK_SUPPRESSION_REASON_NONE);
updatePlayWhenReady(playbackInfo.playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
}
} else {
updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
playbackInfo.playWhenReady,
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
updatePlayWhenReady(playbackInfo.playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
}
}
@ -2953,12 +2910,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
.build();
}
private static int getPlayWhenReadyChangeReason(int playerCommand) {
return playerCommand == AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY
? PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS
: PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
}
private static final class MediaSourceHolderSnapshot implements MediaSourceInfoHolder {
private final Object uid;
@ -2995,7 +2946,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
SurfaceHolder.Callback,
TextureView.SurfaceTextureListener,
SphericalGLSurfaceView.VideoSurfaceListener,
AudioFocusManager.PlayerControl,
AudioBecomingNoisyManager.EventListener,
StreamVolumeManager.Listener,
AudioOffloadListener {
@ -3228,28 +3178,12 @@ import java.util.concurrent.CopyOnWriteArraySet;
setVideoOutputInternal(/* videoOutput= */ null);
}
// AudioFocusManager.PlayerControl implementation
@Override
public void setVolumeMultiplier(float volumeMultiplier) {
sendVolumeToInternalPlayer();
}
@Override
public void executePlayerCommand(@AudioFocusManager.PlayerCommand int playerCommand) {
boolean playWhenReady = getPlayWhenReady();
updatePlayWhenReady(
playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playerCommand));
}
// AudioBecomingNoisyManager.EventListener implementation.
@Override
public void onAudioBecomingNoisy() {
updatePlayWhenReady(
/* playWhenReady= */ false,
AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY,
Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY);
/* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY);
}
// StreamVolumeManager.Listener implementation.

View File

@ -27,6 +27,7 @@ import static androidx.media3.exoplayer.audio.AudioSink.OFFLOAD_MODE_DISABLED;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@ -87,7 +88,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
TrackSelector.InvalidationListener,
MediaSourceList.MediaSourceListInfoRefreshListener,
PlaybackParametersListener,
PlayerMessage.Sender {
PlayerMessage.Sender,
AudioFocusManager.PlayerControl {
private static final String TAG = "ExoPlayerImplInternal";
@ -164,6 +166,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
private static final int MSG_SET_VIDEO_OUTPUT = 30;
private static final int MSG_SET_AUDIO_ATTRIBUTES = 31;
private static final int MSG_SET_VOLUME = 32;
private static final int MSG_AUDIO_FOCUS_PLAYER_COMMAND = 33;
private static final int MSG_AUDIO_FOCUS_VOLUME_MULTIPLIER = 34;
private static final long BUFFERING_MAXIMUM_INTERVAL_MS =
Util.usToMs(Renderer.DEFAULT_DURATION_TO_PROGRESS_US);
@ -210,6 +214,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private final AnalyticsCollector analyticsCollector;
private final HandlerWrapper applicationLooperHandler;
private final boolean hasSecondaryRenderers;
private final AudioFocusManager audioFocusManager;
@SuppressWarnings("unused")
private SeekParameters seekParameters;
@ -240,8 +245,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
private Timeline lastPreloadPoolInvalidationTimeline;
private long prewarmingMediaPeriodDiscontinuity = C.TIME_UNSET;
private boolean isPrewarmingDisabledUntilNextTransition;
private float volume;
public ExoPlayerImplInternal(
Context context,
Renderer[] renderers,
Renderer[] secondaryRenderers,
TrackSelector trackSelector,
@ -279,6 +286,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
this.playerId = playerId;
this.preloadConfiguration = preloadConfiguration;
this.analyticsCollector = analyticsCollector;
this.volume = 1f;
playbackMaybeBecameStuckAtMs = C.TIME_UNSET;
lastRebufferRealtimeMs = C.TIME_UNSET;
@ -333,6 +341,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
(playbackLooperProvider == null) ? new PlaybackLooperProvider() : playbackLooperProvider;
this.playbackLooper = this.playbackLooperProvider.obtainLooper();
handler = clock.createHandler(this.playbackLooper, this);
audioFocusManager = new AudioFocusManager(context, playbackLooper, /* playerControl= */ this);
}
private MediaPeriodHolder createMediaPeriodHolder(
@ -453,14 +463,29 @@ import java.util.concurrent.atomic.AtomicBoolean;
.sendToTarget();
}
public void setAudioAttributes(AudioAttributes audioAttributes) {
handler.obtainMessage(MSG_SET_AUDIO_ATTRIBUTES, audioAttributes).sendToTarget();
public void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) {
handler
.obtainMessage(MSG_SET_AUDIO_ATTRIBUTES, handleAudioFocus ? 1 : 0, 0, audioAttributes)
.sendToTarget();
}
public void setVolume(float volume) {
handler.obtainMessage(MSG_SET_VOLUME, volume).sendToTarget();
}
private void handleAudioFocusPlayerCommandInternal(
@AudioFocusManager.PlayerCommand int playerCommand) throws ExoPlaybackException {
updatePlayWhenReadyWithAudioFocus(
playbackInfo.playWhenReady,
playerCommand,
playbackInfo.playbackSuppressionReason,
playbackInfo.playWhenReadyChangeReason);
}
private void handleAudioFocusVolumeMultiplierChange() throws ExoPlaybackException {
setVolumeInternal(volume);
}
@Override
public synchronized void sendMessage(PlayerMessage message) {
if (released || !playbackLooper.getThread().isAlive()) {
@ -579,6 +604,18 @@ import java.util.concurrent.atomic.AtomicBoolean;
.sendToTarget();
}
// AudioFocusManager.PlayerControl implementation
@Override
public void setVolumeMultiplier(float volumeMultiplier) {
handler.sendEmptyMessage(MSG_AUDIO_FOCUS_VOLUME_MULTIPLIER);
}
@Override
public void executePlayerCommand(@AudioFocusManager.PlayerCommand int playerCommand) {
handler.obtainMessage(MSG_AUDIO_FOCUS_PLAYER_COMMAND, playerCommand, 0).sendToTarget();
}
// Handler.Callback implementation.
@SuppressWarnings({"unchecked", "WrongConstant"}) // Casting message payload types and IntDef.
@ -679,11 +716,18 @@ import java.util.concurrent.atomic.AtomicBoolean;
updateMediaSourcesWithMediaItemsInternal(msg.arg1, msg.arg2, (List<MediaItem>) msg.obj);
break;
case MSG_SET_AUDIO_ATTRIBUTES:
setAudioAttributesInternal((AudioAttributes) msg.obj);
setAudioAttributesInternal(
(AudioAttributes) msg.obj, /* handleAudioFocus= */ msg.arg1 != 0);
break;
case MSG_SET_VOLUME:
setVolumeInternal((Float) msg.obj);
break;
case MSG_AUDIO_FOCUS_PLAYER_COMMAND:
handleAudioFocusPlayerCommandInternal(/* playerCommand= */ msg.arg1);
break;
case MSG_AUDIO_FOCUS_VOLUME_MULTIPLIER:
handleAudioFocusVolumeMultiplierChange();
break;
case MSG_RELEASE:
releaseInternal();
// Return immediately to not send playback info updates after release.
@ -872,7 +916,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
private void prepareInternal() {
private void prepareInternal() throws ExoPlaybackException {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
resetInternal(
/* resetRenderers= */ false,
@ -881,6 +925,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* resetError= */ true);
loadControl.onPrepared(playerId);
setState(playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING);
updatePlayWhenReadyWithAudioFocus();
mediaSourceList.prepare(bandwidthMeter.getTransferListener());
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
@ -953,13 +998,18 @@ import java.util.concurrent.atomic.AtomicBoolean;
handleMediaSourceListInfoRefreshed(timeline, /* isSourceRefresh= */ false);
}
private void setAudioAttributesInternal(AudioAttributes audioAttributes) {
private void setAudioAttributesInternal(AudioAttributes audioAttributes, boolean handleAudioFocus)
throws ExoPlaybackException {
trackSelector.setAudioAttributes(audioAttributes);
audioFocusManager.setAudioAttributes(handleAudioFocus ? audioAttributes : null);
updatePlayWhenReadyWithAudioFocus();
}
private void setVolumeInternal(float volume) throws ExoPlaybackException {
this.volume = volume;
float scaledVolume = volume * audioFocusManager.getVolumeMultiplier();
for (RendererHolder renderer : renderers) {
renderer.setVolume(volume);
renderer.setVolume(scaledVolume);
}
}
@ -982,8 +1032,47 @@ import java.util.concurrent.atomic.AtomicBoolean;
@Player.PlayWhenReadyChangeReason int reason)
throws ExoPlaybackException {
playbackInfoUpdate.incrementPendingOperationAcks(operationAck ? 1 : 0);
updatePlayWhenReadyWithAudioFocus(playWhenReady, playbackSuppressionReason, reason);
}
private void updatePlayWhenReadyWithAudioFocus() throws ExoPlaybackException {
updatePlayWhenReadyWithAudioFocus(
playbackInfo.playWhenReady,
playbackInfo.playbackSuppressionReason,
playbackInfo.playWhenReadyChangeReason);
}
private void updatePlayWhenReadyWithAudioFocus(
boolean playWhenReady,
@PlaybackSuppressionReason int playbackSuppressionReason,
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason)
throws ExoPlaybackException {
@AudioFocusManager.PlayerCommand
int playerCommand =
audioFocusManager.updateAudioFocus(playWhenReady, playbackInfo.playbackState);
updatePlayWhenReadyWithAudioFocus(
playWhenReady, playerCommand, playbackSuppressionReason, playWhenReadyChangeReason);
}
private void updatePlayWhenReadyWithAudioFocus(
boolean playWhenReady,
@AudioFocusManager.PlayerCommand int playerCommand,
@PlaybackSuppressionReason int playbackSuppressionReason,
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason)
throws ExoPlaybackException {
playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY;
playWhenReadyChangeReason =
updatePlayWhenReadyChangeReason(playerCommand, playWhenReadyChangeReason);
playbackSuppressionReason =
updatePlaybackSuppressionReason(playerCommand, playbackSuppressionReason);
if (playbackInfo.playWhenReady == playWhenReady
&& playbackInfo.playbackSuppressionReason == playbackSuppressionReason
&& playbackInfo.playWhenReadyChangeReason == playWhenReadyChangeReason) {
return;
}
playbackInfo =
playbackInfo.copyWithPlayWhenReady(playWhenReady, reason, playbackSuppressionReason);
playbackInfo.copyWithPlayWhenReady(
playWhenReady, playWhenReadyChangeReason, playbackSuppressionReason);
updateRebufferingState(/* isRebuffering= */ false, /* resetLastRebufferRealtimeMs= */ false);
notifyTrackSelectionPlayWhenReadyChanged(playWhenReady);
if (!shouldPlayWhenReady()) {
@ -1663,6 +1752,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* resetError= */ false);
playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeStop ? 1 : 0);
loadControl.onStopped(playerId);
audioFocusManager.updateAudioFocus(playbackInfo.playWhenReady, Player.STATE_IDLE);
setState(Player.STATE_IDLE);
}
@ -1675,6 +1765,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* resetError= */ false);
releaseRenderers();
loadControl.onReleased(playerId);
audioFocusManager.release();
trackSelector.release();
setState(Player.STATE_IDLE);
} finally {
@ -3658,6 +3749,31 @@ import java.util.concurrent.atomic.AtomicBoolean;
: newTimeline.getPeriod(newPeriodIndex, period).windowIndex;
}
private static @Player.PlayWhenReadyChangeReason int updatePlayWhenReadyChangeReason(
@AudioFocusManager.PlayerCommand int playerCommand,
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
if (playerCommand == AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY) {
return Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS;
}
if (playWhenReadyChangeReason == Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS) {
return Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
}
return playWhenReadyChangeReason;
}
private static @Player.PlaybackSuppressionReason int updatePlaybackSuppressionReason(
@AudioFocusManager.PlayerCommand int playerCommand,
@Player.PlaybackSuppressionReason int playbackSuppressionReason) {
if (playerCommand == AudioFocusManager.PLAYER_COMMAND_WAIT_FOR_CALLBACK) {
return Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS;
}
if (playbackSuppressionReason
== Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS) {
return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
}
return playbackSuppressionReason;
}
private static final class SeekPosition {
public final Timeline timeline;

View File

@ -25,7 +25,6 @@ import static org.robolectric.Shadows.shadowOf;
import android.content.Context;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Handler;
import android.os.Looper;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
@ -59,9 +58,7 @@ public class AudioFocusManagerTest {
testPlayerControl = new TestPlayerControl();
audioFocusManager =
new AudioFocusManager(
ApplicationProvider.getApplicationContext(),
new Handler(Looper.myLooper()),
testPlayerControl);
ApplicationProvider.getApplicationContext(), Looper.myLooper(), testPlayerControl);
}
@Test

View File

@ -205,7 +205,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Range;
import com.google.common.util.concurrent.AtomicDouble;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.util.ArrayList;
@ -4440,6 +4439,7 @@ public class ExoPlayerTest {
player.prepare();
player.play();
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReady = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason int suppressionReason = player.getPlaybackSuppressionReason();
player.release();
@ -4464,12 +4464,19 @@ public class ExoPlayerTest {
player.prepare();
player.play();
boolean playWhenReady = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason int suppressionReason = player.getPlaybackSuppressionReason();
boolean playWhenReadyInitial = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason
int suppressionReasonInitial = player.getPlaybackSuppressionReason();
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReadyFinal = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason
int suppressionReasonFinal = player.getPlaybackSuppressionReason();
player.release();
assertThat(playWhenReady).isFalse();
assertThat(suppressionReason).isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
assertThat(playWhenReadyInitial).isTrue();
assertThat(suppressionReasonInitial).isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
assertThat(playWhenReadyFinal).isFalse();
assertThat(suppressionReasonFinal).isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
verify(listener, never()).onPlaybackSuppressionReasonChanged(anyInt());
verify(listener)
.onPlayWhenReadyChanged(
@ -4486,12 +4493,10 @@ public class ExoPlayerTest {
player.addListener(listener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
player.play();
shadowOf(audioManager)
.getLastAudioFocusRequest()
.listener
.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS);
advance(player).untilPendingCommandsAreFullyHandled();
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_LOSS);
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReady = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason int suppressionReason = player.getPlaybackSuppressionReason();
@ -4522,19 +4527,14 @@ public class ExoPlayerTest {
player.addListener(listener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
player.play();
shadowOf(audioManager)
.getLastAudioFocusRequest()
.listener
.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
advance(player).untilPendingCommandsAreFullyHandled();
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReady = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason int suppressionReason = player.getPlaybackSuppressionReason();
shadowOf(audioManager)
.getLastAudioFocusRequest()
.listener
.onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN);
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_GAIN);
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReadyAfterGain = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason
@ -4574,20 +4574,26 @@ public class ExoPlayerTest {
player.addListener(listener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
player.play();
shadowOf(audioManager)
.getLastAudioFocusRequest()
.listener
.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
advance(player).untilPendingCommandsAreFullyHandled();
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
advance(player).untilPendingCommandsAreFullyHandled();
player.pause();
boolean playWhenReady = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason int suppressionReason = player.getPlaybackSuppressionReason();
boolean playWhenReadyInitial = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason
int suppressionReasonInitial = player.getPlaybackSuppressionReason();
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReadyFinal = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason
int suppressionReasonFinal = player.getPlaybackSuppressionReason();
player.release();
assertThat(playWhenReady).isFalse();
assertThat(suppressionReason)
assertThat(playWhenReadyInitial).isFalse();
assertThat(suppressionReasonInitial)
.isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS);
assertThat(playWhenReadyFinal).isFalse();
assertThat(suppressionReasonFinal)
.isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS);
InOrder inOrder = inOrder(listener);
inOrder
@ -4613,7 +4619,53 @@ public class ExoPlayerTest {
AudioManager audioManager = context.getSystemService(AudioManager.class);
shadowOf(audioManager).setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
Listener listener = mock(Player.Listener.class);
AtomicDouble lastAudioVolume = new AtomicDouble(1.0);
AtomicReference<Float> lastAudioVolume = new AtomicReference<>(1.0f);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setRenderers(
new FakeRenderer(C.TRACK_TYPE_AUDIO) {
@Override
public void handleMessage(
@MessageType int messageType, @Nullable Object message) {
if (messageType == Renderer.MSG_SET_VOLUME) {
lastAudioVolume.set((Float) message);
}
}
})
.build();
player.setVolume(0.9f);
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
player.addListener(listener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
player.play();
advance(player).untilPendingCommandsAreFullyHandled();
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReady = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason int suppressionReason = player.getPlaybackSuppressionReason();
player.release();
assertThat(playWhenReady).isTrue();
assertThat(suppressionReason).isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
verify(listener, never()).onPlaybackSuppressionReasonChanged(anyInt());
verify(listener)
.onPlayWhenReadyChanged(
/* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
verify(listener, never())
.onPlayWhenReadyChanged(
/* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS);
assertThat(lastAudioVolume.get()).isLessThan(0.9f);
}
@Test
public void audioFocus_transientLossDuckAndGainWhilePlaying_restoresOriginalVolume()
throws Exception {
AudioManager audioManager = context.getSystemService(AudioManager.class);
shadowOf(audioManager).setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
Listener listener = mock(Player.Listener.class);
AtomicReference<Float> lastAudioVolume = new AtomicReference<>(1.0f);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setRenderers(
@ -4629,21 +4681,18 @@ public class ExoPlayerTest {
.build();
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
player.addListener(listener);
player.setVolume(0.9f);
player.setMediaSource(new FakeMediaSource());
player.prepare();
player.play();
shadowOf(audioManager)
.getLastAudioFocusRequest()
.listener
.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReady = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason int suppressionReason = player.getPlaybackSuppressionReason();
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
advance(player).untilPendingCommandsAreFullyHandled();
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_GAIN);
advance(player).untilPendingCommandsAreFullyHandled();
player.release();
assertThat(playWhenReady).isTrue();
assertThat(suppressionReason).isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
verify(listener, never()).onPlaybackSuppressionReasonChanged(anyInt());
verify(listener)
.onPlayWhenReadyChanged(
@ -4651,7 +4700,7 @@ public class ExoPlayerTest {
verify(listener, never())
.onPlayWhenReadyChanged(
/* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS);
assertThat(lastAudioVolume.get()).isLessThan(1.0);
assertThat(lastAudioVolume.get()).isEqualTo(0.9f);
}
@Test
@ -4664,13 +4713,11 @@ public class ExoPlayerTest {
player.addListener(listener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
player.play();
advance(player).untilPendingCommandsAreFullyHandled();
player.pause();
shadowOf(audioManager)
.getLastAudioFocusRequest()
.listener
.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS);
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_LOSS);
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReady = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason int suppressionReason = player.getPlaybackSuppressionReason();
@ -4704,20 +4751,15 @@ public class ExoPlayerTest {
player.addListener(listener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
player.play();
advance(player).untilPendingCommandsAreFullyHandled();
player.pause();
shadowOf(audioManager)
.getLastAudioFocusRequest()
.listener
.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReady = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason int suppressionReason = player.getPlaybackSuppressionReason();
shadowOf(audioManager)
.getLastAudioFocusRequest()
.listener
.onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN);
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_GAIN);
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReadyAfterGain = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason
@ -4760,21 +4802,26 @@ public class ExoPlayerTest {
player.addListener(listener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
player.play();
advance(player).untilPendingCommandsAreFullyHandled();
player.pause();
shadowOf(audioManager)
.getLastAudioFocusRequest()
.listener
.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
advance(player).untilPendingCommandsAreFullyHandled();
player.play();
boolean playWhenReady = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason int suppressionReason = player.getPlaybackSuppressionReason();
boolean playWhenReadyInitial = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason
int suppressionReasonInitial = player.getPlaybackSuppressionReason();
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReadyFinal = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason
int suppressionReasonFinal = player.getPlaybackSuppressionReason();
player.release();
assertThat(playWhenReady).isTrue();
assertThat(suppressionReason).isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
assertThat(playWhenReadyInitial).isTrue();
assertThat(suppressionReasonInitial).isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
assertThat(playWhenReadyFinal).isTrue();
assertThat(suppressionReasonFinal).isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
@ -4800,12 +4847,58 @@ public class ExoPlayerTest {
/* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS);
}
@Test
public void audioFocus_playDuringTransientLossWhilePlaying_continuesPlayback() throws Exception {
AudioManager audioManager = context.getSystemService(AudioManager.class);
shadowOf(audioManager).setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
Listener listener = mock(Player.Listener.class);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
player.addListener(listener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
player.play();
advance(player).untilPendingCommandsAreFullyHandled();
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
advance(player).untilPendingCommandsAreFullyHandled();
player.play();
boolean playWhenReadyInitial = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason
int suppressionReasonInitial = player.getPlaybackSuppressionReason();
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReadyFinal = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason
int suppressionReasonFinal = player.getPlaybackSuppressionReason();
player.release();
assertThat(playWhenReadyInitial).isTrue();
assertThat(suppressionReasonInitial).isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
assertThat(playWhenReadyFinal).isTrue();
assertThat(suppressionReasonFinal).isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onPlayWhenReadyChanged(
/* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
inOrder
.verify(listener)
.onPlaybackSuppressionReasonChanged(
Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS);
inOrder
.verify(listener)
.onPlaybackSuppressionReasonChanged(Player.PLAYBACK_SUPPRESSION_REASON_NONE);
verify(listener, never())
.onPlayWhenReadyChanged(
/* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS);
}
@Test
public void audioFocus_transientLossDuckWhilePaused_lowersVolume() throws Exception {
AudioManager audioManager = context.getSystemService(AudioManager.class);
shadowOf(audioManager).setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
Listener listener = mock(Player.Listener.class);
AtomicDouble lastAudioVolume = new AtomicDouble(1.0);
AtomicReference<Float> lastAudioVolume = new AtomicReference<>(1.0f);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setRenderers(
@ -4819,17 +4912,16 @@ public class ExoPlayerTest {
}
})
.build();
player.setVolume(0.9f);
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
player.addListener(listener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
player.play();
advance(player).untilPendingCommandsAreFullyHandled();
player.pause();
shadowOf(audioManager)
.getLastAudioFocusRequest()
.listener
.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
advance(player).untilPendingCommandsAreFullyHandled();
boolean playWhenReady = player.getPlayWhenReady();
@Player.PlaybackSuppressionReason int suppressionReason = player.getPlaybackSuppressionReason();
@ -4850,7 +4942,58 @@ public class ExoPlayerTest {
verify(listener, never())
.onPlayWhenReadyChanged(
/* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS);
assertThat(lastAudioVolume.get()).isLessThan(1.0);
assertThat(lastAudioVolume.get()).isLessThan(0.9f);
}
@Test
public void audioFocus_transientLossDuckAndGainWhilePaused_restoresOriginalVolume()
throws Exception {
AudioManager audioManager = context.getSystemService(AudioManager.class);
shadowOf(audioManager).setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
Listener listener = mock(Player.Listener.class);
AtomicReference<Float> lastAudioVolume = new AtomicReference<>(1.0f);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setRenderers(
new FakeRenderer(C.TRACK_TYPE_AUDIO) {
@Override
public void handleMessage(
@MessageType int messageType, @Nullable Object message) {
if (messageType == Renderer.MSG_SET_VOLUME) {
lastAudioVolume.set((Float) message);
}
}
})
.build();
player.setVolume(0.9f);
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
player.addListener(listener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
player.play();
advance(player).untilPendingCommandsAreFullyHandled();
player.pause();
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
advance(player).untilPendingCommandsAreFullyHandled();
triggerAudioFocusChangeListener(player, AudioManager.AUDIOFOCUS_GAIN);
advance(player).untilPendingCommandsAreFullyHandled();
player.release();
verify(listener, never()).onPlaybackSuppressionReasonChanged(anyInt());
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onPlayWhenReadyChanged(
/* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
inOrder
.verify(listener)
.onPlayWhenReadyChanged(
/* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
verify(listener, never())
.onPlayWhenReadyChanged(
/* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS);
assertThat(lastAudioVolume.get()).isEqualTo(0.9f);
}
@Test
@ -16591,6 +16734,17 @@ public class ExoPlayerTest {
filteredAudioDeviceInfo, /* notifyAudioDeviceCallbacks= */ true));
}
private void triggerAudioFocusChangeListener(ExoPlayer player, int focusChange) {
AudioManager audioManager = context.getSystemService(AudioManager.class);
new Handler(player.getPlaybackLooper())
.post(
() ->
shadowOf(audioManager)
.getLastAudioFocusRequest()
.listener
.onAudioFocusChange(focusChange));
}
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
final Surface surface1 = new Surface(new SurfaceTexture(/* texName= */ 0));
final Surface surface2 = new Surface(new SurfaceTexture(/* texName= */ 1));