mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
9052313245
commit
fc4112beee
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
Loading…
x
Reference in New Issue
Block a user