diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 90c846051d..ab48c5581d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,9 @@ * Common Library: * Replace `SimpleBasePlayer.State.playlist` by `getPlaylist()` method. + * Add override for `SimpleBasePlayer.State.Builder.setPlaylist()` to + directly specify a `Timeline` and current `Tracks` and `Metadata` + instead of building a playlist structure. * ExoPlayer: * `MediaCodecRenderer.onProcessedStreamChange()` can now be called for every media item. Previously it was not called for the first one. Use diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java index 173b36603c..f95d7a5537 100644 --- a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java +++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java @@ -127,8 +127,10 @@ public abstract class SimpleBasePlayer extends BasePlayer { private Size surfaceSize; private boolean newlyRenderedFirstFrame; private Metadata timedMetadata; - private ImmutableList playlist; + @Nullable private ImmutableList playlist; private Timeline timeline; + @Nullable private Tracks currentTracks; + @Nullable private MediaMetadata currentMetadata; private MediaMetadata playlistMetadata; private int currentMediaItemIndex; private int currentAdGroupIndex; @@ -171,7 +173,9 @@ public abstract class SimpleBasePlayer extends BasePlayer { newlyRenderedFirstFrame = false; timedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET); playlist = ImmutableList.of(); - timeline = new PlaylistTimeline(ImmutableList.of()); + timeline = Timeline.EMPTY; + currentTracks = null; + currentMetadata = null; playlistMetadata = MediaMetadata.EMPTY; currentMediaItemIndex = C.INDEX_UNSET; currentAdGroupIndex = C.INDEX_UNSET; @@ -214,7 +218,12 @@ public abstract class SimpleBasePlayer extends BasePlayer { this.newlyRenderedFirstFrame = state.newlyRenderedFirstFrame; this.timedMetadata = state.timedMetadata; this.timeline = state.timeline; - this.playlist = ((PlaylistTimeline) state.timeline).playlist; + if (state.timeline instanceof PlaylistTimeline) { + this.playlist = ((PlaylistTimeline) state.timeline).playlist; + } else { + this.currentTracks = state.currentTracks; + this.currentMetadata = state.currentMetadata; + } this.playlistMetadata = state.playlistMetadata; this.currentMediaItemIndex = state.currentMediaItemIndex; this.currentAdGroupIndex = state.currentAdGroupIndex; @@ -539,10 +548,13 @@ public abstract class SimpleBasePlayer extends BasePlayer { } /** - * Sets the list of {@link MediaItemData media items} in the playlist. + * Sets the playlist as a list of {@link MediaItemData media items}. * *

All items must have unique {@linkplain MediaItemData.Builder#setUid UIDs}. * + *

This call replaces any previous playlist set via {@link #setPlaylist(Timeline, Tracks, + * MediaMetadata)}. + * * @param playlist The list of {@link MediaItemData media items} in the playlist. * @return This builder. */ @@ -554,6 +566,33 @@ public abstract class SimpleBasePlayer extends BasePlayer { } this.playlist = ImmutableList.copyOf(playlist); this.timeline = new PlaylistTimeline(this.playlist); + this.currentTracks = null; + this.currentMetadata = null; + return this; + } + + /** + * Sets the playlist as a {@link Timeline} with information about the current {@link Tracks} + * and {@link MediaMetadata}. + * + *

This call replaces any previous playlist set via {@link #setPlaylist(List)}. + * + * @param timeline The {@link Timeline} containing the playlist data. + * @param currentTracks The {@link Tracks} of the {@linkplain #setCurrentMediaItemIndex + * current media item}. + * @param currentMetadata The combined {@link MediaMetadata} of the {@linkplain + * #setCurrentMediaItemIndex current media item}. If null, the current metadata is assumed + * to be the combination of the {@link MediaItem#mediaMetadata MediaItem} metadata and the + * metadata of the selected {@link Format#metadata Formats}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setPlaylist( + Timeline timeline, Tracks currentTracks, @Nullable MediaMetadata currentMetadata) { + this.playlist = null; + this.timeline = timeline; + this.currentTracks = currentTracks; + this.currentMetadata = currentMetadata; return this; } @@ -854,6 +893,12 @@ public abstract class SimpleBasePlayer extends BasePlayer { /** The {@link Timeline}. */ public final Timeline timeline; + /** The current {@link Tracks}. */ + public final Tracks currentTracks; + + /** The current combined {@link MediaMetadata}. */ + public final MediaMetadata currentMetadata; + /** The playlist {@link MediaMetadata}. */ public final MediaMetadata playlistMetadata; @@ -914,6 +959,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { public final long discontinuityPositionMs; private State(Builder builder) { + Tracks currentTracks = builder.currentTracks; + MediaMetadata currentMetadata = builder.currentMetadata; if (builder.timeline.isEmpty()) { checkArgument( builder.playbackState == Player.STATE_IDLE @@ -923,6 +970,12 @@ public abstract class SimpleBasePlayer extends BasePlayer { builder.currentAdGroupIndex == C.INDEX_UNSET && builder.currentAdIndexInAdGroup == C.INDEX_UNSET, "Ads not allowed if playlist is empty"); + if (currentTracks == null) { + currentTracks = Tracks.EMPTY; + } + if (currentMetadata == null) { + currentMetadata = MediaMetadata.EMPTY; + } } else { int mediaItemIndex = builder.currentMediaItemIndex; if (mediaItemIndex == C.INDEX_UNSET) { @@ -953,6 +1006,17 @@ public abstract class SimpleBasePlayer extends BasePlayer { "Ad group has less ads than adIndexInGroupIndex"); } } + if (builder.playlist != null) { + MediaItemData mediaItemData = builder.playlist.get(mediaItemIndex); + currentTracks = mediaItemData.tracks; + currentMetadata = mediaItemData.mediaMetadata; + } + if (currentMetadata == null) { + currentMetadata = + getCombinedMediaMetadata( + builder.timeline.getWindow(mediaItemIndex, new Timeline.Window()).mediaItem, + checkNotNull(currentTracks)); + } } if (builder.playerError != null) { checkArgument( @@ -1014,6 +1078,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { this.newlyRenderedFirstFrame = builder.newlyRenderedFirstFrame; this.timedMetadata = builder.timedMetadata; this.timeline = builder.timeline; + this.currentTracks = checkNotNull(currentTracks); + this.currentMetadata = currentMetadata; this.playlistMetadata = builder.playlistMetadata; this.currentMediaItemIndex = builder.currentMediaItemIndex; this.currentAdGroupIndex = builder.currentAdGroupIndex; @@ -1039,7 +1105,19 @@ public abstract class SimpleBasePlayer extends BasePlayer { * @see Builder#setPlaylist(List) */ public ImmutableList getPlaylist() { - return ((PlaylistTimeline) timeline).playlist; + if (timeline instanceof PlaylistTimeline) { + return ((PlaylistTimeline) timeline).playlist; + } + Timeline.Window window = new Timeline.Window(); + Timeline.Period period = new Timeline.Period(); + ImmutableList.Builder items = + ImmutableList.builderWithExpectedSize(timeline.getWindowCount()); + for (int i = 0; i < timeline.getWindowCount(); i++) { + items.add( + MediaItemData.buildFromState( + /* state= */ this, /* mediaItemIndex= */ i, period, window)); + } + return items.build(); } @Override @@ -1076,6 +1154,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { && newlyRenderedFirstFrame == state.newlyRenderedFirstFrame && timedMetadata.equals(state.timedMetadata) && timeline.equals(state.timeline) + && currentTracks.equals(state.currentTracks) + && currentMetadata.equals(state.currentMetadata) && playlistMetadata.equals(state.playlistMetadata) && currentMediaItemIndex == state.currentMediaItemIndex && currentAdGroupIndex == state.currentAdGroupIndex @@ -1119,6 +1199,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { result = 31 * result + (newlyRenderedFirstFrame ? 1 : 0); result = 31 * result + timedMetadata.hashCode(); result = 31 * result + timeline.hashCode(); + result = 31 * result + currentTracks.hashCode(); + result = 31 * result + currentMetadata.hashCode(); result = 31 * result + playlistMetadata.hashCode(); result = 31 * result + currentMediaItemIndex; result = 31 * result + currentAdGroupIndex; @@ -1142,9 +1224,9 @@ public abstract class SimpleBasePlayer extends BasePlayer { private final int[] windowIndexByPeriodIndex; private final HashMap periodIndexByUid; - public PlaylistTimeline(ImmutableList playlist) { + public PlaylistTimeline(List playlist) { int mediaItemCount = playlist.size(); - this.playlist = playlist; + this.playlist = ImmutableList.copyOf(playlist); this.firstPeriodIndexByWindowIndex = new int[mediaItemCount]; int periodCount = 0; for (int i = 0; i < mediaItemCount; i++) { @@ -1642,7 +1724,6 @@ public abstract class SimpleBasePlayer extends BasePlayer { public final ImmutableList periods; private final long[] periodPositionInWindowUs; - private final MediaMetadata combinedMediaMetadata; private MediaItemData(Builder builder) { if (builder.liveConfiguration == null) { @@ -1690,8 +1771,6 @@ public abstract class SimpleBasePlayer extends BasePlayer { periodPositionInWindowUs[i + 1] = periodPositionInWindowUs[i] + periods.get(i).durationUs; } } - combinedMediaMetadata = - mediaMetadata != null ? mediaMetadata : getCombinedMediaMetadata(mediaItem, tracks); } /** Returns a {@link Builder} pre-populated with the current values. */ @@ -1750,6 +1829,39 @@ public abstract class SimpleBasePlayer extends BasePlayer { return result; } + private static MediaItemData buildFromState( + State state, int mediaItemIndex, Timeline.Period period, Timeline.Window window) { + boolean isCurrentItem = getCurrentMediaItemIndexInternal(state) == mediaItemIndex; + state.timeline.getWindow(mediaItemIndex, window); + ImmutableList.Builder periods = ImmutableList.builder(); + for (int i = window.firstPeriodIndex; i <= window.lastPeriodIndex; i++) { + state.timeline.getPeriod(/* periodIndex= */ i, period, /* setIds= */ true); + periods.add( + new PeriodData.Builder(checkNotNull(period.uid)) + .setAdPlaybackState(period.adPlaybackState) + .setDurationUs(period.durationUs) + .setIsPlaceholder(period.isPlaceholder) + .build()); + } + return new MediaItemData.Builder(window.uid) + .setDefaultPositionUs(window.defaultPositionUs) + .setDurationUs(window.durationUs) + .setElapsedRealtimeEpochOffsetMs(window.elapsedRealtimeEpochOffsetMs) + .setIsDynamic(window.isDynamic) + .setIsPlaceholder(window.isPlaceholder) + .setIsSeekable(window.isSeekable) + .setLiveConfiguration(window.liveConfiguration) + .setManifest(window.manifest) + .setMediaItem(window.mediaItem) + .setMediaMetadata(isCurrentItem ? state.currentMetadata : null) + .setPeriods(periods.build()) + .setPositionInFirstPeriodUs(window.positionInFirstPeriodUs) + .setPresentationStartTimeMs(window.presentationStartTimeMs) + .setTracks(isCurrentItem ? state.currentTracks : Tracks.EMPTY) + .setWindowStartTimeMs(window.windowStartTimeMs) + .build(); + } + private Timeline.Window getWindow(int firstPeriodIndex, Timeline.Window window) { int periodCount = periods.isEmpty() ? 1 : periods.size(); window.set( @@ -1805,25 +1917,6 @@ public abstract class SimpleBasePlayer extends BasePlayer { Object periodId = periods.get(periodIndexInMediaItem).uid; return Pair.create(uid, periodId); } - - private static MediaMetadata getCombinedMediaMetadata(MediaItem mediaItem, Tracks tracks) { - MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder(); - int trackGroupCount = tracks.getGroups().size(); - for (int i = 0; i < trackGroupCount; i++) { - Tracks.Group group = tracks.getGroups().get(i); - for (int j = 0; j < group.length; j++) { - if (group.isTrackSelected(j)) { - Format format = group.getTrackFormat(j); - if (format.metadata != null) { - for (int k = 0; k < format.metadata.length(); k++) { - format.metadata.get(k).populateMediaMetadata(metadataBuilder); - } - } - } - } - } - return metadataBuilder.populate(mediaItem.mediaMetadata).build(); - } } /** Data describing the properties of a period inside a {@link MediaItemData}. */ @@ -2157,7 +2250,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { updateStateForPendingOperation( /* pendingOperation= */ handleAddMediaItems(correctedIndex, mediaItems), /* placeholderStateSupplier= */ () -> { - List placeholderPlaylist = buildMutablePlaylistFromState(state); + List placeholderPlaylist = + buildMutablePlaylistFromState(state, period, window); for (int i = 0; i < mediaItems.size(); i++) { placeholderPlaylist.add( i + correctedIndex, getPlaceholderMediaItemData(mediaItems.get(i))); @@ -2197,7 +2291,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { /* pendingOperation= */ handleMoveMediaItems( fromIndex, correctedToIndex, correctedNewIndex), /* placeholderStateSupplier= */ () -> { - List placeholderPlaylist = buildMutablePlaylistFromState(state); + List placeholderPlaylist = + buildMutablePlaylistFromState(state, period, window); Util.moveItems(placeholderPlaylist, fromIndex, correctedToIndex, correctedNewIndex); return getStateWithNewPlaylist(state, placeholderPlaylist, period, window); }); @@ -2216,7 +2311,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { updateStateForPendingOperation( /* pendingOperation= */ handleReplaceMediaItems(fromIndex, correctedToIndex, mediaItems), /* placeholderStateSupplier= */ () -> { - List placeholderPlaylist = buildMutablePlaylistFromState(state); + List placeholderPlaylist = + buildMutablePlaylistFromState(state, period, window); for (int i = 0; i < mediaItems.size(); i++) { placeholderPlaylist.add( i + correctedToIndex, getPlaceholderMediaItemData(mediaItems.get(i))); @@ -2262,7 +2358,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { updateStateForPendingOperation( /* pendingOperation= */ handleRemoveMediaItems(fromIndex, correctedToIndex), /* placeholderStateSupplier= */ () -> { - List placeholderPlaylist = buildMutablePlaylistFromState(state); + List placeholderPlaylist = + buildMutablePlaylistFromState(state, period, window); Util.removeRange(placeholderPlaylist, fromIndex, correctedToIndex); return getStateWithNewPlaylist(state, placeholderPlaylist, period, window); }); @@ -2469,7 +2566,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Override public final Tracks getCurrentTracks() { verifyApplicationThreadAndInitState(); - return getCurrentTracksInternal(state); + return state.currentTracks; } @Override @@ -2495,7 +2592,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { @Override public final MediaMetadata getMediaMetadata() { verifyApplicationThreadAndInitState(); - return getMediaMetadataInternal(state); + return state.currentMetadata; } @Override @@ -3420,10 +3517,6 @@ public abstract class SimpleBasePlayer extends BasePlayer { boolean playWhenReadyChanged = previousState.playWhenReady != newState.playWhenReady; boolean playbackStateChanged = previousState.playbackState != newState.playbackState; - Tracks previousTracks = getCurrentTracksInternal(previousState); - Tracks newTracks = getCurrentTracksInternal(newState); - MediaMetadata previousMediaMetadata = getMediaMetadataInternal(previousState); - MediaMetadata newMediaMetadata = getMediaMetadataInternal(newState); int positionDiscontinuityReason = getPositionDiscontinuityReason( previousState, newState, forceSeekDiscontinuity, window, period); @@ -3484,14 +3577,15 @@ public abstract class SimpleBasePlayer extends BasePlayer { listener -> listener.onTrackSelectionParametersChanged(newState.trackSelectionParameters)); } - if (!previousTracks.equals(newTracks)) { + if (!previousState.currentTracks.equals(newState.currentTracks)) { listeners.queueEvent( - Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(newTracks)); + Player.EVENT_TRACKS_CHANGED, + listener -> listener.onTracksChanged(newState.currentTracks)); } - if (!previousMediaMetadata.equals(newMediaMetadata)) { + if (!previousState.currentMetadata.equals(newState.currentMetadata)) { listeners.queueEvent( EVENT_MEDIA_METADATA_CHANGED, - listener -> listener.onMediaMetadataChanged(newMediaMetadata)); + listener -> listener.onMediaMetadataChanged(newState.currentMetadata)); } if (previousState.isLoading != newState.isLoading) { listeners.queueEvent( @@ -3687,20 +3781,6 @@ public abstract class SimpleBasePlayer extends BasePlayer { && state.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; } - private static Tracks getCurrentTracksInternal(State state) { - return state.timeline.isEmpty() - ? Tracks.EMPTY - : ((PlaylistTimeline) state.timeline) - .playlist.get(getCurrentMediaItemIndexInternal(state)).tracks; - } - - private static MediaMetadata getMediaMetadataInternal(State state) { - return state.timeline.isEmpty() - ? MediaMetadata.EMPTY - : ((PlaylistTimeline) state.timeline) - .playlist.get(getCurrentMediaItemIndexInternal(state)).combinedMediaMetadata; - } - private static int getCurrentMediaItemIndexInternal(State state) { if (state.currentMediaItemIndex != C.INDEX_UNSET) { return state.currentMediaItemIndex; @@ -3971,8 +4051,7 @@ public abstract class SimpleBasePlayer extends BasePlayer { Timeline.Period period, Timeline.Window window) { State.Builder stateBuilder = oldState.buildUpon(); - stateBuilder.setPlaylist(newPlaylist); - Timeline newTimeline = stateBuilder.timeline; + Timeline newTimeline = new PlaylistTimeline(newPlaylist); Timeline oldTimeline = oldState.timeline; long oldPositionMs = oldState.contentPositionMsSupplier.get(); int oldIndex = getCurrentMediaItemIndexInternal(oldState); @@ -4008,11 +4087,8 @@ public abstract class SimpleBasePlayer extends BasePlayer { long newPositionMs, Timeline.Window window) { State.Builder stateBuilder = oldState.buildUpon(); - Timeline newTimeline = oldState.timeline; - if (newPlaylist != null) { - stateBuilder.setPlaylist(newPlaylist); - newTimeline = stateBuilder.timeline; - } + Timeline newTimeline = + newPlaylist == null ? oldState.timeline : new PlaylistTimeline(newPlaylist); if (oldState.playbackState != Player.STATE_IDLE) { if (newTimeline.isEmpty() || (newIndex != C.INDEX_UNSET && newIndex >= newTimeline.getWindowCount())) { @@ -4060,6 +4136,19 @@ public abstract class SimpleBasePlayer extends BasePlayer { .getWindow(getCurrentMediaItemIndexInternal(oldState), window) .uid .equals(newTimeline.getWindow(newIndex, window).uid); + // Set timeline, resolving tracks and metadata to the new index. + if (newTimeline.isEmpty()) { + stateBuilder.setPlaylist(newTimeline, Tracks.EMPTY, /* currentMetadata= */ null); + } else if (newTimeline instanceof PlaylistTimeline) { + MediaItemData mediaItemData = ((PlaylistTimeline) newTimeline).playlist.get(newIndex); + stateBuilder.setPlaylist(newTimeline, mediaItemData.tracks, mediaItemData.mediaMetadata); + } else { + boolean keepTracksAndMetadata = !oldOrNewPlaylistEmpty && !mediaItemChanged; + stateBuilder.setPlaylist( + newTimeline, + keepTracksAndMetadata ? oldState.currentTracks : Tracks.EMPTY, + keepTracksAndMetadata ? oldState.currentMetadata : null); + } if (oldOrNewPlaylistEmpty || mediaItemChanged || newPositionMs < oldPositionMs) { // New item or seeking back. Assume no buffer and no ad playback persists. stateBuilder @@ -4098,8 +4187,35 @@ public abstract class SimpleBasePlayer extends BasePlayer { return stateBuilder.build(); } - private static List buildMutablePlaylistFromState(State state) { - return new ArrayList<>(((PlaylistTimeline) state.timeline).playlist); + private static MediaMetadata getCombinedMediaMetadata(MediaItem mediaItem, Tracks tracks) { + MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder(); + int trackGroupCount = tracks.getGroups().size(); + for (int i = 0; i < trackGroupCount; i++) { + Tracks.Group group = tracks.getGroups().get(i); + for (int j = 0; j < group.length; j++) { + if (group.isTrackSelected(j)) { + Format format = group.getTrackFormat(j); + if (format.metadata != null) { + for (int k = 0; k < format.metadata.length(); k++) { + format.metadata.get(k).populateMediaMetadata(metadataBuilder); + } + } + } + } + } + return metadataBuilder.populate(mediaItem.mediaMetadata).build(); + } + + private static List buildMutablePlaylistFromState( + State state, Timeline.Period period, Timeline.Window window) { + if (state.timeline instanceof PlaylistTimeline) { + return new ArrayList<>(((PlaylistTimeline) state.timeline).playlist); + } + ArrayList items = new ArrayList<>(state.timeline.getWindowCount()); + for (int i = 0; i < state.timeline.getWindowCount(); i++) { + items.add(MediaItemData.buildFromState(state, /* mediaItemIndex= */ i, period, window)); + } + return items; } private static final class PlaceholderUid {} 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 d5e3225955..e04192f9e8 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Timeline.java +++ b/libraries/common/src/main/java/androidx/media3/common/Timeline.java @@ -575,7 +575,8 @@ public abstract class Timeline { */ public boolean isPlaceholder; - private AdPlaybackState adPlaybackState; + /** The {@link AdPlaybackState} for all ads in this period. */ + @UnstableApi public AdPlaybackState adPlaybackState; /** Creates a new instance with no ad playback state. */ public Period() { diff --git a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java index 29dfd26e93..a7e2a79b0a 100644 --- a/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/SimpleBasePlayerTest.java @@ -40,7 +40,9 @@ import androidx.media3.common.SimpleBasePlayer.State; import androidx.media3.common.text.Cue; import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Size; +import androidx.media3.extractor.metadata.icy.IcyInfo; import androidx.media3.test.utils.FakeMetadataEntry; +import androidx.media3.test.utils.FakeTimeline; import androidx.media3.test.utils.TestUtil; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -225,6 +227,15 @@ public class SimpleBasePlayerTest { Size surfaceSize = new Size(480, 360); DeviceInfo deviceInfo = new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL).setMaxVolume(7).build(); + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); ImmutableList playlist = ImmutableList.of( new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), @@ -236,6 +247,8 @@ public class SimpleBasePlayerTest { new AdPlaybackState( /* adsId= */ new Object(), /* adGroupTimesUs...= */ 555, 666)) .build())) + .setMediaMetadata(mediaMetadata) + .setTracks(tracks) .build()); MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build(); SimpleBasePlayer.PositionSupplier contentPositionSupplier = () -> 456; @@ -314,6 +327,8 @@ public class SimpleBasePlayerTest { assertThat(state.timedMetadata).isEqualTo(timedMetadata); assertThat(state.getPlaylist()).isEqualTo(playlist); assertThat(state.timeline.getWindowCount()).isEqualTo(2); + assertThat(state.currentTracks).isEqualTo(tracks); + assertThat(state.currentMetadata).isEqualTo(mediaMetadata); assertThat(state.playlistMetadata).isEqualTo(playlistMetadata); assertThat(state.currentMediaItemIndex).isEqualTo(1); assertThat(state.currentAdGroupIndex).isEqualTo(1); @@ -328,6 +343,69 @@ public class SimpleBasePlayerTest { assertThat(state.discontinuityPositionMs).isEqualTo(400); } + @Test + public void stateBuilderBuild_withExplicitTimeline_setsCorrectValues() { + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + + State state = new State.Builder().setPlaylist(timeline, tracks, mediaMetadata).build(); + + assertThat(state.timeline).isEqualTo(timeline); + assertThat(state.currentTracks).isEqualTo(tracks); + assertThat(state.currentMetadata).isEqualTo(mediaMetadata); + } + + @Test + public void + stateBuilderBuild_withUndefinedMediaMetadataAndExplicitTimeline_derivesMediaMetadataFromTracksAndMediaItem() + throws Exception { + Timeline timeline = + new FakeTimeline( + new FakeTimeline.TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ true, + /* isLive= */ true, + /* isPlaceholder= */ false, + /* durationUs= */ 1000, + /* defaultPositionUs= */ 0, + /* windowOffsetInFirstPeriodUs= */ 0, + ImmutableList.of(AdPlaybackState.NONE), + new MediaItem.Builder() + .setMediaId("1") + .setMediaMetadata(new MediaMetadata.Builder().setArtist("artist").build()) + .build())); + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup( + new Format.Builder() + .setMetadata( + new Metadata( + new IcyInfo( + /* rawMetadata= */ new byte[0], "title", /* url= */ null))) + .build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + + State state = + new State.Builder().setPlaylist(timeline, tracks, /* currentMetadata= */ null).build(); + + assertThat(state.currentMetadata) + .isEqualTo(new MediaMetadata.Builder().setArtist("artist").setTitle("title").build()); + } + @Test public void stateBuilderBuild_emptyTimelineWithReadyState_throwsException() { assertThrows( @@ -8070,6 +8148,211 @@ public class SimpleBasePlayerTest { verifyNoMoreInteractions(listener); } + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void seekTo_asyncHandlingToNewItem_usesPlaceholderStateWithUpdatedTracksAndMetadata() { + MediaItem newMediaItem = new MediaItem.Builder().setMediaId("2").build(); + Tracks newTracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + MediaMetadata newMediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1).build(), + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 2) + .setMediaItem(newMediaItem) + .setTracks(newTracks) + .setMediaMetadata(newMediaMetadata) + .build())) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentTracks()).isEqualTo(newTracks); + assertThat(player.getMediaMetadata()).isEqualTo(newMediaMetadata); + verify(listener).onMediaItemTransition(newMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener).onTracksChanged(newTracks); + verify(listener).onMediaMetadataChanged(newMediaMetadata); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingToNewItemWithExplicitTimeline_usesPlaceholderStateWithEmptyTracksAndMetadata() { + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist(timeline, tracks, mediaMetadata) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); + assertThat(player.getCurrentTracks()).isEqualTo(Tracks.EMPTY); + assertThat(player.getMediaMetadata()).isEqualTo(MediaMetadata.EMPTY); + verify(listener) + .onMediaItemTransition( + timeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()).mediaItem, + Player.MEDIA_ITEM_TRANSITION_REASON_SEEK); + verify(listener).onTracksChanged(Tracks.EMPTY); + verify(listener).onMediaMetadataChanged(MediaMetadata.EMPTY); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingToSameItem_usesPlaceholderStateWithoutChangingTracksAndMetadata() { + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist( + ImmutableList.of( + new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ 1) + .setTracks(tracks) + .setMediaMetadata(mediaMetadata) + .build())) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* positionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentTracks()).isEqualTo(tracks); + assertThat(player.getMediaMetadata()).isEqualTo(mediaMetadata); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verifyNoMoreInteractions(listener); + } + + @SuppressWarnings("deprecation") // Verifying deprecated listener calls. + @Test + public void + seekTo_asyncHandlingToSameItemWithExplicitTimeline_usesPlaceholderStateWithoutChangingTracksAndMetadata() { + Tracks tracks = + new Tracks( + ImmutableList.of( + new Tracks.Group( + new TrackGroup(new Format.Builder().build()), + /* adaptiveSupported= */ true, + /* trackSupport= */ new int[] {C.FORMAT_HANDLED}, + /* trackSelected= */ new boolean[] {true}))); + MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + State state = + new State.Builder() + .setAvailableCommands(new Commands.Builder().addAllCommands().build()) + .setPlaylist(timeline, tracks, mediaMetadata) + .build(); + SettableFuture future = SettableFuture.create(); + SimpleBasePlayer player = + new SimpleBasePlayer(Looper.myLooper()) { + @Override + protected State getState() { + return state; + } + + @Override + protected ListenableFuture handleSeek( + int mediaItemIndex, long positionMs, @Player.Command int seekCommand) { + return future; + } + }; + Listener listener = mock(Listener.class); + player.addListener(listener); + + player.seekTo(/* positionMs= */ 3000); + + // Verify placeholder state and listener calls. + assertThat(player.getCurrentTracks()).isEqualTo(tracks); + assertThat(player.getMediaMetadata()).isEqualTo(mediaMetadata); + verify(listener).onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); + verify(listener).onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK)); + verifyNoMoreInteractions(listener); + } + @Test public void seekTo_withoutAvailableCommandForSeekToMediaItem_isNotForwarded() { State state =