From f8d81d05a48e7e445d2ca4cd42af30c733661304 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 13 Sep 2019 17:38:51 +0100 Subject: [PATCH] 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 --- RELEASENOTES.md | 5 ++ .../exoplayer2/ext/cast/CastPlayer.java | 12 ++++ .../google/android/exoplayer2/BasePlayer.java | 7 ++ .../android/exoplayer2/ExoPlayerImpl.java | 65 ++++++++++++++----- .../com/google/android/exoplayer2/Player.java | 51 +++++++++++++++ .../android/exoplayer2/SimpleExoPlayer.java | 15 ++++- .../exoplayer2/testutil/StubExoPlayer.java | 6 ++ 7 files changed, 144 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7a452140a2..947f6ebd3c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,11 @@ ### 2.10.5 ### +* 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)). * Track selection * Add `allowAudioMixedChannelCountAdaptiveness` parameter to `DefaultTrackSelector` to allow adaptive selections of audio tracks with diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index bc0987322b..23d625b9ee 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -327,6 +327,12 @@ public final class CastPlayer extends BasePlayer { return playbackState; } + @Override + @PlaybackSuppressionReason + public int getPlaybackSuppressionReason() { + return Player.PLAYBACK_SUPPRESSION_REASON_NONE; + } + @Override @Nullable public ExoPlaybackException getPlaybackError() { @@ -542,6 +548,7 @@ public final class CastPlayer extends BasePlayer { return; } + boolean wasPlaying = playbackState == Player.STATE_READY && playWhenReady; int playbackState = fetchPlaybackState(remoteMediaClient); boolean playWhenReady = !remoteMediaClient.isPaused(); if (this.playbackState != playbackState @@ -552,6 +559,11 @@ public final class CastPlayer extends BasePlayer { new ListenerNotificationTask( 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); if (this.repeatMode != repeatMode) { this.repeatMode = repeatMode; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java index 774f1b452c..7ccfb57093 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java @@ -27,6 +27,13 @@ public abstract class BasePlayer implements Player { window = new Timeline.Window(); } + @Override + public final boolean isPlaying() { + return getPlaybackState() == Player.STATE_READY + && getPlayWhenReady() + && getPlaybackSuppressionReason() == PLAYBACK_SUPPRESSION_REASON_NONE; + } + @Override public final void seekToDefaultPosition() { seekToDefaultPosition(getCurrentWindowIndex()); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index a10416fac8..5010faaa9d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -64,8 +64,8 @@ import java.util.concurrent.CopyOnWriteArrayList; private MediaSource mediaSource; private boolean playWhenReady; - private boolean internalPlayWhenReady; - private @RepeatMode int repeatMode; + @PlaybackSuppressionReason private int playbackSuppressionReason; + @RepeatMode private int repeatMode; private boolean shuffleModeEnabled; private int pendingOperationAcks; private boolean hasPendingPrepare; @@ -119,6 +119,7 @@ import java.util.concurrent.CopyOnWriteArrayList; period = new Timeline.Period(); playbackParameters = PlaybackParameters.DEFAULT; seekParameters = SeekParameters.DEFAULT; + playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE; eventHandler = new Handler(looper) { @Override @@ -197,8 +198,14 @@ import java.util.concurrent.CopyOnWriteArrayList; return playbackInfo.playbackState; } + @PlaybackSuppressionReason + public int getPlaybackSuppressionReason() { + return playbackSuppressionReason; + } + @Override - public @Nullable ExoPlaybackException getPlaybackError() { + @Nullable + public ExoPlaybackException getPlaybackError() { return playbackError; } @@ -239,19 +246,35 @@ import java.util.concurrent.CopyOnWriteArrayList; @Override public void setPlayWhenReady(boolean playWhenReady) { - setPlayWhenReady(playWhenReady, /* suppressPlayback= */ false); + setPlayWhenReady(playWhenReady, PLAYBACK_SUPPRESSION_REASON_NONE); } - public void setPlayWhenReady(boolean playWhenReady, boolean suppressPlayback) { - boolean internalPlayWhenReady = playWhenReady && !suppressPlayback; - if (this.internalPlayWhenReady != internalPlayWhenReady) { - this.internalPlayWhenReady = internalPlayWhenReady; + public void setPlayWhenReady( + boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) { + boolean oldIsPlaying = isPlaying(); + boolean oldInternalPlayWhenReady = + this.playWhenReady && this.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; + boolean internalPlayWhenReady = + playWhenReady && playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; + if (oldInternalPlayWhenReady != internalPlayWhenReady) { internalPlayer.setPlayWhenReady(internalPlayWhenReady); } - if (this.playWhenReady != playWhenReady) { - this.playWhenReady = playWhenReady; + boolean playWhenReadyChanged = this.playWhenReady != playWhenReady; + this.playWhenReady = playWhenReady; + this.playbackSuppressionReason = playbackSuppressionReason; + boolean isPlaying = isPlaying(); + boolean isPlayingChanged = oldIsPlaying != isPlaying; + if (playWhenReadyChanged || isPlayingChanged) { int playbackState = playbackInfo.playbackState; - notifyListeners(listener -> listener.onPlayerStateChanged(playWhenReady, playbackState)); + notifyListeners( + listener -> { + if (playWhenReadyChanged) { + listener.onPlayerStateChanged(playWhenReady, playbackState); + } + if (isPlayingChanged) { + listener.onIsPlayingChanged(isPlaying); + } + }); } } @@ -697,9 +720,11 @@ import java.util.concurrent.CopyOnWriteArrayList; @Player.DiscontinuityReason int positionDiscontinuityReason, @Player.TimelineChangeReason int timelineChangeReason, boolean seekProcessed) { + boolean previousIsPlaying = isPlaying(); // Assign playback info immediately such that all getters return the right values. PlaybackInfo previousPlaybackInfo = this.playbackInfo; this.playbackInfo = playbackInfo; + boolean isPlaying = isPlaying(); notifyListeners( new PlaybackInfoUpdate( playbackInfo, @@ -710,7 +735,8 @@ import java.util.concurrent.CopyOnWriteArrayList; positionDiscontinuityReason, timelineChangeReason, seekProcessed, - playWhenReady)); + playWhenReady, + /* isPlayingChanged= */ previousIsPlaying != isPlaying)); } private void notifyListeners(ListenerInvocation listenerInvocation) { @@ -755,6 +781,7 @@ import java.util.concurrent.CopyOnWriteArrayList; private final boolean isLoadingChanged; private final boolean trackSelectorResultChanged; private final boolean playWhenReady; + private final boolean isPlayingChanged; public PlaybackInfoUpdate( PlaybackInfo playbackInfo, @@ -762,10 +789,11 @@ import java.util.concurrent.CopyOnWriteArrayList; CopyOnWriteArrayList listeners, TrackSelector trackSelector, boolean positionDiscontinuity, - @Player.DiscontinuityReason int positionDiscontinuityReason, - @Player.TimelineChangeReason int timelineChangeReason, + @DiscontinuityReason int positionDiscontinuityReason, + @TimelineChangeReason int timelineChangeReason, boolean seekProcessed, - boolean playWhenReady) { + boolean playWhenReady, + boolean isPlayingChanged) { this.playbackInfo = playbackInfo; this.listenerSnapshot = new CopyOnWriteArrayList<>(listeners); this.trackSelector = trackSelector; @@ -774,6 +802,7 @@ import java.util.concurrent.CopyOnWriteArrayList; this.timelineChangeReason = timelineChangeReason; this.seekProcessed = seekProcessed; this.playWhenReady = playWhenReady; + this.isPlayingChanged = isPlayingChanged; playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState; timelineOrManifestChanged = previousPlaybackInfo.timeline != playbackInfo.timeline @@ -813,6 +842,12 @@ import java.util.concurrent.CopyOnWriteArrayList; listenerSnapshot, listener -> listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState)); } + if (isPlayingChanged) { + invokeAll( + listenerSnapshot, + listener -> + listener.onIsPlayingChanged(playbackInfo.playbackState == Player.STATE_READY)); + } if (seekProcessed) { invokeAll(listenerSnapshot, EventListener::onSeekProcessed); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 8885be2e02..dad0945d7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -365,6 +365,13 @@ public interface Player { */ default void onPlayerStateChanged(boolean playWhenReady, 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. * @@ -462,6 +469,20 @@ public interface Player { */ 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_MODE_ALL}. @@ -587,12 +608,42 @@ public interface Player { */ int getPlaybackState(); + /** + * Returns reason why playback is suppressed even if {@link #getPlaybackState()} is {@link + * #STATE_READY} and {@link #getPlayWhenReady()} is {@code true}. + * + *

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. + * + *

