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}: + * + *

*/ int COMMAND_GET_CURRENT_MEDIA_ITEM = 16; @@ -1662,8 +1680,6 @@ public interface Player { *
  • {@link #getPreviousMediaItemIndex()} *
  • {@link #hasPreviousMediaItem()} *
  • {@link #hasNextMediaItem()} - *
  • {@link #getCurrentAdGroupIndex()} - *
  • {@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