diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java
index 3873f5eb9d..611c5fa2b9 100644
--- a/libraries/common/src/main/java/androidx/media3/common/Player.java
+++ b/libraries/common/src/main/java/androidx/media3/common/Player.java
@@ -1639,10 +1639,28 @@ public interface Player {
int COMMAND_SET_REPEAT_MODE = 15;
/**
- * Command to get the currently playing {@link MediaItem}.
+ * Command to get information about the currently playing {@link MediaItem}.
*
- *
The {@link #getCurrentMediaItem()} method must only be called if this command is {@linkplain
- * #isCommandAvailable(int) available}.
+ *
The following methods must only be called if this command is {@linkplain
+ * #isCommandAvailable(int) available}:
+ *
+ *
{@link #getCurrentAdIndexInAdGroup()}
*
*/
int COMMAND_GET_TIMELINE = 17;
@@ -2706,18 +2722,27 @@ public interface Player {
/**
* Returns the duration of the current content or ad in milliseconds, or {@link C#TIME_UNSET} if
* the duration is not known.
+ *
+ * This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
+ * #getAvailableCommands() available}.
*/
long getDuration();
/**
* Returns the playback position in the current content or ad, in milliseconds, or the prospective
* position in milliseconds if the {@link #getCurrentTimeline() current timeline} is empty.
+ *
+ *
This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
+ * #getAvailableCommands() available}.
*/
long getCurrentPosition();
/**
* Returns an estimate of the position in the current content or ad up to which data is buffered,
* in milliseconds.
+ *
+ *
This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
+ * #getAvailableCommands() available}.
*/
long getBufferedPosition();
@@ -2731,6 +2756,9 @@ public interface Player {
/**
* Returns an estimate of the total buffered duration from the current position, in milliseconds.
* This includes pre-buffered data for subsequent ads and {@linkplain MediaItem media items}.
+ *
+ *
This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
+ * #getAvailableCommands() available}.
*/
long getTotalBufferedDuration();
@@ -2745,6 +2773,9 @@ public interface Player {
* Returns whether the current {@link MediaItem} is dynamic (may change when the {@link Timeline}
* is updated), or {@code false} if the {@link Timeline} is empty.
*
+ *
This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
+ * #getAvailableCommands() available}.
+ *
* @see Timeline.Window#isDynamic
*/
boolean isCurrentMediaItemDynamic();
@@ -2760,6 +2791,9 @@ public interface Player {
* Returns whether the current {@link MediaItem} is live, or {@code false} if the {@link Timeline}
* is empty.
*
+ *
This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
+ * #getAvailableCommands() available}.
+ *
* @see Timeline.Window#isLive()
*/
boolean isCurrentMediaItemLive();
@@ -2774,6 +2808,9 @@ public interface Player {
*
*
Note that this offset may rely on an accurate local time, so this method may return an
* incorrect value if the difference between system clock and server clock is unknown.
+ *
+ *
This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
+ * #getAvailableCommands() available}.
*/
long getCurrentLiveOffset();
@@ -2788,18 +2825,26 @@ public interface Player {
* Returns whether the current {@link MediaItem} is seekable, or {@code false} if the {@link
* Timeline} is empty.
*
+ *
This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
+ * #getAvailableCommands() available}.
+ *
* @see Timeline.Window#isSeekable
*/
boolean isCurrentMediaItemSeekable();
- /** Returns whether the player is currently playing an ad. */
+ /**
+ * Returns whether the player is currently playing an ad.
+ *
+ *
This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
+ * #getAvailableCommands() available}.
+ */
boolean isPlayingAd();
/**
* If {@link #isPlayingAd()} returns true, returns the index of the ad group in the period
* currently being played. Returns {@link C#INDEX_UNSET} otherwise.
*
- *
This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain
+ *
This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
* #getAvailableCommands() available}.
*/
int getCurrentAdGroupIndex();
@@ -2808,7 +2853,7 @@ public interface Player {
* If {@link #isPlayingAd()} returns true, returns the index of the ad in its ad group. Returns
* {@link C#INDEX_UNSET} otherwise.
*
- *
This method must only be called if {@link #COMMAND_GET_TIMELINE} is {@linkplain
+ *
This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
* #getAvailableCommands() available}.
*/
int getCurrentAdIndexInAdGroup();
@@ -2817,6 +2862,9 @@ public interface Player {
* If {@link #isPlayingAd()} returns {@code true}, returns the duration of the current content in
* milliseconds, or {@link C#TIME_UNSET} if the duration is not known. If there is no ad playing,
* the returned duration is the same as that returned by {@link #getDuration()}.
+ *
+ *
This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
+ * #getAvailableCommands() available}.
*/
long getContentDuration();
@@ -2824,6 +2872,9 @@ public interface Player {
* If {@link #isPlayingAd()} returns {@code true}, returns the content position that will be
* played once all ads in the ad group have finished playing, in milliseconds. If there is no ad
* playing, the returned position is the same as that returned by {@link #getCurrentPosition()}.
+ *
+ *
This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
+ * #getAvailableCommands() available}.
*/
long getContentPosition();
@@ -2831,6 +2882,9 @@ public interface Player {
* If {@link #isPlayingAd()} returns {@code true}, returns an estimate of the content position in
* the current content up to which data is buffered, in milliseconds. If there is no ad playing,
* the returned position is the same as that returned by {@link #getBufferedPosition()}.
+ *
+ *
This method must only be called if {@link #COMMAND_GET_CURRENT_MEDIA_ITEM} is {@linkplain
+ * #getAvailableCommands() available}.
*/
long getContentBufferedPosition();
diff --git a/libraries/common/src/main/java/androidx/media3/common/Timeline.java b/libraries/common/src/main/java/androidx/media3/common/Timeline.java
index 1d7706f907..d470b37b52 100644
--- a/libraries/common/src/main/java/androidx/media3/common/Timeline.java
+++ b/libraries/common/src/main/java/androidx/media3/common/Timeline.java
@@ -1416,6 +1416,39 @@ public abstract class Timeline implements Bundleable {
return bundle;
}
+ /**
+ * Returns a {@link Bundle} containing just the specified {@link Window}.
+ *
+ *
The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of
+ * an instance restored by {@link #CREATOR} may have missing fields as described in {@link
+ * Window#toBundle()} and {@link Period#toBundle()}.
+ *
+ * @param windowIndex The index of the {@link Window} to include in the {@link Bundle}.
+ */
+ @UnstableApi
+ public final Bundle toBundleWithOneWindowOnly(int windowIndex) {
+ Window window = getWindow(windowIndex, new Window(), /* defaultPositionProjectionUs= */ 0);
+
+ List periodBundles = new ArrayList<>();
+ Period period = new Period();
+ for (int i = window.firstPeriodIndex; i <= window.lastPeriodIndex; i++) {
+ getPeriod(i, period, /* setIds= */ false);
+ period.windowIndex = 0;
+ periodBundles.add(period.toBundle());
+ }
+
+ window.lastPeriodIndex = window.lastPeriodIndex - window.firstPeriodIndex;
+ window.firstPeriodIndex = 0;
+ Bundle windowBundle = window.toBundle();
+
+ Bundle bundle = new Bundle();
+ BundleUtil.putBinder(
+ bundle, FIELD_WINDOWS, new BundleListRetriever(ImmutableList.of(windowBundle)));
+ BundleUtil.putBinder(bundle, FIELD_PERIODS, new BundleListRetriever(periodBundles));
+ bundle.putIntArray(FIELD_SHUFFLED_WINDOW_INDICES, new int[] {0});
+ return bundle;
+ }
+
/**
* Object that can restore a {@link Timeline} from a {@link Bundle}.
*
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java
index 5d65a88d8e..ca5ee0c330 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java
@@ -811,8 +811,6 @@ import org.checkerframework.checker.initialization.qual.Initialized;
if (player == null) {
return;
}
- // Note: OK to omit mediaItem here, because PlayerInfo changed message will copy playerInfo
- // with sessionPositionInfo, which includes current window index.
session.playerInfo = session.playerInfo.copyWithMediaItemTransitionReason(reason);
session.onPlayerInfoChangedHandler.sendPlayerInfoChangedMessage(
/* excludeTimeline= */ true, /* excludeTracks= */ true);
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java
index 3a36b80368..6ae74ccbbf 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java
@@ -136,11 +136,16 @@ import java.util.concurrent.ExecutionException;
private static
SessionTask, K> sendSessionResultSuccess(
Consumer task) {
+ return sendSessionResultSuccess((player, controller) -> task.accept(player));
+ }
+
+ private static
+ SessionTask, K> sendSessionResultSuccess(ControllerPlayerTask task) {
return (sessionImpl, controller, sequenceNumber) -> {
if (sessionImpl.isReleased()) {
return Futures.immediateVoidFuture();
}
- task.accept(sessionImpl.getPlayerWrapper());
+ task.run(sessionImpl.getPlayerWrapper(), controller);
sendSessionResult(
controller, sequenceNumber, new SessionResult(SessionResult.RESULT_SUCCESS));
return Futures.immediateVoidFuture();
@@ -189,7 +194,8 @@ import java.util.concurrent.ExecutionException;
sessionImpl.getApplicationHandler(),
() -> {
if (!sessionImpl.isReleased()) {
- mediaItemPlayerTask.run(sessionImpl.getPlayerWrapper(), mediaItems);
+ mediaItemPlayerTask.run(
+ sessionImpl.getPlayerWrapper(), controller, mediaItems);
}
},
new SessionResult(SessionResult.RESULT_SUCCESS)));
@@ -370,6 +376,20 @@ import java.util.concurrent.ExecutionException;
return outputFuture;
}
+ private int maybeCorrectMediaItemIndex(
+ ControllerInfo controllerInfo, PlayerWrapper player, int mediaItemIndex) {
+ if (player.isCommandAvailable(Player.COMMAND_GET_TIMELINE)
+ && !connectedControllersManager.isPlayerCommandAvailable(
+ controllerInfo, Player.COMMAND_GET_TIMELINE)
+ && connectedControllersManager.isPlayerCommandAvailable(
+ controllerInfo, Player.COMMAND_GET_CURRENT_MEDIA_ITEM)) {
+ // COMMAND_GET_TIMELINE was filtered out for this controller, so all indices are relative to
+ // the current one.
+ return mediaItemIndex + player.getCurrentMediaItemIndex();
+ }
+ return mediaItemIndex;
+ }
+
public void connect(
IMediaController caller,
int controllerVersion,
@@ -555,7 +575,7 @@ import java.util.concurrent.ExecutionException;
return;
}
queueSessionTaskWithPlayerCommand(
- caller, sequenceNumber, COMMAND_STOP, sendSessionResultSuccess(Player::stop));
+ caller, sequenceNumber, COMMAND_STOP, sendSessionResultSuccess(player -> player.stop()));
}
@Override
@@ -655,7 +675,7 @@ import java.util.concurrent.ExecutionException;
caller,
sequenceNumber,
COMMAND_SEEK_TO_DEFAULT_POSITION,
- sendSessionResultSuccess(Player::seekToDefaultPosition));
+ sendSessionResultSuccess(player -> player.seekToDefaultPosition()));
}
@Override
@@ -668,7 +688,10 @@ import java.util.concurrent.ExecutionException;
caller,
sequenceNumber,
COMMAND_SEEK_TO_MEDIA_ITEM,
- sendSessionResultSuccess(player -> player.seekToDefaultPosition(mediaItemIndex)));
+ sendSessionResultSuccess(
+ (player, controller) ->
+ player.seekToDefaultPosition(
+ maybeCorrectMediaItemIndex(controller, player, mediaItemIndex))));
}
@Override
@@ -695,7 +718,10 @@ import java.util.concurrent.ExecutionException;
caller,
sequenceNumber,
COMMAND_SEEK_TO_MEDIA_ITEM,
- sendSessionResultSuccess(player -> player.seekTo(mediaItemIndex, positionMs)));
+ sendSessionResultSuccess(
+ (player, controller) ->
+ player.seekTo(
+ maybeCorrectMediaItemIndex(controller, player, mediaItemIndex), positionMs)));
}
@Override
@@ -843,7 +869,8 @@ import java.util.concurrent.ExecutionException;
handleMediaItemsWhenReady(
(sessionImpl, controller, sequenceNum) ->
sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)),
- Player::setMediaItems)));
+ (playerWrapper, controller, mediaItems) ->
+ playerWrapper.setMediaItems(mediaItems))));
}
@Override
@@ -870,7 +897,7 @@ import java.util.concurrent.ExecutionException;
handleMediaItemsWhenReady(
(sessionImpl, controller, sequenceNum) ->
sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)),
- (player, mediaItems) ->
+ (player, controller, mediaItems) ->
player.setMediaItems(mediaItems, /* startIndex= */ 0, startPositionMs))));
}
@@ -898,7 +925,8 @@ import java.util.concurrent.ExecutionException;
handleMediaItemsWhenReady(
(sessionImpl, controller, sequenceNum) ->
sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)),
- (player, mediaItems) -> player.setMediaItems(mediaItems, resetPosition))));
+ (player, controller, mediaItems) ->
+ player.setMediaItems(mediaItems, resetPosition))));
}
@Override
@@ -927,7 +955,8 @@ import java.util.concurrent.ExecutionException;
handleMediaItemsWhenReady(
(sessionImpl, controller, sequenceNum) ->
sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList),
- Player::setMediaItems)));
+ (playerWrapper, controller, mediaItems) ->
+ playerWrapper.setMediaItems(mediaItems))));
}
@Override
@@ -956,7 +985,8 @@ import java.util.concurrent.ExecutionException;
handleMediaItemsWhenReady(
(sessionImpl, controller, sequenceNum) ->
sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList),
- (player, mediaItems) -> player.setMediaItems(mediaItems, resetPosition))));
+ (player, controller, mediaItems) ->
+ player.setMediaItems(mediaItems, resetPosition))));
}
@Override
@@ -986,7 +1016,7 @@ import java.util.concurrent.ExecutionException;
handleMediaItemsWhenReady(
(sessionImpl, controller, sequenceNum) ->
sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList),
- (player, mediaItems) ->
+ (player, controller, mediaItems) ->
player.setMediaItems(mediaItems, startIndex, startPositionMs))));
}
@@ -1033,7 +1063,8 @@ import java.util.concurrent.ExecutionException;
handleMediaItemsWhenReady(
(sessionImpl, controller, sequenceNum) ->
sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)),
- Player::addMediaItems)));
+ (playerWrapper, controller, mediaItems) ->
+ playerWrapper.addMediaItems(mediaItems))));
}
@Override
@@ -1057,7 +1088,9 @@ import java.util.concurrent.ExecutionException;
handleMediaItemsWhenReady(
(sessionImpl, controller, sequenceNum) ->
sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)),
- (player, mediaItems) -> player.addMediaItems(index, mediaItems))));
+ (player, controller, mediaItems) ->
+ player.addMediaItems(
+ maybeCorrectMediaItemIndex(controller, player, index), mediaItems))));
}
@Override
@@ -1085,7 +1118,7 @@ import java.util.concurrent.ExecutionException;
handleMediaItemsWhenReady(
(sessionImpl, controller, sequenceNum) ->
sessionImpl.onAddMediaItemsOnHandler(controller, mediaItems),
- Player::addMediaItems)));
+ (playerWrapper, controller, items) -> playerWrapper.addMediaItems(items))));
}
@Override
@@ -1114,7 +1147,9 @@ import java.util.concurrent.ExecutionException;
handleMediaItemsWhenReady(
(sessionImpl, controller, sequenceNum) ->
sessionImpl.onAddMediaItemsOnHandler(controller, mediaItems),
- (player, items) -> player.addMediaItems(index, items))));
+ (player, controller, items) ->
+ player.addMediaItems(
+ maybeCorrectMediaItemIndex(controller, player, index), items))));
}
@Override
@@ -1126,7 +1161,9 @@ import java.util.concurrent.ExecutionException;
caller,
sequenceNumber,
COMMAND_CHANGE_MEDIA_ITEMS,
- sendSessionResultSuccess(player -> player.removeMediaItem(index)));
+ sendSessionResultSuccess(
+ (player, controller) ->
+ player.removeMediaItem(maybeCorrectMediaItemIndex(controller, player, index))));
}
@Override
@@ -1139,7 +1176,11 @@ import java.util.concurrent.ExecutionException;
caller,
sequenceNumber,
COMMAND_CHANGE_MEDIA_ITEMS,
- sendSessionResultSuccess(player -> player.removeMediaItems(fromIndex, toIndex)));
+ sendSessionResultSuccess(
+ (player, controller) ->
+ player.removeMediaItems(
+ maybeCorrectMediaItemIndex(controller, player, fromIndex),
+ maybeCorrectMediaItemIndex(controller, player, toIndex))));
}
@Override
@@ -1576,7 +1617,11 @@ import java.util.concurrent.ExecutionException;
}
private interface MediaItemPlayerTask {
- void run(PlayerWrapper player, List mediaItems);
+ void run(PlayerWrapper player, ControllerInfo controller, List mediaItems);
+ }
+
+ private interface ControllerPlayerTask {
+ void run(PlayerWrapper player, ControllerInfo controller);
}
/* package */ static final class Controller2Cb implements ControllerCb {
diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java
index 93433bc2da..3c6bbc1f3b 100644
--- a/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java
+++ b/libraries/session/src/main/java/androidx/media3/session/PlayerInfo.java
@@ -823,6 +823,10 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue;
bundle.putBoolean(FIELD_SHUFFLE_MODE_ENABLED, shuffleModeEnabled);
if (!excludeTimeline && canAccessTimeline) {
bundle.putBundle(FIELD_TIMELINE, timeline.toBundle());
+ } else if (!canAccessTimeline && canAccessCurrentMediaItem && !timeline.isEmpty()) {
+ bundle.putBundle(
+ FIELD_TIMELINE,
+ timeline.toBundleWithOneWindowOnly(sessionPositionInfo.positionInfo.mediaItemIndex));
}
bundle.putBundle(FIELD_VIDEO_SIZE, videoSize.toBundle());
if (availableCommands.contains(Player.COMMAND_GET_MEDIA_ITEMS_METADATA)) {
diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java
index 42c391f9c4..c4922f4b4f 100644
--- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java
+++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java
@@ -17,6 +17,7 @@ package androidx.media3.session;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
+import static androidx.media3.common.util.Util.msToUs;
import static androidx.media3.common.util.Util.postOrRun;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_MEDIA_ID_COMPAT;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT;
@@ -588,7 +589,12 @@ import java.util.List;
}
public Timeline getCurrentTimelineWithCommandCheck() {
- return isCommandAvailable(COMMAND_GET_TIMELINE) ? getCurrentTimeline() : Timeline.EMPTY;
+ if (isCommandAvailable(COMMAND_GET_TIMELINE)) {
+ return getCurrentTimeline();
+ } else if (isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) {
+ return new CurrentMediaItemOnlyTimeline(this);
+ }
+ return Timeline.EMPTY;
}
@Override
@@ -1165,4 +1171,75 @@ import java.util.List;
return 0;
}
}
+
+ private static final class CurrentMediaItemOnlyTimeline extends Timeline {
+
+ private static final Object UID = new Object();
+
+ @Nullable private final MediaItem mediaItem;
+ private final boolean isSeekable;
+ private final boolean isDynamic;
+ @Nullable private final MediaItem.LiveConfiguration liveConfiguration;
+ private final long durationUs;
+
+ public CurrentMediaItemOnlyTimeline(PlayerWrapper player) {
+ mediaItem = player.getCurrentMediaItem();
+ isSeekable = player.isCurrentMediaItemSeekable();
+ isDynamic = player.isCurrentMediaItemDynamic();
+ liveConfiguration =
+ player.isCurrentMediaItemLive() ? MediaItem.LiveConfiguration.UNSET : null;
+ durationUs = msToUs(player.getContentDuration());
+ }
+
+ @Override
+ public int getWindowCount() {
+ return 1;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
+ window.set(
+ UID,
+ mediaItem,
+ /* manifest= */ null,
+ /* presentationStartTimeMs= */ C.TIME_UNSET,
+ /* windowStartTimeMs= */ C.TIME_UNSET,
+ /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
+ isSeekable,
+ isDynamic,
+ liveConfiguration,
+ /* defaultPositionUs= */ 0,
+ durationUs,
+ /* firstPeriodIndex= */ 0,
+ /* lastPeriodIndex= */ 0,
+ /* positionInFirstPeriodUs= */ 0);
+ return window;
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return 1;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ period.set(
+ /* id= */ UID,
+ /* uid= */ UID,
+ /* windowIndex= */ 0,
+ durationUs,
+ /* positionInWindowUs= */ 0);
+ return period;
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return UID.equals(uid) ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public Object getUidOfPeriod(int periodIndex) {
+ return UID;
+ }
+ }
}
diff --git a/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java b/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java
index fdc4e2e4c0..32ea6e18a5 100644
--- a/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java
+++ b/libraries/session/src/test/java/androidx/media3/session/PlayerInfoTest.java
@@ -376,7 +376,32 @@ public class PlayerInfoTest {
/* currentLiveOffsetMs= */ 3000,
/* contentDurationMs= */ 27000,
/* contentBufferedPositionMs= */ 15000))
- .setTimeline(new FakeTimeline(/* windowCount= */ 10))
+ .setTimeline(
+ new FakeTimeline(
+ new FakeTimeline.TimelineWindowDefinition(
+ /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000),
+ new FakeTimeline.TimelineWindowDefinition(
+ /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000),
+ new FakeTimeline.TimelineWindowDefinition(
+ /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000),
+ new FakeTimeline.TimelineWindowDefinition(
+ /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000),
+ new FakeTimeline.TimelineWindowDefinition(
+ /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000),
+ new FakeTimeline.TimelineWindowDefinition(
+ /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000),
+ new FakeTimeline.TimelineWindowDefinition(
+ /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000),
+ new FakeTimeline.TimelineWindowDefinition(
+ /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000),
+ new FakeTimeline.TimelineWindowDefinition(
+ /* periodCount= */ 2,
+ /* id= */ new Object(),
+ /* isSeekable= */ true,
+ /* isDynamic= */ true,
+ /* durationUs= */ 5000),
+ new FakeTimeline.TimelineWindowDefinition(
+ /* isSeekable= */ true, /* isDynamic= */ true, /* durationUs= */ 1000)))
.build();
PlayerInfo infoAfterBundling =
@@ -421,7 +446,21 @@ public class PlayerInfoTest {
assertThat(infoAfterBundling.sessionPositionInfo.currentLiveOffsetMs).isEqualTo(3000);
assertThat(infoAfterBundling.sessionPositionInfo.contentDurationMs).isEqualTo(27000);
assertThat(infoAfterBundling.sessionPositionInfo.contentBufferedPositionMs).isEqualTo(15000);
- assertThat(infoAfterBundling.timeline).isEqualTo(Timeline.EMPTY);
+ assertThat(infoAfterBundling.timeline.getWindowCount()).isEqualTo(1);
+ Timeline.Window window =
+ infoAfterBundling.timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window());
+ assertThat(window.durationUs).isEqualTo(5000);
+ assertThat(window.firstPeriodIndex).isEqualTo(0);
+ assertThat(window.lastPeriodIndex).isEqualTo(1);
+ Timeline.Period period =
+ infoAfterBundling.timeline.getPeriod(/* periodIndex= */ 0, new Timeline.Period());
+ assertThat(period.durationUs)
+ .isEqualTo(
+ 2500 + FakeTimeline.TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US);
+ assertThat(period.windowIndex).isEqualTo(0);
+ infoAfterBundling.timeline.getPeriod(/* periodIndex= */ 1, period);
+ assertThat(period.durationUs).isEqualTo(2500);
+ assertThat(period.windowIndex).isEqualTo(0);
}
@Test
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java
index 13f7d64d4e..0beb95bef4 100644
--- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerListenerTest.java
@@ -2215,7 +2215,7 @@ public class MediaControllerListenerTest {
}
@Test
- public void onTimelineChanged_playerCommandUnavailable_emptyTimelineMediaItemAndMetadata()
+ public void onTimelineChanged_playerCommandUnavailable_reducesTimelineToOneItem()
throws Exception {
int testMediaItemsSize = 2;
List testMediaItemList = MediaTestUtils.createMediaItems(testMediaItemsSize);
@@ -2227,8 +2227,6 @@ public class MediaControllerListenerTest {
CountDownLatch latch = new CountDownLatch(3);
AtomicReference timelineFromParamRef = new AtomicReference<>();
AtomicReference timelineFromGetterRef = new AtomicReference<>();
- List onEventsTimelines = new ArrayList<>();
- AtomicReference metadataFromGetterRef = new AtomicReference<>();
AtomicReference isCurrentMediaItemNullRef = new AtomicReference<>();
List eventsList = new ArrayList<>();
Player.Listener listener =
@@ -2237,7 +2235,6 @@ public class MediaControllerListenerTest {
public void onTimelineChanged(Timeline timeline, int reason) {
timelineFromParamRef.set(timeline);
timelineFromGetterRef.set(controller.getCurrentTimeline());
- metadataFromGetterRef.set(controller.getMediaMetadata());
isCurrentMediaItemNullRef.set(controller.getCurrentMediaItem() == null);
latch.countDown();
}
@@ -2245,7 +2242,6 @@ public class MediaControllerListenerTest {
@Override
public void onEvents(Player player, Player.Events events) {
// onEvents is called twice.
- onEventsTimelines.add(player.getCurrentTimeline());
eventsList.add(events);
latch.countDown();
}
@@ -2256,27 +2252,17 @@ public class MediaControllerListenerTest {
remoteSession.getMockPlayer().notifyAvailableCommandsChanged(commandsWithoutGetTimeline);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
- assertThat(timelineFromParamRef.get()).isEqualTo(Timeline.EMPTY);
- assertThat(onEventsTimelines).hasSize(2);
- for (int i = 0; i < onEventsTimelines.get(1).getWindowCount(); i++) {
- assertThat(
- onEventsTimelines
- .get(1)
- .getWindow(/* windowIndex= */ i, new Timeline.Window())
- .mediaItem)
- .isEqualTo(MediaItem.EMPTY);
- }
- assertThat(metadataFromGetterRef.get()).isEqualTo(MediaMetadata.EMPTY);
- assertThat(isCurrentMediaItemNullRef.get()).isTrue();
+ assertThat(timelineFromParamRef.get().getWindowCount()).isEqualTo(1);
+ assertThat(timelineFromGetterRef.get().getWindowCount()).isEqualTo(1);
+ assertThat(isCurrentMediaItemNullRef.get()).isFalse();
assertThat(eventsList).hasSize(2);
assertThat(getEventsAsList(eventsList.get(0)))
.containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED);
- assertThat(getEventsAsList(eventsList.get(1)))
- .containsExactly(Player.EVENT_TIMELINE_CHANGED, Player.EVENT_MEDIA_ITEM_TRANSITION);
+ assertThat(getEventsAsList(eventsList.get(1))).containsExactly(Player.EVENT_TIMELINE_CHANGED);
}
@Test
- public void onTimelineChanged_sessionCommandUnavailable_emptyTimelineMediaItemAndMetadata()
+ public void onTimelineChanged_sessionCommandUnavailable_reducesTimelineToOneItem()
throws Exception {
int testMediaItemsSize = 2;
List testMediaItemList = MediaTestUtils.createMediaItems(testMediaItemsSize);
@@ -2288,7 +2274,6 @@ public class MediaControllerListenerTest {
CountDownLatch latch = new CountDownLatch(3);
AtomicReference timelineFromParamRef = new AtomicReference<>();
AtomicReference timelineFromGetterRef = new AtomicReference<>();
- AtomicReference metadataFromGetterRef = new AtomicReference<>();
AtomicReference isCurrentMediaItemNullRef = new AtomicReference<>();
List eventsList = new ArrayList<>();
Player.Listener listener =
@@ -2297,7 +2282,6 @@ public class MediaControllerListenerTest {
public void onTimelineChanged(Timeline timeline, int reason) {
timelineFromParamRef.set(timeline);
timelineFromGetterRef.set(controller.getCurrentTimeline());
- metadataFromGetterRef.set(controller.getMediaMetadata());
isCurrentMediaItemNullRef.set(controller.getCurrentMediaItem() == null);
latch.countDown();
}
@@ -2315,14 +2299,13 @@ public class MediaControllerListenerTest {
remoteSession.setAvailableCommands(SessionCommands.EMPTY, commandsWithoutGetTimeline);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
- assertThat(timelineFromParamRef.get()).isEqualTo(Timeline.EMPTY);
- assertThat(metadataFromGetterRef.get()).isEqualTo(MediaMetadata.EMPTY);
- assertThat(isCurrentMediaItemNullRef.get()).isTrue();
+ assertThat(timelineFromParamRef.get().getWindowCount()).isEqualTo(1);
+ assertThat(timelineFromGetterRef.get().getWindowCount()).isEqualTo(1);
+ assertThat(isCurrentMediaItemNullRef.get()).isFalse();
assertThat(eventsList).hasSize(2);
assertThat(getEventsAsList(eventsList.get(0)))
.containsExactly(Player.EVENT_AVAILABLE_COMMANDS_CHANGED);
- assertThat(getEventsAsList(eventsList.get(1)))
- .containsExactly(Player.EVENT_TIMELINE_CHANGED, Player.EVENT_MEDIA_ITEM_TRANSITION);
+ assertThat(getEventsAsList(eventsList.get(1))).containsExactly(Player.EVENT_TIMELINE_CHANGED);
}
/** This also tests {@link MediaController#getAvailableCommands()}. */
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java
index 076643c2a2..2098f6ca29 100644
--- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java
@@ -34,6 +34,8 @@ import androidx.media3.test.session.common.TestUtils;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import org.junit.After;
@@ -164,6 +166,47 @@ public class MediaSessionPlayerTest {
assertThat(player.seekMediaItemIndex).isEqualTo(mediaItemIndex);
}
+ @Test
+ public void seekToDefaultPosition_withMediaItemIndexWithoutGetTimelineCommand() throws Exception {
+ MockPlayer player =
+ new MockPlayer.Builder()
+ .setApplicationLooper(threadTestRule.getHandler().getLooper())
+ .setMediaItems(/* itemCount= */ 5)
+ .build();
+ player.currentMediaItemIndex = 3;
+ MediaSession session =
+ new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player)
+ .setCallback(
+ new MediaSession.Callback() {
+ @Override
+ public MediaSession.ConnectionResult onConnect(
+ MediaSession session, MediaSession.ControllerInfo controller) {
+ SessionCommands sessionCommands =
+ new SessionCommands.Builder().addAllSessionCommands().build();
+ Player.Commands playerCommands =
+ new Player.Commands.Builder()
+ .addAllCommands()
+ .remove(Player.COMMAND_GET_TIMELINE)
+ .build();
+ return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands);
+ }
+ })
+ .setId("seekToDefaultPosition_withMediaItemIndexWithoutGetTimelineCommand")
+ .build();
+ RemoteMediaController controller =
+ remoteControllerTestRule.createRemoteController(session.getToken());
+
+ // The controller should only be able to see the current item without Timeline access.
+ controller.seekToDefaultPosition(/* mediaItemIndex= */ 0);
+ player.awaitMethodCalled(
+ MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS);
+ controller.release();
+ session.release();
+ player.release();
+
+ assertThat(player.seekMediaItemIndex).isEqualTo(3);
+ }
+
@Test
public void seekTo() throws Exception {
long seekPositionMs = 12125L;
@@ -185,6 +228,47 @@ public class MediaSessionPlayerTest {
assertThat(player.seekPositionMs).isEqualTo(seekPositionMs);
}
+ @Test
+ public void seekTo_withMediaItemIndexWithoutGetTimelineCommand() throws Exception {
+ MockPlayer player =
+ new MockPlayer.Builder()
+ .setApplicationLooper(threadTestRule.getHandler().getLooper())
+ .setMediaItems(/* itemCount= */ 5)
+ .build();
+ player.currentMediaItemIndex = 3;
+ MediaSession session =
+ new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player)
+ .setCallback(
+ new MediaSession.Callback() {
+ @Override
+ public MediaSession.ConnectionResult onConnect(
+ MediaSession session, MediaSession.ControllerInfo controller) {
+ SessionCommands sessionCommands =
+ new SessionCommands.Builder().addAllSessionCommands().build();
+ Player.Commands playerCommands =
+ new Player.Commands.Builder()
+ .addAllCommands()
+ .remove(Player.COMMAND_GET_TIMELINE)
+ .build();
+ return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands);
+ }
+ })
+ .setId("seekTo_withMediaItemIndexWithoutGetTimelineCommand")
+ .build();
+ RemoteMediaController controller =
+ remoteControllerTestRule.createRemoteController(session.getToken());
+
+ // The controller should only be able to see the current item without Timeline access.
+ controller.seekTo(/* mediaItemIndex= */ 0, /* seekPositionMs= */ 2000);
+ player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS);
+ controller.release();
+ session.release();
+ player.release();
+
+ assertThat(player.seekMediaItemIndex).isEqualTo(3);
+ assertThat(player.seekPositionMs).isEqualTo(2000);
+ }
+
@Test
public void setPlaybackSpeed() throws Exception {
float testSpeed = 1.5f;
@@ -352,6 +436,55 @@ public class MediaSessionPlayerTest {
assertThat(player.mediaItems).hasSize(6);
}
+ @Test
+ public void addMediaItem_withIndexWithoutGetTimelineCommand() throws Exception {
+ MockPlayer player =
+ new MockPlayer.Builder()
+ .setApplicationLooper(threadTestRule.getHandler().getLooper())
+ .setMediaItems(/* itemCount= */ 5)
+ .build();
+ player.currentMediaItemIndex = 3;
+ MediaSession session =
+ new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player)
+ .setCallback(
+ new MediaSession.Callback() {
+ @Override
+ public MediaSession.ConnectionResult onConnect(
+ MediaSession session, MediaSession.ControllerInfo controller) {
+ SessionCommands sessionCommands =
+ new SessionCommands.Builder().addAllSessionCommands().build();
+ Player.Commands playerCommands =
+ new Player.Commands.Builder()
+ .addAllCommands()
+ .remove(Player.COMMAND_GET_TIMELINE)
+ .build();
+ return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands);
+ }
+
+ @Override
+ public ListenableFuture> onAddMediaItems(
+ MediaSession mediaSession,
+ MediaSession.ControllerInfo controller,
+ List mediaItems) {
+ return Futures.immediateFuture(mediaItems);
+ }
+ })
+ .setId("addMediaItem_withIndexWithoutGetTimelineCommand")
+ .build();
+ RemoteMediaController controller =
+ remoteControllerTestRule.createRemoteController(session.getToken());
+ MediaItem mediaItem = MediaTestUtils.createMediaItem("addMediaItem_withIndex");
+
+ // The controller should only be able to see the current item without Timeline access.
+ controller.addMediaItem(/* index= */ 1, mediaItem);
+ player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS_WITH_INDEX, TIMEOUT_MS);
+ controller.release();
+ session.release();
+ player.release();
+
+ assertThat(player.index).isEqualTo(4);
+ }
+
@Test
public void addMediaItems() throws Exception {
int size = 2;
@@ -376,6 +509,55 @@ public class MediaSessionPlayerTest {
assertThat(player.mediaItems).hasSize(7);
}
+ @Test
+ public void addMediaItems_withIndexWithoutGetTimelineCommand() throws Exception {
+ MockPlayer player =
+ new MockPlayer.Builder()
+ .setApplicationLooper(threadTestRule.getHandler().getLooper())
+ .setMediaItems(/* itemCount= */ 5)
+ .build();
+ player.currentMediaItemIndex = 3;
+ MediaSession session =
+ new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player)
+ .setCallback(
+ new MediaSession.Callback() {
+ @Override
+ public MediaSession.ConnectionResult onConnect(
+ MediaSession session, MediaSession.ControllerInfo controller) {
+ SessionCommands sessionCommands =
+ new SessionCommands.Builder().addAllSessionCommands().build();
+ Player.Commands playerCommands =
+ new Player.Commands.Builder()
+ .addAllCommands()
+ .remove(Player.COMMAND_GET_TIMELINE)
+ .build();
+ return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands);
+ }
+
+ @Override
+ public ListenableFuture> onAddMediaItems(
+ MediaSession mediaSession,
+ MediaSession.ControllerInfo controller,
+ List mediaItems) {
+ return Futures.immediateFuture(mediaItems);
+ }
+ })
+ .setId("addMediaItems_withIndexWithoutGetTimelineCommand")
+ .build();
+ RemoteMediaController controller =
+ remoteControllerTestRule.createRemoteController(session.getToken());
+ MediaItem mediaItem = MediaTestUtils.createMediaItem("addMediaItem_withIndex");
+
+ // The controller should only be able to see the current item without Timeline access.
+ controller.addMediaItems(/* index= */ 1, ImmutableList.of(mediaItem, mediaItem));
+ player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS_WITH_INDEX, TIMEOUT_MS);
+ controller.release();
+ session.release();
+ player.release();
+
+ assertThat(player.index).isEqualTo(4);
+ }
+
@Test
public void removeMediaItem() throws Exception {
int index = 3;
@@ -386,6 +568,46 @@ public class MediaSessionPlayerTest {
assertThat(player.index).isEqualTo(index);
}
+ @Test
+ public void removeMediaItem_withoutGetTimelineCommand() throws Exception {
+ MockPlayer player =
+ new MockPlayer.Builder()
+ .setApplicationLooper(threadTestRule.getHandler().getLooper())
+ .setMediaItems(/* itemCount= */ 5)
+ .build();
+ player.currentMediaItemIndex = 3;
+ MediaSession session =
+ new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player)
+ .setCallback(
+ new MediaSession.Callback() {
+ @Override
+ public MediaSession.ConnectionResult onConnect(
+ MediaSession session, MediaSession.ControllerInfo controller) {
+ SessionCommands sessionCommands =
+ new SessionCommands.Builder().addAllSessionCommands().build();
+ Player.Commands playerCommands =
+ new Player.Commands.Builder()
+ .addAllCommands()
+ .remove(Player.COMMAND_GET_TIMELINE)
+ .build();
+ return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands);
+ }
+ })
+ .setId("removeMediaItem_withoutGetTimelineCommand")
+ .build();
+ RemoteMediaController controller =
+ remoteControllerTestRule.createRemoteController(session.getToken());
+
+ // The controller should only be able to see the current item without Timeline access.
+ controller.removeMediaItem(/* index= */ 0);
+ player.awaitMethodCalled(MockPlayer.METHOD_REMOVE_MEDIA_ITEM, TIMEOUT_MS);
+ controller.release();
+ session.release();
+ player.release();
+
+ assertThat(player.index).isEqualTo(3);
+ }
+
@Test
public void removeMediaItems() throws Exception {
int fromIndex = 0;
@@ -398,6 +620,47 @@ public class MediaSessionPlayerTest {
assertThat(player.toIndex).isEqualTo(toIndex);
}
+ @Test
+ public void removeMediaItems_withoutGetTimelineCommand() throws Exception {
+ MockPlayer player =
+ new MockPlayer.Builder()
+ .setApplicationLooper(threadTestRule.getHandler().getLooper())
+ .setMediaItems(/* itemCount= */ 5)
+ .build();
+ player.currentMediaItemIndex = 3;
+ MediaSession session =
+ new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player)
+ .setCallback(
+ new MediaSession.Callback() {
+ @Override
+ public MediaSession.ConnectionResult onConnect(
+ MediaSession session, MediaSession.ControllerInfo controller) {
+ SessionCommands sessionCommands =
+ new SessionCommands.Builder().addAllSessionCommands().build();
+ Player.Commands playerCommands =
+ new Player.Commands.Builder()
+ .addAllCommands()
+ .remove(Player.COMMAND_GET_TIMELINE)
+ .build();
+ return MediaSession.ConnectionResult.accept(sessionCommands, playerCommands);
+ }
+ })
+ .setId("removeMediaItems_withoutGetTimelineCommand")
+ .build();
+ RemoteMediaController controller =
+ remoteControllerTestRule.createRemoteController(session.getToken());
+
+ // The controller should only be able to see the current item without Timeline access.
+ controller.removeMediaItems(/* fromIndex= */ 0, /* toIndex= */ 0);
+ player.awaitMethodCalled(MockPlayer.METHOD_REMOVE_MEDIA_ITEMS, TIMEOUT_MS);
+ controller.release();
+ session.release();
+ player.release();
+
+ assertThat(player.fromIndex).isEqualTo(3);
+ assertThat(player.toIndex).isEqualTo(3);
+ }
+
@Test
public void clearMediaItems() throws Exception {
controller.clearMediaItems();
diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java
index 23e30bf4b8..4d33c9efe3 100644
--- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java
+++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockPlayer.java
@@ -795,7 +795,9 @@ public class MockPlayer implements Player {
@Override
public boolean isCurrentMediaItemDynamic() {
- throw new UnsupportedOperationException();
+ Timeline timeline = getCurrentTimeline();
+ return !timeline.isEmpty()
+ && timeline.getWindow(getCurrentMediaItemIndex(), new Timeline.Window()).isDynamic;
}
/**
@@ -809,7 +811,9 @@ public class MockPlayer implements Player {
@Override
public boolean isCurrentMediaItemLive() {
- throw new UnsupportedOperationException();
+ Timeline timeline = getCurrentTimeline();
+ return !timeline.isEmpty()
+ && timeline.getWindow(getCurrentMediaItemIndex(), new Timeline.Window()).isLive();
}
/**
@@ -823,7 +827,9 @@ public class MockPlayer implements Player {
@Override
public boolean isCurrentMediaItemSeekable() {
- throw new UnsupportedOperationException();
+ Timeline timeline = getCurrentTimeline();
+ return !timeline.isEmpty()
+ && timeline.getWindow(getCurrentMediaItemIndex(), new Timeline.Window()).isSeekable;
}
@Override