From e784d2c507df8e35846f48ef29612362f4d4fa66 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 | 14 +++- .../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 | 8 ++- 7 files changed, 146 insertions(+), 19 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fdd8d2c5ea..3a24a8bf6e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -72,6 +72,11 @@ * Fix decoder selection for E-AC3 JOC streams ([#6398](https://github.com/google/ExoPlayer/issues/6398)). * 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 ### 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 6a33aa0428..b82e736c86 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 @@ -323,11 +323,17 @@ public final class CastPlayer extends BasePlayer { } @Override - @Player.State + @State public int getPlaybackState() { return playbackState; } + @Override + @PlaybackSuppressionReason + public int getPlaybackSuppressionReason() { + return Player.PLAYBACK_SUPPRESSION_REASON_NONE; + } + @Override @Nullable public ExoPlaybackException getPlaybackError() { @@ -538,6 +544,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 @@ -548,6 +555,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 1b3e57cede..baa2a767b5 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 15a0e3a7bd..d87bdc7d74 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 @@ -193,11 +194,17 @@ import java.util.concurrent.CopyOnWriteArrayList; } @Override - @Player.State + @State public int getPlaybackState() { return playbackInfo.playbackState; } + @Override + @PlaybackSuppressionReason + public int getPlaybackSuppressionReason() { + return playbackSuppressionReason; + } + @Override @Nullable public ExoPlaybackException getPlaybackError() { @@ -242,19 +249,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); + } + }); } } @@ -674,9 +697,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, @@ -687,7 +712,8 @@ import java.util.concurrent.CopyOnWriteArrayList; positionDiscontinuityReason, timelineChangeReason, seekProcessed, - playWhenReady)); + playWhenReady, + /* isPlayingChanged= */ previousIsPlaying != isPlaying)); } private void notifyListeners(ListenerInvocation listenerInvocation) { @@ -733,6 +759,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, @@ -740,10 +767,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; @@ -752,6 +780,7 @@ import java.util.concurrent.CopyOnWriteArrayList; this.timelineChangeReason = timelineChangeReason; this.seekProcessed = seekProcessed; this.playWhenReady = playWhenReady; + this.isPlayingChanged = isPlayingChanged; playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState; playbackErrorChanged = previousPlaybackInfo.playbackError != playbackInfo.playbackError @@ -793,6 +822,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 f4d3dfdeda..b8be7bafa7 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 @@ -392,6 +392,13 @@ public interface Player { */ 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. * @@ -509,6 +516,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}. @@ -635,12 +656,42 @@ public interface Player { @State 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 795eca5ea0..43a5ebab99 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 @@ -1078,12 +1078,19 @@ public class SimpleExoPlayer extends BasePlayer } @Override - @Player.State + @State public int getPlaybackState() { verifyApplicationThread(); return player.getPlaybackState(); } + @Override + @PlaybackSuppressionReason + public int getPlaybackSuppressionReason() { + verifyApplicationThread(); + return player.getPlaybackSuppressionReason(); + } + @Override @Nullable public ExoPlaybackException getPlaybackError() { @@ -1407,9 +1414,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/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index eaebe5a12d..18eaec2cd7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -75,11 +75,17 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer { } @Override - @Player.State + @State public int getPlaybackState() { throw new UnsupportedOperationException(); } + @Override + @PlaybackSuppressionReason + public int getPlaybackSuppressionReason() { + throw new UnsupportedOperationException(); + } + @Override public ExoPlaybackException getPlaybackError() { throw new UnsupportedOperationException();