diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index ab48c5581d..84af1d467c 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -3,6 +3,10 @@
### Unreleased changes
* Common Library:
+ * Add `ForwardingSimpleBasePlayer` that allows forwarding to another
+ player with small adjustments while ensuring full consistency and
+ listener handling
+ ([#1183](https://github.com/androidx/media/issues/1183)).
* Replace `SimpleBasePlayer.State.playlist` by `getPlaylist()` method.
* Add override for `SimpleBasePlayer.State.Builder.setPlaylist()` to
directly specify a `Timeline` and current `Tracks` and `Metadata`
diff --git a/libraries/common/src/main/java/androidx/media3/common/ForwardingSimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/ForwardingSimpleBasePlayer.java
new file mode 100644
index 0000000000..821f8687de
--- /dev/null
+++ b/libraries/common/src/main/java/androidx/media3/common/ForwardingSimpleBasePlayer.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.common;
+
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import androidx.annotation.Nullable;
+import androidx.media3.common.util.UnstableApi;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.util.List;
+
+// LINT.IfChange(javadoc)
+/**
+ * A {@link SimpleBasePlayer} that forwards all calls to another {@link Player} instance.
+ *
+ *
The class can be used to selectively override {@link #getState()} or {@code handle{Action}}
+ * methods:
+ *
+ *
{@code
+ * new ForwardingSimpleBasePlayer(player) {
+ * @Override
+ * protected State getState() {
+ * State state = super.getState();
+ * // Modify current state as required:
+ * return state.buildUpon().setAvailableCommands(filteredCommands).build();
+ * }
+ *
+ * @Override
+ * protected ListenableFuture> handleSetRepeatMode(int repeatMode) {
+ * // Modify actions by directly calling the underlying player as needed:
+ * getPlayer().setShuffleModeEnabled(true);
+ * // ..or forward to the default handling with modified parameters:
+ * return super.handleSetRepeatMode(Player.REPEAT_MODE_ALL);
+ * }
+ * }
+ * }
+ *
+ * This base class handles many aspect of the player implementation to simplify the subclass, for
+ * example listener handling. See the documentation of {@link SimpleBasePlayer} for a more detailed
+ * description.
+ */
+@UnstableApi
+public class ForwardingSimpleBasePlayer extends SimpleBasePlayer {
+
+ private final Player player;
+
+ private ForwardingPositionSupplier currentPositionSupplier;
+ private Metadata lastTimedMetadata;
+ private @Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason;
+ private @Player.DiscontinuityReason int pendingDiscontinuityReason;
+ private long pendingPositionDiscontinuityNewPositionMs;
+ private boolean pendingFirstFrameRendered;
+
+ /**
+ * Creates the forwarding player.
+ *
+ * @param player The {@link Player} to forward to.
+ */
+ public ForwardingSimpleBasePlayer(Player player) {
+ super(player.getApplicationLooper());
+ this.player = player;
+ this.lastTimedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET);
+ this.playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
+ this.pendingDiscontinuityReason = Player.DISCONTINUITY_REASON_INTERNAL;
+ this.currentPositionSupplier = new ForwardingPositionSupplier(player);
+ player.addListener(
+ new Listener() {
+ @Override
+ public void onMetadata(Metadata metadata) {
+ lastTimedMetadata = metadata;
+ }
+
+ @Override
+ public void onPlayWhenReadyChanged(
+ boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
+ playWhenReadyChangeReason = reason;
+ }
+
+ @Override
+ public void onPositionDiscontinuity(
+ PositionInfo oldPosition,
+ PositionInfo newPosition,
+ @Player.DiscontinuityReason int reason) {
+ pendingDiscontinuityReason = reason;
+ pendingPositionDiscontinuityNewPositionMs = newPosition.positionMs;
+ // Any previously created State will directly call through to player.getCurrentPosition
+ // via the existing position supplier. From this point onwards, this is wrong as the
+ // player had a discontinuity and will now return a new position unrelated to the old
+ // State. We can disconnect these old State objects from the underlying Player by fixing
+ // the position to the one before the discontinuity and using a new (live) position
+ // supplier for future State objects.
+ currentPositionSupplier.setConstant(
+ oldPosition.positionMs, oldPosition.contentPositionMs);
+ currentPositionSupplier = new ForwardingPositionSupplier(player);
+ }
+
+ @Override
+ public void onRenderedFirstFrame() {
+ pendingFirstFrameRendered = true;
+ }
+
+ @SuppressWarnings("method.invocation.invalid") // Calling method from constructor.
+ @Override
+ public void onEvents(Player player, Events events) {
+ invalidateState();
+ }
+ });
+ }
+
+ /** Returns the wrapped player. */
+ protected final Player getPlayer() {
+ return player;
+ }
+
+ @Override
+ protected State getState() {
+ // Ordered alphabetically by State.Builder setters.
+ State.Builder state = new State.Builder();
+ ForwardingPositionSupplier positionSupplier = currentPositionSupplier;
+ if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
+ state.setAdBufferedPositionMs(positionSupplier::getBufferedPositionMs);
+ state.setAdPositionMs(positionSupplier::getCurrentPositionMs);
+ }
+ if (player.isCommandAvailable(Player.COMMAND_GET_AUDIO_ATTRIBUTES)) {
+ state.setAudioAttributes(player.getAudioAttributes());
+ }
+ state.setAvailableCommands(player.getAvailableCommands());
+ if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
+ state.setContentBufferedPositionMs(positionSupplier::getContentBufferedPositionMs);
+ state.setContentPositionMs(positionSupplier::getContentPositionMs);
+ if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) {
+ state.setCurrentAd(player.getCurrentAdGroupIndex(), player.getCurrentAdIndexInAdGroup());
+ }
+ }
+ if (player.isCommandAvailable(Player.COMMAND_GET_TEXT)) {
+ state.setCurrentCues(player.getCurrentCues());
+ }
+ if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) {
+ state.setCurrentMediaItemIndex(player.getCurrentMediaItemIndex());
+ }
+ state.setDeviceInfo(player.getDeviceInfo());
+ if (player.isCommandAvailable(Player.COMMAND_GET_DEVICE_VOLUME)) {
+ state.setDeviceVolume(player.getDeviceVolume());
+ state.setIsDeviceMuted(player.isDeviceMuted());
+ }
+ state.setIsLoading(player.isLoading());
+ state.setMaxSeekToPreviousPositionMs(player.getMaxSeekToPreviousPosition());
+ if (pendingFirstFrameRendered) {
+ state.setNewlyRenderedFirstFrame(true);
+ pendingFirstFrameRendered = false;
+ }
+ state.setPlaybackParameters(player.getPlaybackParameters());
+ state.setPlaybackState(player.getPlaybackState());
+ state.setPlaybackSuppressionReason(player.getPlaybackSuppressionReason());
+ state.setPlayerError(player.getPlayerError());
+ if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)) {
+ Tracks tracks =
+ player.isCommandAvailable(Player.COMMAND_GET_TRACKS)
+ ? player.getCurrentTracks()
+ : Tracks.EMPTY;
+ MediaMetadata mediaMetadata =
+ player.isCommandAvailable(Player.COMMAND_GET_METADATA) ? player.getMediaMetadata() : null;
+ state.setPlaylist(player.getCurrentTimeline(), tracks, mediaMetadata);
+ }
+ if (player.isCommandAvailable(Player.COMMAND_GET_METADATA)) {
+ state.setPlaylistMetadata(player.getPlaylistMetadata());
+ }
+ state.setPlayWhenReady(player.getPlayWhenReady(), playWhenReadyChangeReason);
+ if (pendingPositionDiscontinuityNewPositionMs != C.TIME_UNSET) {
+ state.setPositionDiscontinuity(
+ pendingDiscontinuityReason, pendingPositionDiscontinuityNewPositionMs);
+ pendingPositionDiscontinuityNewPositionMs = C.TIME_UNSET;
+ }
+ state.setRepeatMode(player.getRepeatMode());
+ state.setSeekBackIncrementMs(player.getSeekBackIncrement());
+ state.setSeekForwardIncrementMs(player.getSeekForwardIncrement());
+ state.setShuffleModeEnabled(player.getShuffleModeEnabled());
+ state.setSurfaceSize(player.getSurfaceSize());
+ state.setTimedMetadata(lastTimedMetadata);
+ if (player.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
+ state.setTotalBufferedDurationMs(positionSupplier::getTotalBufferedDurationMs);
+ }
+ state.setTrackSelectionParameters(player.getTrackSelectionParameters());
+ state.setVideoSize(player.getVideoSize());
+ if (player.isCommandAvailable(Player.COMMAND_GET_VOLUME)) {
+ state.setVolume(player.getVolume());
+ }
+ return state.build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetPlayWhenReady(boolean playWhenReady) {
+ player.setPlayWhenReady(playWhenReady);
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handlePrepare() {
+ player.prepare();
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleStop() {
+ player.stop();
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleRelease() {
+ player.release();
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetRepeatMode(@Player.RepeatMode int repeatMode) {
+ player.setRepeatMode(repeatMode);
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetShuffleModeEnabled(boolean shuffleModeEnabled) {
+ player.setShuffleModeEnabled(shuffleModeEnabled);
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetPlaybackParameters(PlaybackParameters playbackParameters) {
+ player.setPlaybackParameters(playbackParameters);
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetTrackSelectionParameters(
+ TrackSelectionParameters trackSelectionParameters) {
+ player.setTrackSelectionParameters(trackSelectionParameters);
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetPlaylistMetadata(MediaMetadata playlistMetadata) {
+ player.setPlaylistMetadata(playlistMetadata);
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetVolume(float volume) {
+ player.setVolume(volume);
+ return Futures.immediateVoidFuture();
+ }
+
+ @SuppressWarnings("deprecation") // Calling deprecated method if updated command not available.
+ @Override
+ protected ListenableFuture> handleSetDeviceVolume(int deviceVolume, int flags) {
+ if (player.isCommandAvailable(Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS)) {
+ player.setDeviceVolume(deviceVolume, flags);
+ } else {
+ player.setDeviceVolume(deviceVolume);
+ }
+ return Futures.immediateVoidFuture();
+ }
+
+ @SuppressWarnings("deprecation") // Calling deprecated method if updated command not available.
+ @Override
+ protected ListenableFuture> handleIncreaseDeviceVolume(@C.VolumeFlags int flags) {
+ if (player.isCommandAvailable(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)) {
+ player.increaseDeviceVolume(flags);
+ } else {
+ player.increaseDeviceVolume();
+ }
+ return Futures.immediateVoidFuture();
+ }
+
+ @SuppressWarnings("deprecation") // Calling deprecated method if updated command not available.
+ @Override
+ protected ListenableFuture> handleDecreaseDeviceVolume(@C.VolumeFlags int flags) {
+ if (player.isCommandAvailable(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)) {
+ player.decreaseDeviceVolume(flags);
+ } else {
+ player.decreaseDeviceVolume();
+ }
+ return Futures.immediateVoidFuture();
+ }
+
+ @SuppressWarnings("deprecation") // Calling deprecated method if updated command not available.
+ @Override
+ protected ListenableFuture> handleSetDeviceMuted(boolean muted, @C.VolumeFlags int flags) {
+ if (player.isCommandAvailable(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)) {
+ player.setDeviceMuted(muted, flags);
+ } else {
+ player.setDeviceMuted(muted);
+ }
+
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetAudioAttributes(
+ AudioAttributes audioAttributes, boolean handleAudioFocus) {
+ player.setAudioAttributes(audioAttributes, handleAudioFocus);
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetVideoOutput(Object videoOutput) {
+ if (videoOutput instanceof SurfaceView) {
+ player.setVideoSurfaceView((SurfaceView) videoOutput);
+ } else if (videoOutput instanceof TextureView) {
+ player.setVideoTextureView((TextureView) videoOutput);
+ } else if (videoOutput instanceof SurfaceHolder) {
+ player.setVideoSurfaceHolder((SurfaceHolder) videoOutput);
+ } else if (videoOutput instanceof Surface) {
+ player.setVideoSurface((Surface) videoOutput);
+ } else {
+ throw new IllegalStateException();
+ }
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleClearVideoOutput(@Nullable Object videoOutput) {
+ if (videoOutput instanceof SurfaceView) {
+ player.clearVideoSurfaceView((SurfaceView) videoOutput);
+ } else if (videoOutput instanceof TextureView) {
+ player.clearVideoTextureView((TextureView) videoOutput);
+ } else if (videoOutput instanceof SurfaceHolder) {
+ player.clearVideoSurfaceHolder((SurfaceHolder) videoOutput);
+ } else if (videoOutput instanceof Surface) {
+ player.clearVideoSurface((Surface) videoOutput);
+ } else if (videoOutput == null) {
+ player.clearVideoSurface();
+ } else {
+ throw new IllegalStateException();
+ }
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetMediaItems(
+ List mediaItems, int startIndex, long startPositionMs) {
+ boolean useSingleItemCall =
+ mediaItems.size() == 1 && player.isCommandAvailable(Player.COMMAND_SET_MEDIA_ITEM);
+ if (startIndex == C.INDEX_UNSET) {
+ if (useSingleItemCall) {
+ player.setMediaItem(mediaItems.get(0));
+ } else {
+ player.setMediaItems(mediaItems);
+ }
+ } else {
+ if (useSingleItemCall) {
+ player.setMediaItem(mediaItems.get(0), startPositionMs);
+ } else {
+ player.setMediaItems(mediaItems, startIndex, startPositionMs);
+ }
+ }
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleAddMediaItems(int index, List mediaItems) {
+ if (mediaItems.size() == 1) {
+ player.addMediaItem(index, mediaItems.get(0));
+ } else {
+ player.addMediaItems(index, mediaItems);
+ }
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleMoveMediaItems(int fromIndex, int toIndex, int newIndex) {
+ if (toIndex == fromIndex + 1) {
+ player.moveMediaItem(fromIndex, newIndex);
+ } else {
+ player.moveMediaItems(fromIndex, toIndex, newIndex);
+ }
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleReplaceMediaItems(
+ int fromIndex, int toIndex, List mediaItems) {
+ if (toIndex == fromIndex + 1 && mediaItems.size() == 1) {
+ player.replaceMediaItem(fromIndex, mediaItems.get(0));
+ } else {
+ player.replaceMediaItems(fromIndex, toIndex, mediaItems);
+ }
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleRemoveMediaItems(int fromIndex, int toIndex) {
+ if (toIndex == fromIndex + 1) {
+ player.removeMediaItem(fromIndex);
+ } else {
+ player.removeMediaItems(fromIndex, toIndex);
+ }
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleSeek(
+ int mediaItemIndex, long positionMs, @Command int seekCommand) {
+ switch (seekCommand) {
+ case Player.COMMAND_SEEK_BACK:
+ player.seekBack();
+ break;
+ case Player.COMMAND_SEEK_FORWARD:
+ player.seekForward();
+ break;
+ case Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM:
+ player.seekTo(positionMs);
+ break;
+ case Player.COMMAND_SEEK_TO_DEFAULT_POSITION:
+ player.seekToDefaultPosition();
+ break;
+ case Player.COMMAND_SEEK_TO_MEDIA_ITEM:
+ if (mediaItemIndex != C.INDEX_UNSET) {
+ player.seekTo(mediaItemIndex, positionMs);
+ }
+ break;
+ case Player.COMMAND_SEEK_TO_NEXT:
+ player.seekToNext();
+ break;
+ case Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM:
+ player.seekToNextMediaItem();
+ break;
+ case Player.COMMAND_SEEK_TO_PREVIOUS:
+ player.seekToPrevious();
+ break;
+ case Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM:
+ player.seekToPreviousMediaItem();
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ return Futures.immediateVoidFuture();
+ }
+
+ /**
+ * Forwards to the changing position values of the wrapped player until the forwarding is
+ * deactivated with constant values.
+ */
+ private static final class ForwardingPositionSupplier {
+
+ private final Player player;
+
+ private long positionsMs;
+ private long contentPositionMs;
+
+ public ForwardingPositionSupplier(Player player) {
+ this.player = player;
+ this.positionsMs = C.TIME_UNSET;
+ this.contentPositionMs = C.TIME_UNSET;
+ }
+
+ public void setConstant(long positionMs, long contentPositionMs) {
+ this.positionsMs = positionMs;
+ this.contentPositionMs = contentPositionMs;
+ }
+
+ public long getCurrentPositionMs() {
+ return positionsMs == C.TIME_UNSET ? player.getCurrentPosition() : positionsMs;
+ }
+
+ public long getBufferedPositionMs() {
+ return positionsMs == C.TIME_UNSET ? player.getBufferedPosition() : positionsMs;
+ }
+
+ public long getContentPositionMs() {
+ return contentPositionMs == C.TIME_UNSET ? player.getContentPosition() : contentPositionMs;
+ }
+
+ public long getContentBufferedPositionMs() {
+ return contentPositionMs == C.TIME_UNSET
+ ? player.getContentBufferedPosition()
+ : contentPositionMs;
+ }
+
+ public long getTotalBufferedDurationMs() {
+ return positionsMs == C.TIME_UNSET ? player.getTotalBufferedDuration() : 0;
+ }
+ }
+}
diff --git a/libraries/common/src/test/java/androidx/media3/common/ForwardingSimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/ForwardingSimpleBasePlayerTest.java
new file mode 100644
index 0000000000..8def2784d5
--- /dev/null
+++ b/libraries/common/src/test/java/androidx/media3/common/ForwardingSimpleBasePlayerTest.java
@@ -0,0 +1,1611 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.common;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.graphics.SurfaceTexture;
+import android.os.Looper;
+import android.view.Surface;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import androidx.annotation.Nullable;
+import androidx.media3.common.text.Cue;
+import androidx.media3.common.text.CueGroup;
+import androidx.media3.common.util.Size;
+import androidx.media3.test.utils.FakeMetadataEntry;
+import androidx.media3.test.utils.TestUtil;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.ShadowSurfaceView;
+
+/** Unit test for {@link ForwardingSimpleBasePlayer}. */
+@RunWith(AndroidJUnit4.class)
+public final class ForwardingSimpleBasePlayerTest {
+
+ @Test
+ public void forwardingSimpleBasePlayer_overridesAllSimpleBasePlayerMethods() throws Exception {
+ // Check with reflection that ForwardingSimpleBasePlayer overrides all overridable
+ // SimpleBasePlayer methods. This guards against accidentally forgetting to forward a method.
+ for (Method method : SimpleBasePlayer.class.getDeclaredMethods()) {
+ int modifiers = method.getModifiers();
+ if (!Modifier.isPrivate(modifiers)
+ && !Modifier.isFinal(modifiers)
+ && !method.isSynthetic()
+ && !method.getName().equals("getPlaceholderState")
+ && !method.getName().equals("getPlaceholderMediaItemData")) {
+ assertThat(
+ ForwardingSimpleBasePlayer.class
+ .getDeclaredMethod(method.getName(), method.getParameterTypes())
+ .getDeclaringClass())
+ .isEqualTo(ForwardingSimpleBasePlayer.class);
+ }
+ }
+ }
+
+ @Test
+ public void getterMethods_noOtherMethodCalls_returnCurrentStateFromWrappedPlayer() {
+ Player.Commands commands =
+ new Player.Commands.Builder()
+ .addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE)
+ .build();
+ PlaybackException error =
+ new PlaybackException(
+ /* message= */ null, /* cause= */ null, PlaybackException.ERROR_CODE_DECODING_FAILED);
+ PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 2f);
+ TrackSelectionParameters trackSelectionParameters =
+ TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT
+ .buildUpon()
+ .setMaxVideoBitrate(1000)
+ .build();
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build();
+ VideoSize videoSize = new VideoSize(/* width= */ 200, /* height= */ 400);
+ CueGroup cueGroup =
+ new CueGroup(
+ ImmutableList.of(new Cue.Builder().setText("text").build()),
+ /* presentationTimeUs= */ 123);
+ DeviceInfo deviceInfo =
+ new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL).setMaxVolume(7).build();
+ MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build();
+ SimpleBasePlayer.PositionSupplier contentPositionSupplier = () -> 456;
+ SimpleBasePlayer.PositionSupplier contentBufferedPositionSupplier = () -> 499;
+ SimpleBasePlayer.PositionSupplier totalBufferedPositionSupplier = () -> 567;
+ Object mediaItemUid = new Object();
+ Object periodUid = new Object();
+ Tracks tracks =
+ new Tracks(
+ ImmutableList.of(
+ new Tracks.Group(
+ new TrackGroup(new Format.Builder().build()),
+ /* adaptiveSupported= */ true,
+ /* trackSupport= */ new int[] {C.FORMAT_HANDLED},
+ /* trackSelected= */ new boolean[] {true})));
+ MediaItem mediaItem = new MediaItem.Builder().setMediaId("id").build();
+ MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build();
+ Object manifest = new Object();
+ Size surfaceSize = new Size(480, 360);
+ MediaItem.LiveConfiguration liveConfiguration =
+ new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(2000).build();
+ ImmutableList playlist =
+ ImmutableList.of(
+ new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(),
+ new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid)
+ .setTracks(tracks)
+ .setMediaItem(mediaItem)
+ .setMediaMetadata(mediaMetadata)
+ .setManifest(manifest)
+ .setLiveConfiguration(liveConfiguration)
+ .setPresentationStartTimeMs(12)
+ .setWindowStartTimeMs(23)
+ .setElapsedRealtimeEpochOffsetMs(10234)
+ .setIsSeekable(true)
+ .setIsDynamic(true)
+ .setDefaultPositionUs(456_789)
+ .setDurationUs(500_000)
+ .setPositionInFirstPeriodUs(100_000)
+ .setIsPlaceholder(true)
+ .setPeriods(
+ ImmutableList.of(
+ new SimpleBasePlayer.PeriodData.Builder(periodUid)
+ .setIsPlaceholder(true)
+ .setDurationUs(600_000)
+ .setAdPlaybackState(
+ new AdPlaybackState(
+ /* adsId= */ new Object(), /* adGroupTimesUs...= */ 555, 666))
+ .build()))
+ .build());
+ SimpleBasePlayer.State state =
+ new SimpleBasePlayer.State.Builder()
+ .setAvailableCommands(commands)
+ .setPlayWhenReady(
+ /* playWhenReady= */ true,
+ /* playWhenReadyChangeReason= */ Player
+ .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS)
+ .setPlaybackState(Player.STATE_IDLE)
+ .setPlaybackSuppressionReason(
+ Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS)
+ .setPlayerError(error)
+ .setRepeatMode(Player.REPEAT_MODE_ALL)
+ .setShuffleModeEnabled(true)
+ .setIsLoading(false)
+ .setSeekBackIncrementMs(5000)
+ .setSeekForwardIncrementMs(4000)
+ .setMaxSeekToPreviousPositionMs(3000)
+ .setPlaybackParameters(playbackParameters)
+ .setTrackSelectionParameters(trackSelectionParameters)
+ .setAudioAttributes(audioAttributes)
+ .setVolume(0.5f)
+ .setVideoSize(videoSize)
+ .setCurrentCues(cueGroup)
+ .setDeviceInfo(deviceInfo)
+ .setDeviceVolume(5)
+ .setIsDeviceMuted(true)
+ .setSurfaceSize(surfaceSize)
+ .setPlaylist(playlist)
+ .setPlaylistMetadata(playlistMetadata)
+ .setCurrentMediaItemIndex(1)
+ .setContentPositionMs(contentPositionSupplier)
+ .setContentBufferedPositionMs(contentBufferedPositionSupplier)
+ .setTotalBufferedDurationMs(totalBufferedPositionSupplier)
+ .build();
+ Player wrappedPlayer =
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return state;
+ }
+ };
+
+ Player forwardingPlayer = new ForwardingPlayer(wrappedPlayer);
+
+ assertThat(forwardingPlayer.getApplicationLooper()).isEqualTo(Looper.myLooper());
+ assertThat(forwardingPlayer.getAvailableCommands()).isEqualTo(commands);
+ assertThat(forwardingPlayer.getPlayWhenReady()).isTrue();
+ assertThat(forwardingPlayer.getPlaybackState()).isEqualTo(Player.STATE_IDLE);
+ assertThat(forwardingPlayer.getPlaybackSuppressionReason())
+ .isEqualTo(Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS);
+ assertThat(forwardingPlayer.getPlayerError()).isEqualTo(error);
+ assertThat(forwardingPlayer.getRepeatMode()).isEqualTo(Player.REPEAT_MODE_ALL);
+ assertThat(forwardingPlayer.getShuffleModeEnabled()).isTrue();
+ assertThat(forwardingPlayer.isLoading()).isFalse();
+ assertThat(forwardingPlayer.getSeekBackIncrement()).isEqualTo(5000);
+ assertThat(forwardingPlayer.getSeekForwardIncrement()).isEqualTo(4000);
+ assertThat(forwardingPlayer.getMaxSeekToPreviousPosition()).isEqualTo(3000);
+ assertThat(forwardingPlayer.getPlaybackParameters()).isEqualTo(playbackParameters);
+ assertThat(forwardingPlayer.getCurrentTracks()).isEqualTo(tracks);
+ assertThat(forwardingPlayer.getTrackSelectionParameters()).isEqualTo(trackSelectionParameters);
+ assertThat(forwardingPlayer.getMediaMetadata()).isEqualTo(mediaMetadata);
+ assertThat(forwardingPlayer.getPlaylistMetadata()).isEqualTo(playlistMetadata);
+ assertThat(forwardingPlayer.getCurrentPeriodIndex()).isEqualTo(1);
+ assertThat(forwardingPlayer.getCurrentMediaItemIndex()).isEqualTo(1);
+ assertThat(forwardingPlayer.getDuration()).isEqualTo(500);
+ assertThat(forwardingPlayer.getCurrentPosition()).isEqualTo(456);
+ assertThat(forwardingPlayer.getBufferedPosition()).isEqualTo(499);
+ assertThat(forwardingPlayer.getTotalBufferedDuration()).isEqualTo(567);
+ assertThat(forwardingPlayer.isPlayingAd()).isFalse();
+ assertThat(forwardingPlayer.getCurrentAdGroupIndex()).isEqualTo(C.INDEX_UNSET);
+ assertThat(forwardingPlayer.getCurrentAdIndexInAdGroup()).isEqualTo(C.INDEX_UNSET);
+ assertThat(forwardingPlayer.getContentPosition()).isEqualTo(456);
+ assertThat(forwardingPlayer.getContentBufferedPosition()).isEqualTo(499);
+ assertThat(forwardingPlayer.getAudioAttributes()).isEqualTo(audioAttributes);
+ assertThat(forwardingPlayer.getVolume()).isEqualTo(0.5f);
+ assertThat(forwardingPlayer.getVideoSize()).isEqualTo(videoSize);
+ assertThat(forwardingPlayer.getCurrentCues()).isEqualTo(cueGroup);
+ assertThat(forwardingPlayer.getDeviceInfo()).isEqualTo(deviceInfo);
+ assertThat(forwardingPlayer.getDeviceVolume()).isEqualTo(5);
+ assertThat(forwardingPlayer.isDeviceMuted()).isTrue();
+ assertThat(forwardingPlayer.getSurfaceSize()).isEqualTo(surfaceSize);
+ Timeline timeline = forwardingPlayer.getCurrentTimeline();
+ assertThat(timeline.getPeriodCount()).isEqualTo(2);
+ assertThat(timeline.getWindowCount()).isEqualTo(2);
+ Timeline.Window window = timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window());
+ assertThat(window.defaultPositionUs).isEqualTo(0);
+ assertThat(window.durationUs).isEqualTo(C.TIME_UNSET);
+ assertThat(window.elapsedRealtimeEpochOffsetMs).isEqualTo(C.TIME_UNSET);
+ assertThat(window.firstPeriodIndex).isEqualTo(0);
+ assertThat(window.isDynamic).isFalse();
+ assertThat(window.isPlaceholder).isFalse();
+ assertThat(window.isSeekable).isFalse();
+ assertThat(window.lastPeriodIndex).isEqualTo(0);
+ assertThat(window.positionInFirstPeriodUs).isEqualTo(0);
+ assertThat(window.presentationStartTimeMs).isEqualTo(C.TIME_UNSET);
+ assertThat(window.windowStartTimeMs).isEqualTo(C.TIME_UNSET);
+ assertThat(window.liveConfiguration).isNull();
+ assertThat(window.manifest).isNull();
+ assertThat(window.mediaItem).isEqualTo(MediaItem.EMPTY);
+ window = timeline.getWindow(/* windowIndex= */ 1, new Timeline.Window());
+ assertThat(window.defaultPositionUs).isEqualTo(456_789);
+ assertThat(window.durationUs).isEqualTo(500_000);
+ assertThat(window.elapsedRealtimeEpochOffsetMs).isEqualTo(10234);
+ assertThat(window.firstPeriodIndex).isEqualTo(1);
+ assertThat(window.isDynamic).isTrue();
+ assertThat(window.isPlaceholder).isTrue();
+ assertThat(window.isSeekable).isTrue();
+ assertThat(window.lastPeriodIndex).isEqualTo(1);
+ assertThat(window.positionInFirstPeriodUs).isEqualTo(100_000);
+ assertThat(window.presentationStartTimeMs).isEqualTo(12);
+ assertThat(window.windowStartTimeMs).isEqualTo(23);
+ assertThat(window.liveConfiguration).isEqualTo(liveConfiguration);
+ assertThat(window.manifest).isEqualTo(manifest);
+ assertThat(window.mediaItem).isEqualTo(mediaItem);
+ assertThat(window.uid).isEqualTo(mediaItemUid);
+ Timeline.Period period =
+ timeline.getPeriod(/* periodIndex= */ 0, new Timeline.Period(), /* setIds= */ true);
+ assertThat(period.durationUs).isEqualTo(C.TIME_UNSET);
+ assertThat(period.isPlaceholder).isFalse();
+ assertThat(period.positionInWindowUs).isEqualTo(0);
+ assertThat(period.windowIndex).isEqualTo(0);
+ assertThat(period.getAdGroupCount()).isEqualTo(0);
+ period = timeline.getPeriod(/* periodIndex= */ 1, new Timeline.Period(), /* setIds= */ true);
+ assertThat(period.durationUs).isEqualTo(600_000);
+ assertThat(period.isPlaceholder).isTrue();
+ assertThat(period.positionInWindowUs).isEqualTo(-100_000);
+ assertThat(period.windowIndex).isEqualTo(1);
+ assertThat(period.id).isEqualTo(periodUid);
+ assertThat(period.getAdGroupCount()).isEqualTo(2);
+ assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 0)).isEqualTo(555);
+ assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 1)).isEqualTo(666);
+ }
+
+ @SuppressWarnings("deprecation") // Verifying deprecated listener call.
+ @Test
+ public void stateChangesInWrappedPlayer_areForwardedToListeners() throws Exception {
+ Object mediaItemUid0 = new Object();
+ MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build();
+ SimpleBasePlayer.MediaItemData mediaItemData0 =
+ new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid0).setMediaItem(mediaItem0).build();
+ SimpleBasePlayer.State state1 =
+ new SimpleBasePlayer.State.Builder()
+ .setAvailableCommands(
+ new Player.Commands.Builder()
+ .addAllCommands()
+ .remove(Player.COMMAND_SET_MEDIA_ITEM)
+ .build())
+ .setPlayWhenReady(
+ /* playWhenReady= */ true,
+ /* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
+ .setPlaybackState(Player.STATE_READY)
+ .setPlaybackSuppressionReason(Player.PLAYBACK_SUPPRESSION_REASON_NONE)
+ .setPlayerError(null)
+ .setRepeatMode(Player.REPEAT_MODE_ONE)
+ .setShuffleModeEnabled(false)
+ .setIsLoading(true)
+ .setSeekBackIncrementMs(7000)
+ .setSeekForwardIncrementMs(2000)
+ .setMaxSeekToPreviousPositionMs(8000)
+ .setPlaybackParameters(PlaybackParameters.DEFAULT)
+ .setTrackSelectionParameters(TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT)
+ .setAudioAttributes(AudioAttributes.DEFAULT)
+ .setVolume(1f)
+ .setVideoSize(VideoSize.UNKNOWN)
+ .setCurrentCues(CueGroup.EMPTY_TIME_ZERO)
+ .setDeviceInfo(DeviceInfo.UNKNOWN)
+ .setDeviceVolume(0)
+ .setIsDeviceMuted(false)
+ .setPlaylist(ImmutableList.of(mediaItemData0))
+ .setPlaylistMetadata(MediaMetadata.EMPTY)
+ .setCurrentMediaItemIndex(0)
+ .setContentPositionMs(8_000)
+ .build();
+ Object mediaItemUid1 = new Object();
+ MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build();
+ MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build();
+ Player.Commands commands =
+ new Player.Commands.Builder()
+ .addAllCommands()
+ .remove(Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS)
+ .build();
+ Tracks tracks =
+ new Tracks(
+ ImmutableList.of(
+ new Tracks.Group(
+ new TrackGroup(new Format.Builder().build()),
+ /* adaptiveSupported= */ true,
+ /* trackSupport= */ new int[] {C.FORMAT_HANDLED},
+ /* trackSelected= */ new boolean[] {true})));
+ SimpleBasePlayer.MediaItemData mediaItemData1 =
+ new SimpleBasePlayer.MediaItemData.Builder(mediaItemUid1)
+ .setMediaItem(mediaItem1)
+ .setMediaMetadata(mediaMetadata)
+ .setTracks(tracks)
+ .build();
+ PlaybackException error =
+ new PlaybackException(
+ /* message= */ null, /* cause= */ null, PlaybackException.ERROR_CODE_DECODING_FAILED);
+ PlaybackParameters playbackParameters = new PlaybackParameters(/* speed= */ 2f);
+ TrackSelectionParameters trackSelectionParameters =
+ TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT
+ .buildUpon()
+ .setMaxVideoBitrate(1000)
+ .build();
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build();
+ VideoSize videoSize = new VideoSize(/* width= */ 200, /* height= */ 400);
+ CueGroup cueGroup =
+ new CueGroup(
+ ImmutableList.of(new Cue.Builder().setText("text").build()),
+ /* presentationTimeUs= */ 123);
+ Metadata timedMetadata =
+ new Metadata(/* presentationTimeUs= */ 42, new FakeMetadataEntry("data"));
+ Size surfaceSize = new Size(480, 360);
+ DeviceInfo deviceInfo =
+ new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL).setMaxVolume(7).build();
+ MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build();
+ SimpleBasePlayer.State state2 =
+ new SimpleBasePlayer.State.Builder()
+ .setAvailableCommands(commands)
+ .setPlayWhenReady(
+ /* playWhenReady= */ false,
+ /* playWhenReadyChangeReason= */ Player
+ .PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS)
+ .setPlaybackState(Player.STATE_IDLE)
+ .setPlaybackSuppressionReason(
+ Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS)
+ .setPlayerError(error)
+ .setRepeatMode(Player.REPEAT_MODE_ALL)
+ .setShuffleModeEnabled(true)
+ .setIsLoading(false)
+ .setSeekBackIncrementMs(5000)
+ .setSeekForwardIncrementMs(4000)
+ .setMaxSeekToPreviousPositionMs(3000)
+ .setPlaybackParameters(playbackParameters)
+ .setTrackSelectionParameters(trackSelectionParameters)
+ .setAudioAttributes(audioAttributes)
+ .setVolume(0.5f)
+ .setVideoSize(videoSize)
+ .setCurrentCues(cueGroup)
+ .setDeviceInfo(deviceInfo)
+ .setDeviceVolume(5)
+ .setIsDeviceMuted(true)
+ .setSurfaceSize(surfaceSize)
+ .setNewlyRenderedFirstFrame(true)
+ .setTimedMetadata(timedMetadata)
+ .setPlaylist(ImmutableList.of(mediaItemData0, mediaItemData1))
+ .setPlaylistMetadata(playlistMetadata)
+ .setCurrentMediaItemIndex(1)
+ .setContentPositionMs(12_000)
+ .setPositionDiscontinuity(
+ Player.DISCONTINUITY_REASON_SEEK, /* discontinuityPositionMs= */ 11_500)
+ .build();
+ AtomicBoolean returnState2 = new AtomicBoolean();
+ SimpleBasePlayer innerPlayer =
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return returnState2.get() ? state2 : state1;
+ }
+ };
+ ForwardingSimpleBasePlayer forwardingPlayer = new ForwardingSimpleBasePlayer(innerPlayer);
+ // Ensure state1 is used.
+ assertThat(forwardingPlayer.getPlayWhenReady()).isTrue();
+ Player.Listener listener = mock(Player.Listener.class);
+ forwardingPlayer.addListener(listener);
+
+ returnState2.set(true);
+ innerPlayer.invalidateState();
+ // Idle Looper to ensure all callbacks (including onEvents) are delivered.
+ ShadowLooper.idleMainLooper();
+
+ verify(listener).onAvailableCommandsChanged(commands);
+ verify(listener)
+ .onPlayWhenReadyChanged(
+ /* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS);
+ verify(listener)
+ .onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE);
+ verify(listener).onPlaybackStateChanged(Player.STATE_IDLE);
+ verify(listener)
+ .onPlaybackSuppressionReasonChanged(
+ Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS);
+ verify(listener).onIsPlayingChanged(false);
+ verify(listener).onPlayerError(error);
+ verify(listener).onPlayerErrorChanged(error);
+ verify(listener).onRepeatModeChanged(Player.REPEAT_MODE_ALL);
+ verify(listener).onShuffleModeEnabledChanged(true);
+ verify(listener).onLoadingChanged(false);
+ verify(listener).onIsLoadingChanged(false);
+ verify(listener).onSeekBackIncrementChanged(5000);
+ verify(listener).onSeekForwardIncrementChanged(4000);
+ verify(listener).onMaxSeekToPreviousPositionChanged(3000);
+ verify(listener).onPlaybackParametersChanged(playbackParameters);
+ verify(listener).onTrackSelectionParametersChanged(trackSelectionParameters);
+ verify(listener).onAudioAttributesChanged(audioAttributes);
+ verify(listener).onVolumeChanged(0.5f);
+ verify(listener).onVideoSizeChanged(videoSize);
+ verify(listener).onCues(cueGroup.cues);
+ verify(listener).onCues(cueGroup);
+ verify(listener).onDeviceInfoChanged(deviceInfo);
+ verify(listener).onDeviceVolumeChanged(/* volume= */ 5, /* muted= */ true);
+ verify(listener)
+ .onTimelineChanged(state2.timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
+ verify(listener).onMediaMetadataChanged(mediaMetadata);
+ verify(listener).onTracksChanged(tracks);
+ verify(listener).onPlaylistMetadataChanged(playlistMetadata);
+ verify(listener).onRenderedFirstFrame();
+ verify(listener).onMetadata(timedMetadata);
+ verify(listener).onSurfaceSizeChanged(surfaceSize.getWidth(), surfaceSize.getHeight());
+ verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
+ verify(listener)
+ .onPositionDiscontinuity(
+ /* oldPosition= */ new Player.PositionInfo(
+ mediaItemUid0,
+ /* mediaItemIndex= */ 0,
+ mediaItem0,
+ /* periodUid= */ mediaItemUid0,
+ /* periodIndex= */ 0,
+ /* positionMs= */ 8_000,
+ /* contentPositionMs= */ 8_000,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET),
+ /* newPosition= */ new Player.PositionInfo(
+ mediaItemUid1,
+ /* mediaItemIndex= */ 1,
+ mediaItem1,
+ /* periodUid= */ mediaItemUid1,
+ /* periodIndex= */ 1,
+ /* positionMs= */ 11_500,
+ /* contentPositionMs= */ 11_500,
+ /* adGroupIndex= */ C.INDEX_UNSET,
+ /* adIndexInAdGroup= */ C.INDEX_UNSET),
+ Player.DISCONTINUITY_REASON_SEEK);
+ verify(listener).onMediaItemTransition(mediaItem1, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK);
+ verify(listener)
+ .onEvents(
+ forwardingPlayer,
+ new Player.Events(
+ new FlagSet.Builder()
+ .addAll(
+ Player.EVENT_TIMELINE_CHANGED,
+ Player.EVENT_MEDIA_ITEM_TRANSITION,
+ Player.EVENT_TRACKS_CHANGED,
+ Player.EVENT_IS_LOADING_CHANGED,
+ Player.EVENT_PLAYBACK_STATE_CHANGED,
+ Player.EVENT_PLAY_WHEN_READY_CHANGED,
+ Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED,
+ Player.EVENT_IS_PLAYING_CHANGED,
+ Player.EVENT_REPEAT_MODE_CHANGED,
+ Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
+ Player.EVENT_PLAYER_ERROR,
+ Player.EVENT_POSITION_DISCONTINUITY,
+ Player.EVENT_PLAYBACK_PARAMETERS_CHANGED,
+ Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
+ Player.EVENT_MEDIA_METADATA_CHANGED,
+ Player.EVENT_PLAYLIST_METADATA_CHANGED,
+ Player.EVENT_SEEK_BACK_INCREMENT_CHANGED,
+ Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED,
+ Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED,
+ Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED,
+ Player.EVENT_AUDIO_ATTRIBUTES_CHANGED,
+ Player.EVENT_VOLUME_CHANGED,
+ Player.EVENT_SURFACE_SIZE_CHANGED,
+ Player.EVENT_VIDEO_SIZE_CHANGED,
+ Player.EVENT_RENDERED_FIRST_FRAME,
+ Player.EVENT_CUES,
+ Player.EVENT_METADATA,
+ Player.EVENT_DEVICE_INFO_CHANGED,
+ Player.EVENT_DEVICE_VOLUME_CHANGED)
+ .build()));
+ verifyNoMoreInteractions(listener);
+ // Assert that we actually called all listeners. This guards against forgetting a State setter
+ // when forwarding state in ForwardingSimpleBasePlayer.getState().
+ for (Method method : TestUtil.getPublicMethods(Player.Listener.class)) {
+ if (method.getName().equals("onAudioSessionIdChanged")
+ || method.getName().equals("onSkipSilenceEnabledChanged")) {
+ // Skip listeners for ExoPlayer-specific states
+ continue;
+ }
+ method.invoke(verify(listener), getAnyArguments(method));
+ }
+ }
+
+ @Test
+ public void setPlayWhenReady_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetPlayWhenReady(boolean playWhenReady) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.setPlayWhenReady(true);
+ forwardingPlayer.play();
+ forwardingPlayer.pause();
+
+ verify(wrappedPlayer, times(2)).setPlayWhenReady(true);
+ verify(wrappedPlayer).setPlayWhenReady(false);
+ }
+
+ @Test
+ public void prepare_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_PREPARE).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handlePrepare() {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.prepare();
+
+ verify(wrappedPlayer).prepare();
+ }
+
+ @Test
+ public void stop_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_STOP).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleStop() {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.stop();
+
+ verify(wrappedPlayer).stop();
+ }
+
+ @Test
+ public void release_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_RELEASE).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleRelease() {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.release();
+
+ verify(wrappedPlayer).release();
+ }
+
+ @Test
+ public void setRepeatMode_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_SET_REPEAT_MODE).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetRepeatMode(
+ @Player.RepeatMode int repeatMode) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
+
+ verify(wrappedPlayer).setRepeatMode(Player.REPEAT_MODE_ONE);
+ }
+
+ @Test
+ public void setShuffleModeEnabled_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_SET_SHUFFLE_MODE).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetShuffleModeEnabled(
+ boolean shuffleModeEnabled) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.setShuffleModeEnabled(true);
+
+ verify(wrappedPlayer).setShuffleModeEnabled(true);
+ }
+
+ @Test
+ public void setPlaybackParameters_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_SET_SPEED_AND_PITCH).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetPlaybackParameters(
+ PlaybackParameters playbackParameters) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.setPlaybackParameters(
+ new PlaybackParameters(/* speed= */ 2f, /* pitch= */ 3f));
+ forwardingPlayer.setPlaybackSpeed(5f);
+
+ verify(wrappedPlayer)
+ .setPlaybackParameters(new PlaybackParameters(/* speed= */ 2f, /* pitch= */ 3f));
+ verify(wrappedPlayer).setPlaybackParameters(new PlaybackParameters(/* speed= */ 5f));
+ }
+
+ @Test
+ public void setTrackSelectionParameters_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .add(Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS)
+ .build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetTrackSelectionParameters(
+ TrackSelectionParameters trackSelectionParameters) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+ TrackSelectionParameters parameters =
+ new TrackSelectionParameters.Builder(ApplicationProvider.getApplicationContext())
+ .setMaxVideoBitrate(1000)
+ .build();
+
+ forwardingPlayer.setTrackSelectionParameters(parameters);
+
+ verify(wrappedPlayer).setTrackSelectionParameters(parameters);
+ }
+
+ @Test
+ public void setPlaylistMetadata_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .add(Player.COMMAND_SET_PLAYLIST_METADATA)
+ .build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetPlaylistMetadata(
+ MediaMetadata playlistMetadata) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+ MediaMetadata metadata = new MediaMetadata.Builder().setTitle("title").build();
+
+ forwardingPlayer.setPlaylistMetadata(metadata);
+
+ verify(wrappedPlayer).setPlaylistMetadata(metadata);
+ }
+
+ @Test
+ public void setVolume_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_SET_VOLUME).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetVolume(float volume) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.setVolume(0.5f);
+
+ verify(wrappedPlayer).setVolume(0.5f);
+ }
+
+ @Test
+ public void setDeviceVolume_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .add(Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS)
+ .build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetDeviceVolume(int deviceVolume, int flags) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.setDeviceVolume(50, C.VOLUME_FLAG_SHOW_UI);
+
+ verify(wrappedPlayer).setDeviceVolume(50, C.VOLUME_FLAG_SHOW_UI);
+ }
+
+ @Test
+ public void increaseDeviceVolume_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .add(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)
+ .build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleIncreaseDeviceVolume(
+ @C.VolumeFlags int flags) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.increaseDeviceVolume(C.VOLUME_FLAG_PLAY_SOUND);
+
+ verify(wrappedPlayer).increaseDeviceVolume(C.VOLUME_FLAG_PLAY_SOUND);
+ }
+
+ @Test
+ public void decreaseDeviceVolume_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .add(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)
+ .build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleDecreaseDeviceVolume(
+ @C.VolumeFlags int flags) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.decreaseDeviceVolume(C.VOLUME_FLAG_PLAY_SOUND);
+
+ verify(wrappedPlayer).decreaseDeviceVolume(C.VOLUME_FLAG_PLAY_SOUND);
+ }
+
+ @Test
+ public void setDeviceMuted_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .add(Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS)
+ .build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetDeviceMuted(
+ boolean muted, @C.VolumeFlags int flags) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.setDeviceMuted(true, C.VOLUME_FLAG_PLAY_SOUND);
+
+ verify(wrappedPlayer).setDeviceMuted(true, C.VOLUME_FLAG_PLAY_SOUND);
+ }
+
+ @Test
+ public void setAudioAttributes_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_SET_AUDIO_ATTRIBUTES).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetAudioAttributes(
+ AudioAttributes audioAttributes, boolean handleAudioFocus) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+ AudioAttributes audioAttributes =
+ new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).build();
+
+ forwardingPlayer.setAudioAttributes(audioAttributes, /* handleAudioFocus= */ true);
+
+ verify(wrappedPlayer).setAudioAttributes(audioAttributes, /* handleAudioFocus= */ true);
+ }
+
+ @Test
+ public void setVideoSurface_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_SET_VIDEO_SURFACE).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetVideoOutput(Object videoOutput) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+ Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0));
+ TextureView textureView = new TextureView(ApplicationProvider.getApplicationContext());
+ SurfaceView surfaceView = new SurfaceView(ApplicationProvider.getApplicationContext());
+ ShadowSurfaceView.FakeSurfaceHolder surfaceHolder = new ShadowSurfaceView.FakeSurfaceHolder();
+
+ forwardingPlayer.setVideoSurface(surface);
+ forwardingPlayer.setVideoTextureView(textureView);
+ forwardingPlayer.setVideoSurfaceView(surfaceView);
+ forwardingPlayer.setVideoSurfaceHolder(surfaceHolder);
+
+ verify(wrappedPlayer).setVideoSurface(surface);
+ verify(wrappedPlayer).setVideoTextureView(textureView);
+ verify(wrappedPlayer).setVideoSurfaceView(surfaceView);
+ verify(wrappedPlayer).setVideoSurfaceHolder(surfaceHolder);
+ }
+
+ @Test
+ public void clearVideoSurface_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_SET_VIDEO_SURFACE).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleClearVideoOutput(
+ @Nullable Object videoOutput) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+ Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0));
+ TextureView textureView = new TextureView(ApplicationProvider.getApplicationContext());
+ SurfaceView surfaceView = new SurfaceView(ApplicationProvider.getApplicationContext());
+ ShadowSurfaceView.FakeSurfaceHolder surfaceHolder = new ShadowSurfaceView.FakeSurfaceHolder();
+
+ forwardingPlayer.clearVideoSurface();
+ forwardingPlayer.clearVideoSurface(surface);
+ forwardingPlayer.clearVideoTextureView(textureView);
+ forwardingPlayer.clearVideoSurfaceView(surfaceView);
+ forwardingPlayer.clearVideoSurfaceHolder(surfaceHolder);
+
+ verify(wrappedPlayer).clearVideoSurface();
+ verify(wrappedPlayer).clearVideoSurface(surface);
+ verify(wrappedPlayer).clearVideoTextureView(textureView);
+ verify(wrappedPlayer).clearVideoSurfaceView(surfaceView);
+ verify(wrappedPlayer).clearVideoSurfaceHolder(surfaceHolder);
+ }
+
+ @Test
+ public void setMediaItems_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .addAll(
+ Player.COMMAND_SET_MEDIA_ITEM,
+ Player.COMMAND_CHANGE_MEDIA_ITEMS)
+ .build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetMediaItems(
+ List mediaItems, int startIndex, long startPositionMs) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+ MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build();
+ MediaItem mediaItem2 = new MediaItem.Builder().setMediaId("2").build();
+
+ forwardingPlayer.setMediaItem(mediaItem1);
+ forwardingPlayer.setMediaItem(mediaItem1, /* startPositionMs= */ 500);
+ forwardingPlayer.setMediaItems(ImmutableList.of(mediaItem1, mediaItem2));
+ forwardingPlayer.setMediaItems(
+ ImmutableList.of(mediaItem1, mediaItem2), /* startIndex= */ 1, /* startPositionMs= */ 600);
+
+ verify(wrappedPlayer).setMediaItem(mediaItem1);
+ verify(wrappedPlayer).setMediaItem(mediaItem1, /* startPositionMs= */ 500);
+ verify(wrappedPlayer).setMediaItems(ImmutableList.of(mediaItem1, mediaItem2));
+ verify(wrappedPlayer)
+ .setMediaItems(
+ ImmutableList.of(mediaItem1, mediaItem2),
+ /* startIndex= */ 1,
+ /* startPositionMs= */ 600);
+ }
+
+ @Test
+ public void addMediaItems_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .addAll(
+ Player.COMMAND_CHANGE_MEDIA_ITEMS, Player.COMMAND_GET_TIMELINE)
+ .build())
+ .setPlaylist(
+ ImmutableList.of(
+ new MediaItemData.Builder(new Object()).build(),
+ new MediaItemData.Builder(new Object()).build()))
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleAddMediaItems(
+ int index, List mediaItems) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+ MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build();
+ MediaItem mediaItem2 = new MediaItem.Builder().setMediaId("2").build();
+
+ forwardingPlayer.addMediaItem(/* index= */ 1, mediaItem1);
+ forwardingPlayer.addMediaItems(/* index= */ 2, ImmutableList.of(mediaItem1, mediaItem2));
+
+ verify(wrappedPlayer).addMediaItem(/* index= */ 1, mediaItem1);
+ verify(wrappedPlayer).addMediaItems(/* index= */ 2, ImmutableList.of(mediaItem1, mediaItem2));
+ }
+
+ @Test
+ public void moveMediaItems_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .addAll(
+ Player.COMMAND_CHANGE_MEDIA_ITEMS, Player.COMMAND_GET_TIMELINE)
+ .build())
+ .setPlaylist(
+ ImmutableList.of(
+ new MediaItemData.Builder(new Object()).build(),
+ new MediaItemData.Builder(new Object()).build(),
+ new MediaItemData.Builder(new Object()).build()))
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleMoveMediaItems(
+ int fromIndex, int toIndex, int newIndex) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 0);
+ forwardingPlayer.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 1);
+
+ verify(wrappedPlayer).moveMediaItem(/* currentIndex= */ 1, /* newIndex= */ 0);
+ verify(wrappedPlayer).moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2, /* newIndex= */ 1);
+ }
+
+ @Test
+ public void replaceMediaItems_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .addAll(
+ Player.COMMAND_CHANGE_MEDIA_ITEMS, Player.COMMAND_GET_TIMELINE)
+ .build())
+ .setPlaylist(
+ ImmutableList.of(
+ new MediaItemData.Builder(new Object()).build(),
+ new MediaItemData.Builder(new Object()).build(),
+ new MediaItemData.Builder(new Object()).build()))
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleReplaceMediaItems(
+ int fromIndex, int toIndex, List mediaItems) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+ MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build();
+ MediaItem mediaItem2 = new MediaItem.Builder().setMediaId("2").build();
+
+ forwardingPlayer.replaceMediaItem(/* index= */ 1, mediaItem1);
+ forwardingPlayer.replaceMediaItems(
+ /* fromIndex= */ 0, /* toIndex= */ 2, ImmutableList.of(mediaItem1, mediaItem2));
+
+ verify(wrappedPlayer).replaceMediaItem(/* index= */ 1, mediaItem1);
+ verify(wrappedPlayer)
+ .replaceMediaItems(
+ /* fromIndex= */ 0, /* toIndex= */ 2, ImmutableList.of(mediaItem1, mediaItem2));
+ }
+
+ @Test
+ public void removeMediaItems_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .addAll(
+ Player.COMMAND_CHANGE_MEDIA_ITEMS, Player.COMMAND_GET_TIMELINE)
+ .build())
+ .setPlaylist(
+ ImmutableList.of(
+ new MediaItemData.Builder(new Object()).build(),
+ new MediaItemData.Builder(new Object()).build(),
+ new MediaItemData.Builder(new Object()).build()))
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleRemoveMediaItems(int fromIndex, int toIndex) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.removeMediaItem(/* index= */ 1);
+ forwardingPlayer.removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2);
+
+ verify(wrappedPlayer).removeMediaItem(/* index= */ 1);
+ verify(wrappedPlayer).removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ 2);
+ }
+
+ @Test
+ public void seekBack_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_SEEK_BACK).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSeek(
+ int mediaItemIndex, long positionMs, @Command int seekCommand) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.seekBack();
+
+ verify(wrappedPlayer).seekBack();
+ }
+
+ @Test
+ public void seekForward_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_SEEK_FORWARD).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSeek(
+ int mediaItemIndex, long positionMs, @Command int seekCommand) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.seekForward();
+
+ verify(wrappedPlayer).seekForward();
+ }
+
+ @Test
+ public void seekInCurrentItem_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .add(Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)
+ .build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSeek(
+ int mediaItemIndex, long positionMs, @Command int seekCommand) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.seekTo(/* positionMs= */ 5000);
+
+ verify(wrappedPlayer).seekTo(/* positionMs= */ 5000);
+ }
+
+ @Test
+ public void seekToDefaultPosition_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .add(Player.COMMAND_SEEK_TO_DEFAULT_POSITION)
+ .build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSeek(
+ int mediaItemIndex, long positionMs, @Command int seekCommand) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.seekToDefaultPosition();
+
+ verify(wrappedPlayer).seekToDefaultPosition();
+ }
+
+ @Test
+ public void seekToMediaItem_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_SEEK_TO_MEDIA_ITEM).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSeek(
+ int mediaItemIndex, long positionMs, @Command int seekCommand) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 3000);
+
+ verify(wrappedPlayer).seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 3000);
+ }
+
+ @Test
+ public void seekToNext_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_SEEK_TO_NEXT).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSeek(
+ int mediaItemIndex, long positionMs, @Command int seekCommand) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.seekToNext();
+
+ verify(wrappedPlayer).seekToNext();
+ }
+
+ @Test
+ public void seekToPrevious_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder().add(Player.COMMAND_SEEK_TO_PREVIOUS).build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSeek(
+ int mediaItemIndex, long positionMs, @Command int seekCommand) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.seekToPrevious();
+
+ verify(wrappedPlayer).seekToPrevious();
+ }
+
+ @Test
+ public void seekToNextMediaItem_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
+ .build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSeek(
+ int mediaItemIndex, long positionMs, @Command int seekCommand) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.seekToNextMediaItem();
+
+ verify(wrappedPlayer).seekToNextMediaItem();
+ }
+
+ @Test
+ public void seekToPreviousMediaItem_isForwardedToWrappedPlayer() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(
+ new Commands.Builder()
+ .add(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
+ .build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSeek(
+ int mediaItemIndex, long positionMs, @Command int seekCommand) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer = new ForwardingSimpleBasePlayer(wrappedPlayer);
+
+ forwardingPlayer.seekToPreviousMediaItem();
+
+ verify(wrappedPlayer).seekToPreviousMediaItem();
+ }
+
+ @Test
+ public void overrideSetters_forwardsOverriddenCallsOnly() {
+ Player wrappedPlayer =
+ spy(
+ new ForwardingPlayer(
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return new State.Builder()
+ .setAvailableCommands(new Commands.Builder().addAllCommands().build())
+ .build();
+ }
+
+ @Override
+ protected ListenableFuture> handleSeek(
+ int mediaItemIndex, long positionMs, @Command int seekCommand) {
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleSetShuffleModeEnabled(
+ boolean shuffleModeEnabled) {
+ return Futures.immediateVoidFuture();
+ }
+ }));
+ Player forwardingPlayer =
+ new ForwardingSimpleBasePlayer(wrappedPlayer) {
+ @Override
+ protected ListenableFuture> handleSetRepeatMode(int repeatMode) {
+ // Usage variant 1: Directly call player.
+ getPlayer().setShuffleModeEnabled(true);
+ return Futures.immediateVoidFuture();
+ }
+
+ @Override
+ protected ListenableFuture> handleSeek(
+ int mediaItemIndex, long positionMs, int seekCommand) {
+ // Usage variant 2: Call super method with different parameters.
+ return super.handleSeek(
+ /* mediaItemIndex= */ 1, /* positionMs= */ 2000, Player.COMMAND_SEEK_TO_MEDIA_ITEM);
+ }
+ };
+
+ forwardingPlayer.seekToNext();
+ forwardingPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
+
+ verify(wrappedPlayer).seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 2000);
+ verify(wrappedPlayer).setShuffleModeEnabled(true);
+ verify(wrappedPlayer, never()).seekToNext();
+ verify(wrappedPlayer, never()).setRepeatMode(anyInt());
+ }
+
+ @Test
+ public void overrideState_triggersListenersAccordingToOverridenState() {
+ SimpleBasePlayer.State wrappedState1 =
+ new SimpleBasePlayer.State.Builder()
+ .setAvailableCommands(
+ new Player.Commands.Builder().add(Player.COMMAND_GET_TIMELINE).build())
+ .setPlaybackState(Player.STATE_READY)
+ .setPlayWhenReady(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
+ .setPlaylist(
+ ImmutableList.of(new SimpleBasePlayer.MediaItemData.Builder(new Object()).build()))
+ .build();
+ SimpleBasePlayer.State wrappedState2 =
+ wrappedState1.buildUpon().setPlaybackState(Player.STATE_BUFFERING).build();
+ AtomicBoolean returnWrappedState2 = new AtomicBoolean();
+ SimpleBasePlayer wrappedPlayer =
+ new SimpleBasePlayer(Looper.myLooper()) {
+ @Override
+ protected State getState() {
+ return returnWrappedState2.get() ? wrappedState2 : wrappedState1;
+ }
+ };
+ Player forwardingPlayer =
+ new ForwardingSimpleBasePlayer(wrappedPlayer) {
+ @Override
+ protected State getState() {
+ State state = super.getState();
+ if (state.playbackState == Player.STATE_BUFFERING) {
+ // Suppress changes in the wrapped player and add new changes.
+ state =
+ state
+ .buildUpon()
+ .setPlayWhenReady(false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE)
+ .setPlaybackState(Player.STATE_READY)
+ .build();
+ }
+ return state;
+ }
+ };
+ Player.Listener listener = mock(Player.Listener.class);
+ forwardingPlayer.addListener(listener);
+
+ @Player.State int playbackState1 = forwardingPlayer.getPlaybackState();
+ boolean playWhenReady1 = forwardingPlayer.getPlayWhenReady();
+ returnWrappedState2.set(true);
+ wrappedPlayer.invalidateState();
+ // Idle Looper to ensure all callbacks (including onEvents) are delivered.
+ ShadowLooper.idleMainLooper();
+ @Player.State int playbackState2 = forwardingPlayer.getPlaybackState();
+ boolean playWhenReady2 = forwardingPlayer.getPlayWhenReady();
+
+ assertThat(playbackState1).isEqualTo(Player.STATE_READY);
+ assertThat(playbackState2).isEqualTo(Player.STATE_READY);
+ assertThat(playWhenReady1).isTrue();
+ assertThat(playWhenReady2).isFalse();
+ verify(listener, never()).onPlaybackStateChanged(anyInt());
+ verify(listener).onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
+ }
+
+ private static Object[] getAnyArguments(Method method) {
+ Object[] arguments = new Object[method.getParameterCount()];
+ Class>[] argumentTypes = method.getParameterTypes();
+ for (int i = 0; i < arguments.length; i++) {
+ if (argumentTypes[i].equals(Integer.TYPE)) {
+ arguments[i] = anyInt();
+ } else if (argumentTypes[i].equals(Long.TYPE)) {
+ arguments[i] = anyLong();
+ } else if (argumentTypes[i].equals(Float.TYPE)) {
+ arguments[i] = anyFloat();
+ } else if (argumentTypes[i].equals(Boolean.TYPE)) {
+ arguments[i] = anyBoolean();
+ } else {
+ arguments[i] = any();
+ }
+ }
+ return arguments;
+ }
+}