From f15b7525436b45694b5e1971dac922adff48b5ae Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 19 Jan 2023 09:50:52 +0000 Subject: [PATCH] Extend command GET_CURRENT_MEDIA_ITEM to more methods. We currently only document it for the getCurrentMediaItem(), but the command was always meant to cover all information about the current media item and the position therein. To correctly hide information for controllers, we need to filter the Timeline when bundling the PlayerInfo class if only this command is available. PiperOrigin-RevId: 503098124 --- .../java/androidx/media3/common/Player.java | 70 ++++- .../java/androidx/media3/common/Timeline.java | 33 +++ .../media3/session/MediaSessionImpl.java | 2 - .../media3/session/MediaSessionStub.java | 83 ++++-- .../androidx/media3/session/PlayerInfo.java | 4 + .../media3/session/PlayerWrapper.java | 79 +++++- .../media3/session/PlayerInfoTest.java | 43 ++- .../session/MediaControllerListenerTest.java | 37 +-- .../session/MediaSessionPlayerTest.java | 263 ++++++++++++++++++ .../androidx/media3/session/MockPlayer.java | 12 +- 10 files changed, 564 insertions(+), 62 deletions(-) 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