Commit e784d2c5 by tonihei Committed by Oliver Woodman

Add Player.isPlaying and Player.getPlaybackSuppressionReason

The player may suppress playback when waiting for audio focus even if the
state==Player.READY. There is currently no getter or callback to obtain this
piece of information for UI updates or analytics.

Also, it's a important derived state to know whether the playback position is
advancing. Add isPlaying and the corresponding callback to allow retrieving
this information more easily.

Issue:#6203
PiperOrigin-RevId: 268921721
parent d443be2a
...@@ -72,6 +72,11 @@ ...@@ -72,6 +72,11 @@
* Fix decoder selection for E-AC3 JOC streams * Fix decoder selection for E-AC3 JOC streams
([#6398](https://github.com/google/ExoPlayer/issues/6398)). ([#6398](https://github.com/google/ExoPlayer/issues/6398)).
* Fix Dolby Vision fallback to AVC and HEVC. * Fix Dolby Vision fallback to AVC and HEVC.
* Add `Player.isPlaying` and `EventListener.onIsPlayingChanged` to check whether
the playback position is advancing. This helps to determine if playback is
suppressed due to audio focus loss. Also add
`Player.getPlaybackSuppressedReason` to determine the reason of the
suppression ([#6203](https://github.com/google/ExoPlayer/issues/6203)).
### 2.10.4 ### ### 2.10.4 ###
......
...@@ -323,12 +323,18 @@ public final class CastPlayer extends BasePlayer { ...@@ -323,12 +323,18 @@ public final class CastPlayer extends BasePlayer {
} }
@Override @Override
@Player.State @State
public int getPlaybackState() { public int getPlaybackState() {
return playbackState; return playbackState;
} }
@Override @Override
@PlaybackSuppressionReason
public int getPlaybackSuppressionReason() {
return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
}
@Override
@Nullable @Nullable
public ExoPlaybackException getPlaybackError() { public ExoPlaybackException getPlaybackError() {
return null; return null;
...@@ -538,6 +544,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -538,6 +544,7 @@ public final class CastPlayer extends BasePlayer {
return; return;
} }
boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady;
int playbackState = fetchPlaybackState(remoteMediaClient); int playbackState = fetchPlaybackState(remoteMediaClient);
boolean playWhenReady = !remoteMediaClient.isPaused(); boolean playWhenReady = !remoteMediaClient.isPaused();
if (this.playbackState != playbackState if (this.playbackState != playbackState
...@@ -548,6 +555,11 @@ public final class CastPlayer extends BasePlayer { ...@@ -548,6 +555,11 @@ public final class CastPlayer extends BasePlayer {
new ListenerNotificationTask( new ListenerNotificationTask(
listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState))); listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState)));
} }
boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady;
if (wasPlaying != isPlaying) {
notificationsBatch.add(
new ListenerNotificationTask(listener -> listener.onIsPlayingChanged(isPlaying)));
}
@RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient); @RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient);
if (this.repeatMode != repeatMode) { if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode; this.repeatMode = repeatMode;
......
...@@ -28,6 +28,13 @@ public abstract class BasePlayer implements Player { ...@@ -28,6 +28,13 @@ public abstract class BasePlayer implements Player {
} }
@Override @Override
public final boolean isPlaying() {
return getPlaybackState() == Player.STATE_READY
&& getPlayWhenReady()
&& getPlaybackSuppressionReason() == PLAYBACK_SUPPRESSION_REASON_NONE;
}
@Override
public final void seekToDefaultPosition() { public final void seekToDefaultPosition() {
seekToDefaultPosition(getCurrentWindowIndex()); seekToDefaultPosition(getCurrentWindowIndex());
} }
......
...@@ -64,8 +64,8 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -64,8 +64,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
private MediaSource mediaSource; private MediaSource mediaSource;
private boolean playWhenReady; private boolean playWhenReady;
private boolean internalPlayWhenReady; @PlaybackSuppressionReason private int playbackSuppressionReason;
private @RepeatMode int repeatMode; @RepeatMode private int repeatMode;
private boolean shuffleModeEnabled; private boolean shuffleModeEnabled;
private int pendingOperationAcks; private int pendingOperationAcks;
private boolean hasPendingPrepare; private boolean hasPendingPrepare;
...@@ -119,6 +119,7 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -119,6 +119,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
period = new Timeline.Period(); period = new Timeline.Period();
playbackParameters = PlaybackParameters.DEFAULT; playbackParameters = PlaybackParameters.DEFAULT;
seekParameters = SeekParameters.DEFAULT; seekParameters = SeekParameters.DEFAULT;
playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE;
eventHandler = eventHandler =
new Handler(looper) { new Handler(looper) {
@Override @Override
...@@ -193,12 +194,18 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -193,12 +194,18 @@ import java.util.concurrent.CopyOnWriteArrayList;
} }
@Override @Override
@Player.State @State
public int getPlaybackState() { public int getPlaybackState() {
return playbackInfo.playbackState; return playbackInfo.playbackState;
} }
@Override @Override
@PlaybackSuppressionReason
public int getPlaybackSuppressionReason() {
return playbackSuppressionReason;
}
@Override
@Nullable @Nullable
public ExoPlaybackException getPlaybackError() { public ExoPlaybackException getPlaybackError() {
return playbackInfo.playbackError; return playbackInfo.playbackError;
...@@ -242,19 +249,35 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -242,19 +249,35 @@ import java.util.concurrent.CopyOnWriteArrayList;
@Override @Override
public void setPlayWhenReady(boolean playWhenReady) { public void setPlayWhenReady(boolean playWhenReady) {
setPlayWhenReady(playWhenReady, /* suppressPlayback= */ false); setPlayWhenReady(playWhenReady, PLAYBACK_SUPPRESSION_REASON_NONE);
} }
public void setPlayWhenReady(boolean playWhenReady, boolean suppressPlayback) { public void setPlayWhenReady(
boolean internalPlayWhenReady = playWhenReady && !suppressPlayback; boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) {
if (this.internalPlayWhenReady != internalPlayWhenReady) { boolean oldIsPlaying = isPlaying();
this.internalPlayWhenReady = internalPlayWhenReady; boolean oldInternalPlayWhenReady =
this.playWhenReady && this.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE;
boolean internalPlayWhenReady =
playWhenReady && playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE;
if (oldInternalPlayWhenReady != internalPlayWhenReady) {
internalPlayer.setPlayWhenReady(internalPlayWhenReady); internalPlayer.setPlayWhenReady(internalPlayWhenReady);
} }
if (this.playWhenReady != playWhenReady) { boolean playWhenReadyChanged = this.playWhenReady != playWhenReady;
this.playWhenReady = playWhenReady; this.playWhenReady = playWhenReady;
this.playbackSuppressionReason = playbackSuppressionReason;
boolean isPlaying = isPlaying();
boolean isPlayingChanged = oldIsPlaying != isPlaying;
if (playWhenReadyChanged || isPlayingChanged) {
int playbackState = playbackInfo.playbackState; int playbackState = playbackInfo.playbackState;
notifyListeners(listener -> listener.onPlayerStateChanged(playWhenReady, playbackState)); notifyListeners(
listener -> {
if (playWhenReadyChanged) {
listener.onPlayerStateChanged(playWhenReady, playbackState);
}
if (isPlayingChanged) {
listener.onIsPlayingChanged(isPlaying);
}
});
} }
} }
...@@ -674,9 +697,11 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -674,9 +697,11 @@ import java.util.concurrent.CopyOnWriteArrayList;
@Player.DiscontinuityReason int positionDiscontinuityReason, @Player.DiscontinuityReason int positionDiscontinuityReason,
@Player.TimelineChangeReason int timelineChangeReason, @Player.TimelineChangeReason int timelineChangeReason,
boolean seekProcessed) { boolean seekProcessed) {
boolean previousIsPlaying = isPlaying();
// Assign playback info immediately such that all getters return the right values. // Assign playback info immediately such that all getters return the right values.
PlaybackInfo previousPlaybackInfo = this.playbackInfo; PlaybackInfo previousPlaybackInfo = this.playbackInfo;
this.playbackInfo = playbackInfo; this.playbackInfo = playbackInfo;
boolean isPlaying = isPlaying();
notifyListeners( notifyListeners(
new PlaybackInfoUpdate( new PlaybackInfoUpdate(
playbackInfo, playbackInfo,
...@@ -687,7 +712,8 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -687,7 +712,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
positionDiscontinuityReason, positionDiscontinuityReason,
timelineChangeReason, timelineChangeReason,
seekProcessed, seekProcessed,
playWhenReady)); playWhenReady,
/* isPlayingChanged= */ previousIsPlaying != isPlaying));
} }
private void notifyListeners(ListenerInvocation listenerInvocation) { private void notifyListeners(ListenerInvocation listenerInvocation) {
...@@ -733,6 +759,7 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -733,6 +759,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
private final boolean isLoadingChanged; private final boolean isLoadingChanged;
private final boolean trackSelectorResultChanged; private final boolean trackSelectorResultChanged;
private final boolean playWhenReady; private final boolean playWhenReady;
private final boolean isPlayingChanged;
public PlaybackInfoUpdate( public PlaybackInfoUpdate(
PlaybackInfo playbackInfo, PlaybackInfo playbackInfo,
...@@ -740,10 +767,11 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -740,10 +767,11 @@ import java.util.concurrent.CopyOnWriteArrayList;
CopyOnWriteArrayList<ListenerHolder> listeners, CopyOnWriteArrayList<ListenerHolder> listeners,
TrackSelector trackSelector, TrackSelector trackSelector,
boolean positionDiscontinuity, boolean positionDiscontinuity,
@Player.DiscontinuityReason int positionDiscontinuityReason, @DiscontinuityReason int positionDiscontinuityReason,
@Player.TimelineChangeReason int timelineChangeReason, @TimelineChangeReason int timelineChangeReason,
boolean seekProcessed, boolean seekProcessed,
boolean playWhenReady) { boolean playWhenReady,
boolean isPlayingChanged) {
this.playbackInfo = playbackInfo; this.playbackInfo = playbackInfo;
this.listenerSnapshot = new CopyOnWriteArrayList<>(listeners); this.listenerSnapshot = new CopyOnWriteArrayList<>(listeners);
this.trackSelector = trackSelector; this.trackSelector = trackSelector;
...@@ -752,6 +780,7 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -752,6 +780,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
this.timelineChangeReason = timelineChangeReason; this.timelineChangeReason = timelineChangeReason;
this.seekProcessed = seekProcessed; this.seekProcessed = seekProcessed;
this.playWhenReady = playWhenReady; this.playWhenReady = playWhenReady;
this.isPlayingChanged = isPlayingChanged;
playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState; playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState;
playbackErrorChanged = playbackErrorChanged =
previousPlaybackInfo.playbackError != playbackInfo.playbackError previousPlaybackInfo.playbackError != playbackInfo.playbackError
...@@ -793,6 +822,12 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -793,6 +822,12 @@ import java.util.concurrent.CopyOnWriteArrayList;
listenerSnapshot, listenerSnapshot,
listener -> listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState)); listener -> listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState));
} }
if (isPlayingChanged) {
invokeAll(
listenerSnapshot,
listener ->
listener.onIsPlayingChanged(playbackInfo.playbackState == Player.STATE_READY));
}
if (seekProcessed) { if (seekProcessed) {
invokeAll(listenerSnapshot, EventListener::onSeekProcessed); invokeAll(listenerSnapshot, EventListener::onSeekProcessed);
} }
......
...@@ -393,6 +393,13 @@ public interface Player { ...@@ -393,6 +393,13 @@ public interface Player {
default void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {} default void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {}
/** /**
* Called when the value of {@link #isPlaying()} changes.
*
* @param isPlaying Whether the player is playing.
*/
default void onIsPlayingChanged(boolean isPlaying) {}
/**
* Called when the value of {@link #getRepeatMode()} changes. * Called when the value of {@link #getRepeatMode()} changes.
* *
* @param repeatMode The {@link RepeatMode} used for playback. * @param repeatMode The {@link RepeatMode} used for playback.
...@@ -510,6 +517,20 @@ public interface Player { ...@@ -510,6 +517,20 @@ public interface Player {
int STATE_ENDED = 4; int STATE_ENDED = 4;
/** /**
* Reason why playback is suppressed even if {@link #getPlaybackState()} is {@link #STATE_READY}
* and {@link #getPlayWhenReady()} is {@code true}. One of {@link
* #PLAYBACK_SUPPRESSION_REASON_NONE} or {@link #PLAYBACK_SUPPRESSION_REASON_AUDIO_FOCUS_LOSS}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({PLAYBACK_SUPPRESSION_REASON_NONE, PLAYBACK_SUPPRESSION_REASON_AUDIO_FOCUS_LOSS})
@interface PlaybackSuppressionReason {}
/** Playback is not suppressed. */
int PLAYBACK_SUPPRESSION_REASON_NONE = 0;
/** Playback is suppressed because audio focus is lost or can't be acquired. */
int PLAYBACK_SUPPRESSION_REASON_AUDIO_FOCUS_LOSS = 1;
/**
* Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link * Repeat modes for playback. One of {@link #REPEAT_MODE_OFF}, {@link #REPEAT_MODE_ONE} or {@link
* #REPEAT_MODE_ALL}. * #REPEAT_MODE_ALL}.
*/ */
...@@ -636,11 +657,41 @@ public interface Player { ...@@ -636,11 +657,41 @@ public interface Player {
int getPlaybackState(); int getPlaybackState();
/** /**
* Returns reason why playback is suppressed even if {@link #getPlaybackState()} is {@link
* #STATE_READY} and {@link #getPlayWhenReady()} is {@code true}.
*
* <p>Note that {@link #PLAYBACK_SUPPRESSION_REASON_NONE} indicates that playback is not
* suppressed.
*
* @return The current {@link PlaybackSuppressionReason}.
*/
@PlaybackSuppressionReason
int getPlaybackSuppressionReason();
/**
* Returns whether the player is playing, i.e. {@link #getContentPosition()} is advancing.
*
* <p>If {@code false}, then at least one of the following is true:
*
* <ul>
* <li>The {@link #getPlaybackState() playback state} is not {@link #STATE_READY ready}.
* <li>There is no {@link #getPlayWhenReady() intention to play}.
* <li>Playback is {@link #getPlaybackSuppressionReason() suppressed for other reasons}.
* </ul>
*
* @return Whether the player is playing.
*/
boolean isPlaying();
/**
* Returns the error that caused playback to fail. This is the same error that will have been * Returns the error that caused playback to fail. This is the same error that will have been
* reported via {@link Player.EventListener#onPlayerError(ExoPlaybackException)} at the time of * reported via {@link Player.EventListener#onPlayerError(ExoPlaybackException)} at the time of
* failure. It can be queried using this method until {@code stop(true)} is called or the player * failure. It can be queried using this method until {@code stop(true)} is called or the player
* is re-prepared. * is re-prepared.
* *
* <p>Note that this method will always return {@code null} if {@link #getPlaybackState()} is not
* {@link #STATE_IDLE}.
*
* @return The error, or {@code null}. * @return The error, or {@code null}.
*/ */
@Nullable @Nullable
......
...@@ -1078,13 +1078,20 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1078,13 +1078,20 @@ public class SimpleExoPlayer extends BasePlayer
} }
@Override @Override
@Player.State @State
public int getPlaybackState() { public int getPlaybackState() {
verifyApplicationThread(); verifyApplicationThread();
return player.getPlaybackState(); return player.getPlaybackState();
} }
@Override @Override
@PlaybackSuppressionReason
public int getPlaybackSuppressionReason() {
verifyApplicationThread();
return player.getPlaybackSuppressionReason();
}
@Override
@Nullable @Nullable
public ExoPlaybackException getPlaybackError() { public ExoPlaybackException getPlaybackError() {
verifyApplicationThread(); verifyApplicationThread();
...@@ -1407,9 +1414,13 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1407,9 +1414,13 @@ public class SimpleExoPlayer extends BasePlayer
private void updatePlayWhenReady( private void updatePlayWhenReady(
boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) { boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) {
int playbackSuppressionReason =
playerCommand == AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY
? Player.PLAYBACK_SUPPRESSION_REASON_NONE
: Player.PLAYBACK_SUPPRESSION_REASON_AUDIO_FOCUS_LOSS;
player.setPlayWhenReady( player.setPlayWhenReady(
playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY, playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY,
playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY); playbackSuppressionReason);
} }
private void verifyApplicationThread() { private void verifyApplicationThread() {
......
...@@ -75,12 +75,18 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { ...@@ -75,12 +75,18 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
} }
@Override @Override
@Player.State @State
public int getPlaybackState() { public int getPlaybackState() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override @Override
@PlaybackSuppressionReason
public int getPlaybackSuppressionReason() {
throw new UnsupportedOperationException();
}
@Override
public ExoPlaybackException getPlaybackError() { public ExoPlaybackException getPlaybackError() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment