diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 38cc648768e..10052d632ef 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,6 +12,10 @@ * Request smaller decoder input buffers for Dolby Vision. This fixes an issue that could cause UHD Dolby Vision playbacks to fail on some devices, including Amazon Fire TV 4K. +* Cast extension: + * Implement `CastPlayer.setPlaybackParameters(PlaybackParameters)` to + support setting the playback speed + ([#6784](https://github.com/google/ExoPlayer/issues/6784)). ### 2.15.0 (2021-08-10) 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 e1e752ea762..622ab78d540 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 @@ -95,6 +95,7 @@ public final class CastPlayer extends BasePlayer { COMMAND_SEEK_TO_DEFAULT_POSITION, COMMAND_SEEK_TO_WINDOW, COMMAND_SET_REPEAT_MODE, + COMMAND_SET_SPEED_AND_PITCH, COMMAND_GET_CURRENT_MEDIA_ITEM, COMMAND_GET_TIMELINE, COMMAND_GET_MEDIA_ITEMS_METADATA, @@ -102,6 +103,9 @@ public final class CastPlayer extends BasePlayer { COMMAND_CHANGE_MEDIA_ITEMS) .build(); + public static final float MIN_SPEED_SUPPORTED = 0.5f; + public static final float MAX_SPEED_SUPPORTED = 2.0f; + private static final String TAG = "CastPlayer"; private static final int RENDERER_COUNT = 3; @@ -132,6 +136,7 @@ public final class CastPlayer extends BasePlayer { // Internal state. private final StateHolder playWhenReady; private final StateHolder repeatMode; + private final StateHolder playbackParameters; @Nullable private RemoteMediaClient remoteMediaClient; private CastTimeline currentTimeline; private TrackGroupArray currentTrackGroups; @@ -208,6 +213,7 @@ public CastPlayer( (listener, flags) -> listener.onEvents(/* player= */ this, new Events(flags))); playWhenReady = new StateHolder<>(false); repeatMode = new StateHolder<>(REPEAT_MODE_OFF); + playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT); playbackState = STATE_IDLE; currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; currentTrackGroups = TrackGroupArray.EMPTY; @@ -463,14 +469,9 @@ public int getMaxSeekToPreviousPosition() { return C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS; } - @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { - // Unsupported by the RemoteMediaClient API. Do nothing. - } - @Override public PlaybackParameters getPlaybackParameters() { - return PlaybackParameters.DEFAULT; + return playbackParameters.value; } @Override @@ -489,6 +490,32 @@ public void release() { sessionManager.endCurrentSession(false); } + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + if (remoteMediaClient == null) { + return; + } + PlaybackParameters actualPlaybackParameters = + new PlaybackParameters( + Util.constrainValue( + playbackParameters.speed, MIN_SPEED_SUPPORTED, MAX_SPEED_SUPPORTED)); + setPlaybackParametersAndNotifyIfChanged(actualPlaybackParameters); + listeners.flushEvents(); + PendingResult pendingResult = + remoteMediaClient.setPlaybackRate(actualPlaybackParameters.speed, /* customData= */ null); + this.playbackParameters.pendingResultCallback = + new ResultCallback() { + @Override + public void onResult(MediaChannelResult mediaChannelResult) { + if (remoteMediaClient != null) { + updatePlaybackRateAndNotifyIfChanged(this); + listeners.flushEvents(); + } + } + }; + pendingResult.setResultCallback(this.playbackParameters.pendingResultCallback); + } + @Override public void setRepeatMode(@RepeatMode int repeatMode) { if (remoteMediaClient == null) { @@ -761,6 +788,7 @@ private void updateInternalStateAndNotifyIfChanged() { Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying)); } updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null); + updatePlaybackRateAndNotifyIfChanged(/* resultCallback= */ null); boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged(); Timeline currentTimeline = getCurrentTimeline(); currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline); @@ -844,6 +872,22 @@ private void updatePlayerStateAndNotifyIfChanged(@Nullable ResultCallback res newPlayWhenReadyValue, playWhenReadyChangeReason, fetchPlaybackState(remoteMediaClient)); } + @RequiresNonNull("remoteMediaClient") + private void updatePlaybackRateAndNotifyIfChanged(@Nullable ResultCallback resultCallback) { + if (playbackParameters.acceptsUpdate(resultCallback)) { + @Nullable MediaStatus mediaStatus = remoteMediaClient.getMediaStatus(); + float speed = + mediaStatus != null + ? (float) mediaStatus.getPlaybackRate() + : PlaybackParameters.DEFAULT.speed; + if (speed > 0.0f) { + // Set the speed if not paused. + setPlaybackParametersAndNotifyIfChanged(new PlaybackParameters(speed)); + } + playbackParameters.clearPendingResultCallback(); + } + } + @RequiresNonNull("remoteMediaClient") private void updateRepeatModeAndNotifyIfChanged(@Nullable ResultCallback resultCallback) { if (repeatMode.acceptsUpdate(resultCallback)) { @@ -1100,6 +1144,17 @@ private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) } } + private void setPlaybackParametersAndNotifyIfChanged(PlaybackParameters playbackParameters) { + if (this.playbackParameters.value.equals(playbackParameters)) { + return; + } + this.playbackParameters.value = playbackParameters; + listeners.queueEvent( + Player.EVENT_PLAYBACK_PARAMETERS_CHANGED, + listener -> listener.onPlaybackParametersChanged(playbackParameters)); + updateAvailableCommandsAndNotifyIfChanged(); + } + @SuppressWarnings("deprecation") private void setPlayerStateAndNotifyIfChanged( boolean playWhenReady, 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 58c6eaf10d4..d544cd79139 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 @@ -61,6 +61,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Assertions; @@ -126,6 +127,7 @@ public void setUp() { // Make the remote media client present the same default values as ExoPlayer: when(mockRemoteMediaClient.isPaused()).thenReturn(true); when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF); + when(mockMediaStatus.getPlaybackRate()).thenReturn(1.0d); castPlayer = new CastPlayer(mockCastContext); castPlayer.addListener(mockListener); verify(mockRemoteMediaClient).registerCallback(callbackArgumentCaptor.capture()); @@ -208,6 +210,93 @@ public void playWhenReady_changesOnStatusUpdates() { assertThat(castPlayer.getPlayWhenReady()).isTrue(); } + @Test + public void playbackParameters_defaultPlaybackSpeed_isUnitSpeed() { + assertThat(castPlayer.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + } + + @Test + public void playbackParameters_onStatusUpdated_setsRemotePlaybackSpeed() { + PlaybackParameters expectedPlaybackParameters = new PlaybackParameters(/* speed= */ 1.234f); + when(mockMediaStatus.getPlaybackRate()).thenReturn(1.234d); + + remoteMediaClientCallback.onStatusUpdated(); + + assertThat(castPlayer.getPlaybackParameters()).isEqualTo(expectedPlaybackParameters); + verify(mockListener).onPlaybackParametersChanged(expectedPlaybackParameters); + } + + @Test + public void playbackParameters_onStatusUpdated_ignoreInPausedState() { + when(mockMediaStatus.getPlaybackRate()).thenReturn(0.0d); + + remoteMediaClientCallback.onStatusUpdated(); + + assertThat(castPlayer.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void setPlaybackParameters_speedOutOfRange_valueIsConstraintToMinAndMax() { + when(mockRemoteMediaClient.setPlaybackRate(eq(2d), any())).thenReturn(mockPendingResult); + when(mockRemoteMediaClient.setPlaybackRate(eq(0.5d), any())).thenReturn(mockPendingResult); + PlaybackParameters expectedMaxValuePlaybackParameters = new PlaybackParameters(/* speed= */ 2f); + PlaybackParameters expectedMinValuePlaybackParameters = + new PlaybackParameters(/* speed= */ 0.5f); + + castPlayer.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2.001f)); + verify(mockListener).onPlaybackParametersChanged(expectedMaxValuePlaybackParameters); + castPlayer.setPlaybackParameters(new PlaybackParameters(/* speed= */ 0.499f)); + verify(mockListener).onPlaybackParametersChanged(expectedMinValuePlaybackParameters); + } + + @Test + public void setPlaybackParameters_masksPendingState() { + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 1.234f); + when(mockRemoteMediaClient.setPlaybackRate(eq((double) 1.234f), any())) + .thenReturn(mockPendingResult); + + castPlayer.setPlaybackParameters(playbackParameters); + + verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture()); + assertThat(castPlayer.getPlaybackParameters().speed).isEqualTo(1.234f); + verify(mockListener).onPlaybackParametersChanged(playbackParameters); + + // Simulate a status update while the update is pending that must not override the masked speed. + when(mockMediaStatus.getPlaybackRate()).thenReturn(99.0d); + remoteMediaClientCallback.onStatusUpdated(); + assertThat(castPlayer.getPlaybackParameters().speed).isEqualTo(1.234f); + + // Call the captured result callback when the device responds. The listener must not be called. + when(mockMediaStatus.getPlaybackRate()).thenReturn(1.234d); + setResultCallbackArgumentCaptor + .getValue() + .onResult(mock(RemoteMediaClient.MediaChannelResult.class)); + assertThat(castPlayer.getPlaybackParameters().speed).isEqualTo(1.234f); + verifyNoMoreInteractions(mockListener); + } + + @Test + public void setPlaybackParameters_speedChangeNotSupported_resetOnResultCallback() { + when(mockRemoteMediaClient.setPlaybackRate(eq((double) 1.234f), any())) + .thenReturn(mockPendingResult); + PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 1.234f); + + // Change the playback speed and and capture the result callback. + castPlayer.setPlaybackParameters(playbackParameters); + verify(mockPendingResult).setResultCallback(setResultCallbackArgumentCaptor.capture()); + verify(mockListener).onPlaybackParametersChanged(new PlaybackParameters(/* speed= */ 1.234f)); + + // The device does not support speed changes and returns unit speed to the result callback. + when(mockMediaStatus.getPlaybackRate()).thenReturn(1.0d); + setResultCallbackArgumentCaptor + .getValue() + .onResult(mock(RemoteMediaClient.MediaChannelResult.class)); + assertThat(castPlayer.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + verify(mockListener).onPlaybackParametersChanged(PlaybackParameters.DEFAULT); + verifyNoMoreInteractions(mockListener); + } + @Test public void setRepeatMode_masksRemoteState() { when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult); @@ -1215,7 +1304,7 @@ public void isCommandAvailable_isTrueForAvailableCommands() { assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_TO_WINDOW)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_BACK)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_FORWARD)).isTrue(); - assertThat(castPlayer.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH)).isFalse(); + assertThat(castPlayer.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_SET_SHUFFLE_MODE)).isFalse(); assertThat(castPlayer.isCommandAvailable(COMMAND_SET_REPEAT_MODE)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)).isTrue();