Skip to content

Commit

Permalink
Implement setPlaybackParameters for CastPlayer
Browse files Browse the repository at this point in the history
Issue: #6784
PiperOrigin-RevId: 393374139
  • Loading branch information
marcbaechinger authored and christosts committed Sep 16, 2021
1 parent 8909f20 commit d930e07
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 7 deletions.
4 changes: 4 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,17 @@ 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,
COMMAND_SET_MEDIA_ITEMS_METADATA,
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;
Expand Down Expand Up @@ -132,6 +136,7 @@ public final class CastPlayer extends BasePlayer {
// Internal state.
private final StateHolder<Boolean> playWhenReady;
private final StateHolder<Integer> repeatMode;
private final StateHolder<PlaybackParameters> playbackParameters;
@Nullable private RemoteMediaClient remoteMediaClient;
private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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<MediaChannelResult> pendingResult =
remoteMediaClient.setPlaybackRate(actualPlaybackParameters.speed, /* customData= */ null);
this.playbackParameters.pendingResultCallback =
new ResultCallback<MediaChannelResult>() {
@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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit d930e07

Please sign in to comment.