If {@code false}, then at least one of the following is true: + * + *

+ * + * @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 * 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 * is re-prepared. * + *

Note that this method will always return {@code null} if {@link #getPlaybackState()} is not + * {@link #STATE_IDLE}. + * * @return The error, or {@code null}. */ @Nullable diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index f1b6b5bc90..33dfbae3ec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -878,8 +878,15 @@ public class SimpleExoPlayer extends BasePlayer return player.getPlaybackState(); } + @PlaybackSuppressionReason + public int getPlaybackSuppressionReason() { + verifyApplicationThread(); + return player.getPlaybackSuppressionReason(); + } + @Override - public @Nullable ExoPlaybackException getPlaybackError() { + @Nullable + public ExoPlaybackException getPlaybackError() { verifyApplicationThread(); return player.getPlaybackError(); } @@ -1221,9 +1228,13 @@ public class SimpleExoPlayer extends BasePlayer private void updatePlayWhenReady( 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( playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY, - playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY); + playbackSuppressionReason); } private void verifyApplicationThread() { diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index a0d8c7f9d8..51ac5a986d 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -79,6 +79,12 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + @PlaybackSuppressionReason + public int getPlaybackSuppressionReason() { + throw new UnsupportedOperationException(); + } + @Override public ExoPlaybackException getPlaybackError() { throw new UnsupportedOperationException();