Allow externally provided Timeline in SimpleBasePlayer.State

The Timeline, Tracks and MediaMetadata are currently provided
with a list of MediaItemData objects, that are a declarative
version of these classes. This works well for cases where
SimpleBasePlayer is used for external systems or custom players
that don't have a Timeline object available already. However,
this makes it really hard to provide the data if the app already
has a Timeline, currently requiring to convert it back and forth
to a list of MediaItemData.

This change adds an override for `State.Builder.setPlaylist`
that allows to set these 3 objects directly without going
through MediaItemData. The conversion only happens when needed
(e.g. when modifying the playlist).

PiperOrigin-RevId: 649667983
This commit is contained in:
tonihei 2024-07-05 09:37:04 -07:00 committed by Copybara-Service
parent fafd927702
commit b2585aad0f
4 changed files with 470 additions and 67 deletions

View File

@ -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

View File

@ -127,8 +127,10 @@ public abstract class SimpleBasePlayer extends BasePlayer {
private Size surfaceSize;
private boolean newlyRenderedFirstFrame;
private Metadata timedMetadata;
private ImmutableList<MediaItemData> playlist;
@Nullable private ImmutableList<MediaItemData> 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}.
*
* <p>All items must have unique {@linkplain MediaItemData.Builder#setUid UIDs}.
*
* <p>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}.
*
* <p>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<MediaItemData> 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<MediaItemData> 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<Object, Integer> periodIndexByUid;
public PlaylistTimeline(ImmutableList<MediaItemData> playlist) {
public PlaylistTimeline(List<MediaItemData> 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<PeriodData> 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<PeriodData> 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<MediaItemData> placeholderPlaylist = buildMutablePlaylistFromState(state);
List<MediaItemData> 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<MediaItemData> placeholderPlaylist = buildMutablePlaylistFromState(state);
List<MediaItemData> 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<MediaItemData> placeholderPlaylist = buildMutablePlaylistFromState(state);
List<MediaItemData> 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<MediaItemData> placeholderPlaylist = buildMutablePlaylistFromState(state);
List<MediaItemData> 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<MediaItemData> 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<MediaItemData> buildMutablePlaylistFromState(
State state, Timeline.Period period, Timeline.Window window) {
if (state.timeline instanceof PlaylistTimeline) {
return new ArrayList<>(((PlaylistTimeline) state.timeline).playlist);
}
ArrayList<MediaItemData> 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 {}

View File

@ -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() {

View File

@ -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<SimpleBasePlayer.MediaItemData> 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 =