Implement setPlaybackParameters for CastPlayer

Issue: #6784
PiperOrigin-RevId: 393374139
This commit is contained in:
bachinger 2021-08-27 18:05:16 +01:00 committed by Christos Tsilopoulos
parent 8909f2017f
commit d930e07a0e
3 changed files with 155 additions and 7 deletions

View File

@ -12,6 +12,10 @@
* Request smaller decoder input buffers for Dolby Vision. This fixes an * Request smaller decoder input buffers for Dolby Vision. This fixes an
issue that could cause UHD Dolby Vision playbacks to fail on some issue that could cause UHD Dolby Vision playbacks to fail on some
devices, including Amazon Fire TV 4K. 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) ### 2.15.0 (2021-08-10)

View File

@ -95,6 +95,7 @@ public final class CastPlayer extends BasePlayer {
COMMAND_SEEK_TO_DEFAULT_POSITION, COMMAND_SEEK_TO_DEFAULT_POSITION,
COMMAND_SEEK_TO_WINDOW, COMMAND_SEEK_TO_WINDOW,
COMMAND_SET_REPEAT_MODE, COMMAND_SET_REPEAT_MODE,
COMMAND_SET_SPEED_AND_PITCH,
COMMAND_GET_CURRENT_MEDIA_ITEM, COMMAND_GET_CURRENT_MEDIA_ITEM,
COMMAND_GET_TIMELINE, COMMAND_GET_TIMELINE,
COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_GET_MEDIA_ITEMS_METADATA,
@ -102,6 +103,9 @@ public final class CastPlayer extends BasePlayer {
COMMAND_CHANGE_MEDIA_ITEMS) COMMAND_CHANGE_MEDIA_ITEMS)
.build(); .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 String TAG = "CastPlayer";
private static final int RENDERER_COUNT = 3; private static final int RENDERER_COUNT = 3;
@ -132,6 +136,7 @@ public final class CastPlayer extends BasePlayer {
// Internal state. // Internal state.
private final StateHolder<Boolean> playWhenReady; private final StateHolder<Boolean> playWhenReady;
private final StateHolder<Integer> repeatMode; private final StateHolder<Integer> repeatMode;
private final StateHolder<PlaybackParameters> playbackParameters;
@Nullable private RemoteMediaClient remoteMediaClient; @Nullable private RemoteMediaClient remoteMediaClient;
private CastTimeline currentTimeline; private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups; private TrackGroupArray currentTrackGroups;
@ -208,6 +213,7 @@ public final class CastPlayer extends BasePlayer {
(listener, flags) -> listener.onEvents(/* player= */ this, new Events(flags))); (listener, flags) -> listener.onEvents(/* player= */ this, new Events(flags)));
playWhenReady = new StateHolder<>(false); playWhenReady = new StateHolder<>(false);
repeatMode = new StateHolder<>(REPEAT_MODE_OFF); repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT);
playbackState = STATE_IDLE; playbackState = STATE_IDLE;
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
currentTrackGroups = TrackGroupArray.EMPTY; currentTrackGroups = TrackGroupArray.EMPTY;
@ -463,14 +469,9 @@ public final class CastPlayer extends BasePlayer {
return C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS; return C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS;
} }
@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
// Unsupported by the RemoteMediaClient API. Do nothing.
}
@Override @Override
public PlaybackParameters getPlaybackParameters() { public PlaybackParameters getPlaybackParameters() {
return PlaybackParameters.DEFAULT; return playbackParameters.value;
} }
@Override @Override
@ -489,6 +490,32 @@ public final class CastPlayer extends BasePlayer {
sessionManager.endCurrentSession(false); 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 @Override
public void setRepeatMode(@RepeatMode int repeatMode) { public void setRepeatMode(@RepeatMode int repeatMode) {
if (remoteMediaClient == null) { if (remoteMediaClient == null) {
@ -761,6 +788,7 @@ public final class CastPlayer extends BasePlayer {
Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying)); Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying));
} }
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null); updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
updatePlaybackRateAndNotifyIfChanged(/* resultCallback= */ null);
boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged(); boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged();
Timeline currentTimeline = getCurrentTimeline(); Timeline currentTimeline = getCurrentTimeline();
currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline); currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline);
@ -844,6 +872,22 @@ public final class CastPlayer extends BasePlayer {
newPlayWhenReadyValue, playWhenReadyChangeReason, fetchPlaybackState(remoteMediaClient)); 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") @RequiresNonNull("remoteMediaClient")
private void updateRepeatModeAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) { private void updateRepeatModeAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
if (repeatMode.acceptsUpdate(resultCallback)) { if (repeatMode.acceptsUpdate(resultCallback)) {
@ -1100,6 +1144,17 @@ public final class CastPlayer extends BasePlayer {
} }
} }
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") @SuppressWarnings("deprecation")
private void setPlayerStateAndNotifyIfChanged( private void setPlayerStateAndNotifyIfChanged(
boolean playWhenReady, boolean playWhenReady,

View File

@ -61,6 +61,7 @@ import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
@ -126,6 +127,7 @@ public class CastPlayerTest {
// Make the remote media client present the same default values as ExoPlayer: // Make the remote media client present the same default values as ExoPlayer:
when(mockRemoteMediaClient.isPaused()).thenReturn(true); when(mockRemoteMediaClient.isPaused()).thenReturn(true);
when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF); when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF);
when(mockMediaStatus.getPlaybackRate()).thenReturn(1.0d);
castPlayer = new CastPlayer(mockCastContext); castPlayer = new CastPlayer(mockCastContext);
castPlayer.addListener(mockListener); castPlayer.addListener(mockListener);
verify(mockRemoteMediaClient).registerCallback(callbackArgumentCaptor.capture()); verify(mockRemoteMediaClient).registerCallback(callbackArgumentCaptor.capture());
@ -208,6 +210,93 @@ public class CastPlayerTest {
assertThat(castPlayer.getPlayWhenReady()).isTrue(); 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 @Test
public void setRepeatMode_masksRemoteState() { public void setRepeatMode_masksRemoteState() {
when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult); when(mockRemoteMediaClient.queueSetRepeatMode(anyInt(), any())).thenReturn(mockPendingResult);
@ -1215,7 +1304,7 @@ public class CastPlayerTest {
assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_TO_WINDOW)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_TO_WINDOW)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_BACK)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_BACK)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_SEEK_FORWARD)).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_SHUFFLE_MODE)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_REPEAT_MODE)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_SET_REPEAT_MODE)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)).isTrue();