diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a22876ad1a..666522901b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,7 @@ * Add playlist API ([#6161](https://github.com/google/ExoPlayer/issues/6161)). * Add `play` and `pause` methods to `Player`. * Add `Player.getCurrentLiveOffset` to conveniently return the live offset. + * Add `Player.onPlayWhenReadyChanged` with reasons. * Make `MediaSourceEventListener.LoadEventInfo` and `MediaSourceEventListener.MediaLoadData` top-level classes. * Rename `MediaCodecRenderer.onOutputFormatChanged` to 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 5b91410ff9..1202cf1c81 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 @@ -351,7 +351,8 @@ public final class CastPlayer extends BasePlayer { // We update the local state and send the message to the receiver app, which will cause the // operation to be perceived as synchronous by the user. When the operation reports a result, // the local state will be updated to reflect the state reported by the Cast SDK. - setPlayerStateAndNotifyIfChanged(playWhenReady, playbackState); + setPlayerStateAndNotifyIfChanged( + playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, playbackState); flushNotifications(); PendingResult pendingResult = playWhenReady ? remoteMediaClient.play() : remoteMediaClient.pause(); @@ -625,8 +626,14 @@ public final class CastPlayer extends BasePlayer { newPlayWhenReadyValue = !remoteMediaClient.isPaused(); playWhenReady.clearPendingResultCallback(); } + @PlayWhenReadyChangeReason + int playWhenReadyChangeReason = + newPlayWhenReadyValue != playWhenReady.value + ? PLAY_WHEN_READY_CHANGE_REASON_REMOTE + : PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST; // We do not mask the playback state, so try setting it regardless of the playWhenReady masking. - setPlayerStateAndNotifyIfChanged(newPlayWhenReadyValue, fetchPlaybackState(remoteMediaClient)); + setPlayerStateAndNotifyIfChanged( + newPlayWhenReadyValue, playWhenReadyChangeReason, fetchPlaybackState(remoteMediaClient)); } @RequiresNonNull("remoteMediaClient") @@ -718,13 +725,21 @@ public final class CastPlayer extends BasePlayer { } private void setPlayerStateAndNotifyIfChanged( - boolean playWhenReady, @Player.State int playbackState) { - if (this.playWhenReady.value != playWhenReady || this.playbackState != playbackState) { - this.playWhenReady.value = playWhenReady; + boolean playWhenReady, + @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason, + @Player.State int playbackState) { + boolean playWhenReadyChanged = this.playWhenReady.value != playWhenReady; + if (playWhenReadyChanged || this.playbackState != playbackState) { this.playbackState = playbackState; + this.playWhenReady.value = playWhenReady; notificationsBatch.add( new ListenerNotificationTask( - listener -> listener.onPlayerStateChanged(playWhenReady, playbackState))); + listener -> { + listener.onPlayerStateChanged(playWhenReady, playbackState); + if (playWhenReadyChanged) { + listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason); + } + })); } } diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java index 1346c1f842..55a9b22f9b 100644 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastPlayerTest.java @@ -89,6 +89,8 @@ public class CastPlayerTest { verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture()); assertThat(castPlayer.getPlayWhenReady()).isTrue(); verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); + verify(mockListener) + .onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); // There is a status update in the middle, which should be hidden by masking. remoteMediaClientListener.onStatusUpdated(); @@ -111,21 +113,42 @@ public class CastPlayerTest { verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture()); assertThat(castPlayer.getPlayWhenReady()).isTrue(); verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); + verify(mockListener) + .onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); // Upon result, the remote media client is still paused. The state should reflect that. setResultCallbackArgumentCaptor .getValue() .onResult(Mockito.mock(RemoteMediaClient.MediaChannelResult.class)); verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE); + verify(mockListener).onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE); assertThat(castPlayer.getPlayWhenReady()).isFalse(); } + @Test + public void testSetPlayWhenReady_correctChangeReasonOnPause() { + when(mockRemoteMediaClient.play()).thenReturn(mockPendingResult); + when(mockRemoteMediaClient.pause()).thenReturn(mockPendingResult); + castPlayer.play(); + assertThat(castPlayer.getPlayWhenReady()).isTrue(); + verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); + verify(mockListener) + .onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + + castPlayer.pause(); + assertThat(castPlayer.getPlayWhenReady()).isFalse(); + verify(mockListener).onPlayerStateChanged(false, Player.STATE_IDLE); + verify(mockListener) + .onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + } + @Test public void testPlayWhenReady_changesOnStatusUpdates() { assertThat(castPlayer.getPlayWhenReady()).isFalse(); when(mockRemoteMediaClient.isPaused()).thenReturn(false); remoteMediaClientListener.onStatusUpdated(); verify(mockListener).onPlayerStateChanged(true, Player.STATE_IDLE); + verify(mockListener).onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE); assertThat(castPlayer.getPlayWhenReady()).isTrue(); } diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java index e3af8dbb8f..b89b23516c 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java @@ -93,12 +93,17 @@ import java.util.ArrayList; /** Sets the {@link Player.State} of this player. */ public void setState(@Player.State int state, boolean playWhenReady) { - boolean notify = this.state != state || this.playWhenReady != playWhenReady; + boolean playWhenReadyChanged = this.playWhenReady != playWhenReady; + boolean playerStateChanged = this.state != state || playWhenReadyChanged; this.state = state; this.playWhenReady = playWhenReady; - if (notify) { + if (playerStateChanged) { for (Player.EventListener listener : listeners) { listener.onPlayerStateChanged(playWhenReady, state); + if (playWhenReadyChanged) { + listener.onPlayWhenReadyChanged( + playWhenReady, PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + } } } } 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 af11bcd868..161ea9c6ee 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 @@ -424,11 +424,16 @@ import java.util.concurrent.TimeoutException; @Override public void setPlayWhenReady(boolean playWhenReady) { - setPlayWhenReady(playWhenReady, PLAYBACK_SUPPRESSION_REASON_NONE); + setPlayWhenReady( + playWhenReady, + PLAYBACK_SUPPRESSION_REASON_NONE, + PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); } public void setPlayWhenReady( - boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) { + boolean playWhenReady, + @PlaybackSuppressionReason int playbackSuppressionReason, + @PlayWhenReadyChangeReason int playWhenReadyChangeReason) { boolean oldIsPlaying = isPlaying(); boolean oldInternalPlayWhenReady = this.playWhenReady && this.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; @@ -450,6 +455,9 @@ import java.util.concurrent.TimeoutException; if (playWhenReadyChanged) { listener.onPlayerStateChanged(playWhenReady, playbackState); } + if (playWhenReadyChanged) { + listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason); + } if (suppressionReasonChanged) { listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason); } 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 1f1c23a980..c6bdb424e9 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 @@ -418,6 +418,15 @@ public interface Player { */ default void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {} + /** + * Called when the value returned from {@link #getPlayWhenReady()} changes. + * + * @param playWhenReady Whether playback will proceed when ready. + * @param reason The {@link PlayWhenReadyChangeReason reason} for the change. + */ + default void onPlayWhenReadyChanged( + boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {} + /** * Called when the value returned from {@link #getPlaybackSuppressionReason()} changes. * @@ -549,6 +558,31 @@ public interface Player { */ int STATE_ENDED = 4; + /** + * Reasons for {@link #getPlayWhenReady() playWhenReady} changes. One of {@link + * #PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST}, {@link + * #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS}, {@link + * #PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY} or {@link + * #PLAY_WHEN_READY_CHANGE_REASON_REMOTE}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS, + PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY, + PLAY_WHEN_READY_CHANGE_REASON_REMOTE + }) + @interface PlayWhenReadyChangeReason {} + /** Playback has been started or paused by the user. */ + int PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST = 1; + /** Playback has been paused because of a loss of audio focus. */ + int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS = 2; + /** Playback has been paused to avoid becoming noisy. */ + int PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY = 3; + /** Playback has been started or paused because of a remote change. */ + int PLAY_WHEN_READY_CHANGE_REASON_REMOTE = 4; + /** * Reason why playback is suppressed even though {@link #getPlayWhenReady()} is {@code true}. One * of {@link #PLAYBACK_SUPPRESSION_REASON_NONE} or {@link 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 ef7e715b6e..7e0bed5f3f 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 @@ -688,11 +688,13 @@ public class SimpleExoPlayer extends BasePlayer } } + boolean playWhenReady = getPlayWhenReady(); @AudioFocusManager.PlayerCommand int playerCommand = audioFocusManager.setAudioAttributes( - handleAudioFocus ? audioAttributes : null, getPlayWhenReady(), getPlaybackState()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); + handleAudioFocus ? audioAttributes : null, playWhenReady, getPlaybackState()); + updatePlayWhenReady( + playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand)); } @Override @@ -1179,9 +1181,11 @@ public class SimpleExoPlayer extends BasePlayer @Override public void prepare() { verifyApplicationThread(); + boolean playWhenReady = getPlayWhenReady(); @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); + int playerCommand = audioFocusManager.handlePrepare(playWhenReady); + updatePlayWhenReady( + playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand)); player.prepare(); } @@ -1318,7 +1322,8 @@ public class SimpleExoPlayer extends BasePlayer verifyApplicationThread(); @AudioFocusManager.PlayerCommand int playerCommand = audioFocusManager.handleSetPlayWhenReady(playWhenReady, getPlaybackState()); - updatePlayWhenReady(playWhenReady, playerCommand); + updatePlayWhenReady( + playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand)); } @Override @@ -1634,14 +1639,16 @@ public class SimpleExoPlayer extends BasePlayer } private void updatePlayWhenReady( - boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) { + boolean playWhenReady, + @AudioFocusManager.PlayerCommand int playerCommand, + @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason) { playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY; @PlaybackSuppressionReason int playbackSuppressionReason = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY ? Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS : Player.PLAYBACK_SUPPRESSION_REASON_NONE; - player.setPlayWhenReady(playWhenReady, playbackSuppressionReason); + player.setPlayWhenReady(playWhenReady, playbackSuppressionReason, playWhenReadyChangeReason); } private void verifyApplicationThread() { @@ -1655,6 +1662,12 @@ public class SimpleExoPlayer extends BasePlayer } } + private static int getPlayWhenReadyChangeReason(boolean playWhenReady, int playerCommand) { + return playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY + ? PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS + : PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST; + } + private final class ComponentListener implements VideoRendererEventListener, AudioRendererEventListener, @@ -1872,14 +1885,23 @@ public class SimpleExoPlayer extends BasePlayer @Override public void executePlayerCommand(@AudioFocusManager.PlayerCommand int playerCommand) { - updatePlayWhenReady(getPlayWhenReady(), playerCommand); + boolean playWhenReady = getPlayWhenReady(); + updatePlayWhenReady( + playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand)); } // AudioBecomingNoisyManager.EventListener implementation. @Override public void onAudioBecomingNoisy() { - pause(); + // Command is always PLAYER_COMMAND_DO_NOT_PLAY but the call is needed to abandon the + // audio focus if the focus is currently held. + int playerCommand = + audioFocusManager.handleSetPlayWhenReady(/* playWhenReady= */ false, getPlaybackState()); + updatePlayWhenReady( + /* playWhenReady= */ false, + playerCommand, + Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY); } // Player.EventListener implementation. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 9fa9257459..7cfa532e83 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -446,6 +446,15 @@ public class AnalyticsCollector } } + @Override + public final void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); + for (AnalyticsListener listener : listeners) { + listener.onPlayWhenReadyChanged(eventTime, playWhenReady, reason); + } + } + @Override public void onPlaybackSuppressionReasonChanged( @PlaybackSuppressionReason int playbackSuppressionReason) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 353e7ac340..5f49fd22a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -133,6 +133,16 @@ public interface AnalyticsListener { default void onPlayerStateChanged( EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {} + /** + * Called when the value changed that indicates whether playback will proceed when ready. + * + * @param eventTime The event time. + * @param playWhenReady Whether playback will proceed when ready. + * @param reason The {@link Player.PlayWhenReadyChangeReason reason} of the change. + */ + default void onPlayWhenReadyChanged( + EventTime eventTime, boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {} + /** * Called when playback suppression reason changed. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index 6d5820d1f3..3cf118d943 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -98,7 +98,16 @@ public class EventLogger implements AnalyticsListener { @Override public void onPlayerStateChanged( EventTime eventTime, boolean playWhenReady, @Player.State int state) { - logd(eventTime, "state", playWhenReady + ", " + getStateString(state)); + logd(eventTime, "state", getStateString(state)); + } + + @Override + public void onPlayWhenReadyChanged( + EventTime eventTime, boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + logd( + eventTime, + "playWhenReady", + playWhenReady + ", " + getPlayWhenReadyChangeReasonString(reason)); } @Override @@ -637,4 +646,20 @@ public class EventLogger implements AnalyticsListener { return "?"; } } + + private static String getPlayWhenReadyChangeReasonString( + @Player.PlayWhenReadyChangeReason int reason) { + switch (reason) { + case Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY: + return "AUDIO_BECOMING_NOISY"; + case Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS: + return "AUDIO_FOCUS_LOSS"; + case Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE: + return "REMOTE"; + case Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST: + return "USER_REQUEST"; + default: + return "?"; + } + } }