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: * Common Library:
* Replace `SimpleBasePlayer.State.playlist` by `getPlaylist()` method. * 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: * ExoPlayer:
* `MediaCodecRenderer.onProcessedStreamChange()` can now be called for * `MediaCodecRenderer.onProcessedStreamChange()` can now be called for
every media item. Previously it was not called for the first one. Use 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 Size surfaceSize;
private boolean newlyRenderedFirstFrame; private boolean newlyRenderedFirstFrame;
private Metadata timedMetadata; private Metadata timedMetadata;
private ImmutableList<MediaItemData> playlist; @Nullable private ImmutableList<MediaItemData> playlist;
private Timeline timeline; private Timeline timeline;
@Nullable private Tracks currentTracks;
@Nullable private MediaMetadata currentMetadata;
private MediaMetadata playlistMetadata; private MediaMetadata playlistMetadata;
private int currentMediaItemIndex; private int currentMediaItemIndex;
private int currentAdGroupIndex; private int currentAdGroupIndex;
@ -171,7 +173,9 @@ public abstract class SimpleBasePlayer extends BasePlayer {
newlyRenderedFirstFrame = false; newlyRenderedFirstFrame = false;
timedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET); timedMetadata = new Metadata(/* presentationTimeUs= */ C.TIME_UNSET);
playlist = ImmutableList.of(); playlist = ImmutableList.of();
timeline = new PlaylistTimeline(ImmutableList.of()); timeline = Timeline.EMPTY;
currentTracks = null;
currentMetadata = null;
playlistMetadata = MediaMetadata.EMPTY; playlistMetadata = MediaMetadata.EMPTY;
currentMediaItemIndex = C.INDEX_UNSET; currentMediaItemIndex = C.INDEX_UNSET;
currentAdGroupIndex = C.INDEX_UNSET; currentAdGroupIndex = C.INDEX_UNSET;
@ -214,7 +218,12 @@ public abstract class SimpleBasePlayer extends BasePlayer {
this.newlyRenderedFirstFrame = state.newlyRenderedFirstFrame; this.newlyRenderedFirstFrame = state.newlyRenderedFirstFrame;
this.timedMetadata = state.timedMetadata; this.timedMetadata = state.timedMetadata;
this.timeline = state.timeline; this.timeline = state.timeline;
if (state.timeline instanceof PlaylistTimeline) {
this.playlist = ((PlaylistTimeline) state.timeline).playlist; this.playlist = ((PlaylistTimeline) state.timeline).playlist;
} else {
this.currentTracks = state.currentTracks;
this.currentMetadata = state.currentMetadata;
}
this.playlistMetadata = state.playlistMetadata; this.playlistMetadata = state.playlistMetadata;
this.currentMediaItemIndex = state.currentMediaItemIndex; this.currentMediaItemIndex = state.currentMediaItemIndex;
this.currentAdGroupIndex = state.currentAdGroupIndex; 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>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. * @param playlist The list of {@link MediaItemData media items} in the playlist.
* @return This builder. * @return This builder.
*/ */
@ -554,6 +566,33 @@ public abstract class SimpleBasePlayer extends BasePlayer {
} }
this.playlist = ImmutableList.copyOf(playlist); this.playlist = ImmutableList.copyOf(playlist);
this.timeline = new PlaylistTimeline(this.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; return this;
} }
@ -854,6 +893,12 @@ public abstract class SimpleBasePlayer extends BasePlayer {
/** The {@link Timeline}. */ /** The {@link Timeline}. */
public final Timeline 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}. */ /** The playlist {@link MediaMetadata}. */
public final MediaMetadata playlistMetadata; public final MediaMetadata playlistMetadata;
@ -914,6 +959,8 @@ public abstract class SimpleBasePlayer extends BasePlayer {
public final long discontinuityPositionMs; public final long discontinuityPositionMs;
private State(Builder builder) { private State(Builder builder) {
Tracks currentTracks = builder.currentTracks;
MediaMetadata currentMetadata = builder.currentMetadata;
if (builder.timeline.isEmpty()) { if (builder.timeline.isEmpty()) {
checkArgument( checkArgument(
builder.playbackState == Player.STATE_IDLE builder.playbackState == Player.STATE_IDLE
@ -923,6 +970,12 @@ public abstract class SimpleBasePlayer extends BasePlayer {
builder.currentAdGroupIndex == C.INDEX_UNSET builder.currentAdGroupIndex == C.INDEX_UNSET
&& builder.currentAdIndexInAdGroup == C.INDEX_UNSET, && builder.currentAdIndexInAdGroup == C.INDEX_UNSET,
"Ads not allowed if playlist is empty"); "Ads not allowed if playlist is empty");
if (currentTracks == null) {
currentTracks = Tracks.EMPTY;
}
if (currentMetadata == null) {
currentMetadata = MediaMetadata.EMPTY;
}
} else { } else {
int mediaItemIndex = builder.currentMediaItemIndex; int mediaItemIndex = builder.currentMediaItemIndex;
if (mediaItemIndex == C.INDEX_UNSET) { if (mediaItemIndex == C.INDEX_UNSET) {
@ -953,6 +1006,17 @@ public abstract class SimpleBasePlayer extends BasePlayer {
"Ad group has less ads than adIndexInGroupIndex"); "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) { if (builder.playerError != null) {
checkArgument( checkArgument(
@ -1014,6 +1078,8 @@ public abstract class SimpleBasePlayer extends BasePlayer {
this.newlyRenderedFirstFrame = builder.newlyRenderedFirstFrame; this.newlyRenderedFirstFrame = builder.newlyRenderedFirstFrame;
this.timedMetadata = builder.timedMetadata; this.timedMetadata = builder.timedMetadata;
this.timeline = builder.timeline; this.timeline = builder.timeline;
this.currentTracks = checkNotNull(currentTracks);
this.currentMetadata = currentMetadata;
this.playlistMetadata = builder.playlistMetadata; this.playlistMetadata = builder.playlistMetadata;
this.currentMediaItemIndex = builder.currentMediaItemIndex; this.currentMediaItemIndex = builder.currentMediaItemIndex;
this.currentAdGroupIndex = builder.currentAdGroupIndex; this.currentAdGroupIndex = builder.currentAdGroupIndex;
@ -1039,8 +1105,20 @@ public abstract class SimpleBasePlayer extends BasePlayer {
* @see Builder#setPlaylist(List) * @see Builder#setPlaylist(List)
*/ */
public ImmutableList<MediaItemData> getPlaylist() { public ImmutableList<MediaItemData> getPlaylist() {
if (timeline instanceof PlaylistTimeline) {
return ((PlaylistTimeline) timeline).playlist; 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 @Override
public boolean equals(@Nullable Object o) { public boolean equals(@Nullable Object o) {
@ -1076,6 +1154,8 @@ public abstract class SimpleBasePlayer extends BasePlayer {
&& newlyRenderedFirstFrame == state.newlyRenderedFirstFrame && newlyRenderedFirstFrame == state.newlyRenderedFirstFrame
&& timedMetadata.equals(state.timedMetadata) && timedMetadata.equals(state.timedMetadata)
&& timeline.equals(state.timeline) && timeline.equals(state.timeline)
&& currentTracks.equals(state.currentTracks)
&& currentMetadata.equals(state.currentMetadata)
&& playlistMetadata.equals(state.playlistMetadata) && playlistMetadata.equals(state.playlistMetadata)
&& currentMediaItemIndex == state.currentMediaItemIndex && currentMediaItemIndex == state.currentMediaItemIndex
&& currentAdGroupIndex == state.currentAdGroupIndex && currentAdGroupIndex == state.currentAdGroupIndex
@ -1119,6 +1199,8 @@ public abstract class SimpleBasePlayer extends BasePlayer {
result = 31 * result + (newlyRenderedFirstFrame ? 1 : 0); result = 31 * result + (newlyRenderedFirstFrame ? 1 : 0);
result = 31 * result + timedMetadata.hashCode(); result = 31 * result + timedMetadata.hashCode();
result = 31 * result + timeline.hashCode(); result = 31 * result + timeline.hashCode();
result = 31 * result + currentTracks.hashCode();
result = 31 * result + currentMetadata.hashCode();
result = 31 * result + playlistMetadata.hashCode(); result = 31 * result + playlistMetadata.hashCode();
result = 31 * result + currentMediaItemIndex; result = 31 * result + currentMediaItemIndex;
result = 31 * result + currentAdGroupIndex; result = 31 * result + currentAdGroupIndex;
@ -1142,9 +1224,9 @@ public abstract class SimpleBasePlayer extends BasePlayer {
private final int[] windowIndexByPeriodIndex; private final int[] windowIndexByPeriodIndex;
private final HashMap<Object, Integer> periodIndexByUid; private final HashMap<Object, Integer> periodIndexByUid;
public PlaylistTimeline(ImmutableList<MediaItemData> playlist) { public PlaylistTimeline(List<MediaItemData> playlist) {
int mediaItemCount = playlist.size(); int mediaItemCount = playlist.size();
this.playlist = playlist; this.playlist = ImmutableList.copyOf(playlist);
this.firstPeriodIndexByWindowIndex = new int[mediaItemCount]; this.firstPeriodIndexByWindowIndex = new int[mediaItemCount];
int periodCount = 0; int periodCount = 0;
for (int i = 0; i < mediaItemCount; i++) { for (int i = 0; i < mediaItemCount; i++) {
@ -1642,7 +1724,6 @@ public abstract class SimpleBasePlayer extends BasePlayer {
public final ImmutableList<PeriodData> periods; public final ImmutableList<PeriodData> periods;
private final long[] periodPositionInWindowUs; private final long[] periodPositionInWindowUs;
private final MediaMetadata combinedMediaMetadata;
private MediaItemData(Builder builder) { private MediaItemData(Builder builder) {
if (builder.liveConfiguration == null) { if (builder.liveConfiguration == null) {
@ -1690,8 +1771,6 @@ public abstract class SimpleBasePlayer extends BasePlayer {
periodPositionInWindowUs[i + 1] = periodPositionInWindowUs[i] + periods.get(i).durationUs; 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. */ /** Returns a {@link Builder} pre-populated with the current values. */
@ -1750,6 +1829,39 @@ public abstract class SimpleBasePlayer extends BasePlayer {
return result; 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) { private Timeline.Window getWindow(int firstPeriodIndex, Timeline.Window window) {
int periodCount = periods.isEmpty() ? 1 : periods.size(); int periodCount = periods.isEmpty() ? 1 : periods.size();
window.set( window.set(
@ -1805,25 +1917,6 @@ public abstract class SimpleBasePlayer extends BasePlayer {
Object periodId = periods.get(periodIndexInMediaItem).uid; Object periodId = periods.get(periodIndexInMediaItem).uid;
return Pair.create(uid, periodId); 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}. */ /** Data describing the properties of a period inside a {@link MediaItemData}. */
@ -2157,7 +2250,8 @@ public abstract class SimpleBasePlayer extends BasePlayer {
updateStateForPendingOperation( updateStateForPendingOperation(
/* pendingOperation= */ handleAddMediaItems(correctedIndex, mediaItems), /* pendingOperation= */ handleAddMediaItems(correctedIndex, mediaItems),
/* placeholderStateSupplier= */ () -> { /* placeholderStateSupplier= */ () -> {
List<MediaItemData> placeholderPlaylist = buildMutablePlaylistFromState(state); List<MediaItemData> placeholderPlaylist =
buildMutablePlaylistFromState(state, period, window);
for (int i = 0; i < mediaItems.size(); i++) { for (int i = 0; i < mediaItems.size(); i++) {
placeholderPlaylist.add( placeholderPlaylist.add(
i + correctedIndex, getPlaceholderMediaItemData(mediaItems.get(i))); i + correctedIndex, getPlaceholderMediaItemData(mediaItems.get(i)));
@ -2197,7 +2291,8 @@ public abstract class SimpleBasePlayer extends BasePlayer {
/* pendingOperation= */ handleMoveMediaItems( /* pendingOperation= */ handleMoveMediaItems(
fromIndex, correctedToIndex, correctedNewIndex), fromIndex, correctedToIndex, correctedNewIndex),
/* placeholderStateSupplier= */ () -> { /* placeholderStateSupplier= */ () -> {
List<MediaItemData> placeholderPlaylist = buildMutablePlaylistFromState(state); List<MediaItemData> placeholderPlaylist =
buildMutablePlaylistFromState(state, period, window);
Util.moveItems(placeholderPlaylist, fromIndex, correctedToIndex, correctedNewIndex); Util.moveItems(placeholderPlaylist, fromIndex, correctedToIndex, correctedNewIndex);
return getStateWithNewPlaylist(state, placeholderPlaylist, period, window); return getStateWithNewPlaylist(state, placeholderPlaylist, period, window);
}); });
@ -2216,7 +2311,8 @@ public abstract class SimpleBasePlayer extends BasePlayer {
updateStateForPendingOperation( updateStateForPendingOperation(
/* pendingOperation= */ handleReplaceMediaItems(fromIndex, correctedToIndex, mediaItems), /* pendingOperation= */ handleReplaceMediaItems(fromIndex, correctedToIndex, mediaItems),
/* placeholderStateSupplier= */ () -> { /* placeholderStateSupplier= */ () -> {
List<MediaItemData> placeholderPlaylist = buildMutablePlaylistFromState(state); List<MediaItemData> placeholderPlaylist =
buildMutablePlaylistFromState(state, period, window);
for (int i = 0; i < mediaItems.size(); i++) { for (int i = 0; i < mediaItems.size(); i++) {
placeholderPlaylist.add( placeholderPlaylist.add(
i + correctedToIndex, getPlaceholderMediaItemData(mediaItems.get(i))); i + correctedToIndex, getPlaceholderMediaItemData(mediaItems.get(i)));
@ -2262,7 +2358,8 @@ public abstract class SimpleBasePlayer extends BasePlayer {
updateStateForPendingOperation( updateStateForPendingOperation(
/* pendingOperation= */ handleRemoveMediaItems(fromIndex, correctedToIndex), /* pendingOperation= */ handleRemoveMediaItems(fromIndex, correctedToIndex),
/* placeholderStateSupplier= */ () -> { /* placeholderStateSupplier= */ () -> {
List<MediaItemData> placeholderPlaylist = buildMutablePlaylistFromState(state); List<MediaItemData> placeholderPlaylist =
buildMutablePlaylistFromState(state, period, window);
Util.removeRange(placeholderPlaylist, fromIndex, correctedToIndex); Util.removeRange(placeholderPlaylist, fromIndex, correctedToIndex);
return getStateWithNewPlaylist(state, placeholderPlaylist, period, window); return getStateWithNewPlaylist(state, placeholderPlaylist, period, window);
}); });
@ -2469,7 +2566,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
@Override @Override
public final Tracks getCurrentTracks() { public final Tracks getCurrentTracks() {
verifyApplicationThreadAndInitState(); verifyApplicationThreadAndInitState();
return getCurrentTracksInternal(state); return state.currentTracks;
} }
@Override @Override
@ -2495,7 +2592,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
@Override @Override
public final MediaMetadata getMediaMetadata() { public final MediaMetadata getMediaMetadata() {
verifyApplicationThreadAndInitState(); verifyApplicationThreadAndInitState();
return getMediaMetadataInternal(state); return state.currentMetadata;
} }
@Override @Override
@ -3420,10 +3517,6 @@ public abstract class SimpleBasePlayer extends BasePlayer {
boolean playWhenReadyChanged = previousState.playWhenReady != newState.playWhenReady; boolean playWhenReadyChanged = previousState.playWhenReady != newState.playWhenReady;
boolean playbackStateChanged = previousState.playbackState != newState.playbackState; boolean playbackStateChanged = previousState.playbackState != newState.playbackState;
Tracks previousTracks = getCurrentTracksInternal(previousState);
Tracks newTracks = getCurrentTracksInternal(newState);
MediaMetadata previousMediaMetadata = getMediaMetadataInternal(previousState);
MediaMetadata newMediaMetadata = getMediaMetadataInternal(newState);
int positionDiscontinuityReason = int positionDiscontinuityReason =
getPositionDiscontinuityReason( getPositionDiscontinuityReason(
previousState, newState, forceSeekDiscontinuity, window, period); previousState, newState, forceSeekDiscontinuity, window, period);
@ -3484,14 +3577,15 @@ public abstract class SimpleBasePlayer extends BasePlayer {
listener -> listener ->
listener.onTrackSelectionParametersChanged(newState.trackSelectionParameters)); listener.onTrackSelectionParametersChanged(newState.trackSelectionParameters));
} }
if (!previousTracks.equals(newTracks)) { if (!previousState.currentTracks.equals(newState.currentTracks)) {
listeners.queueEvent( 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( listeners.queueEvent(
EVENT_MEDIA_METADATA_CHANGED, EVENT_MEDIA_METADATA_CHANGED,
listener -> listener.onMediaMetadataChanged(newMediaMetadata)); listener -> listener.onMediaMetadataChanged(newState.currentMetadata));
} }
if (previousState.isLoading != newState.isLoading) { if (previousState.isLoading != newState.isLoading) {
listeners.queueEvent( listeners.queueEvent(
@ -3687,20 +3781,6 @@ public abstract class SimpleBasePlayer extends BasePlayer {
&& state.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE; && 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) { private static int getCurrentMediaItemIndexInternal(State state) {
if (state.currentMediaItemIndex != C.INDEX_UNSET) { if (state.currentMediaItemIndex != C.INDEX_UNSET) {
return state.currentMediaItemIndex; return state.currentMediaItemIndex;
@ -3971,8 +4051,7 @@ public abstract class SimpleBasePlayer extends BasePlayer {
Timeline.Period period, Timeline.Period period,
Timeline.Window window) { Timeline.Window window) {
State.Builder stateBuilder = oldState.buildUpon(); State.Builder stateBuilder = oldState.buildUpon();
stateBuilder.setPlaylist(newPlaylist); Timeline newTimeline = new PlaylistTimeline(newPlaylist);
Timeline newTimeline = stateBuilder.timeline;
Timeline oldTimeline = oldState.timeline; Timeline oldTimeline = oldState.timeline;
long oldPositionMs = oldState.contentPositionMsSupplier.get(); long oldPositionMs = oldState.contentPositionMsSupplier.get();
int oldIndex = getCurrentMediaItemIndexInternal(oldState); int oldIndex = getCurrentMediaItemIndexInternal(oldState);
@ -4008,11 +4087,8 @@ public abstract class SimpleBasePlayer extends BasePlayer {
long newPositionMs, long newPositionMs,
Timeline.Window window) { Timeline.Window window) {
State.Builder stateBuilder = oldState.buildUpon(); State.Builder stateBuilder = oldState.buildUpon();
Timeline newTimeline = oldState.timeline; Timeline newTimeline =
if (newPlaylist != null) { newPlaylist == null ? oldState.timeline : new PlaylistTimeline(newPlaylist);
stateBuilder.setPlaylist(newPlaylist);
newTimeline = stateBuilder.timeline;
}
if (oldState.playbackState != Player.STATE_IDLE) { if (oldState.playbackState != Player.STATE_IDLE) {
if (newTimeline.isEmpty() if (newTimeline.isEmpty()
|| (newIndex != C.INDEX_UNSET && newIndex >= newTimeline.getWindowCount())) { || (newIndex != C.INDEX_UNSET && newIndex >= newTimeline.getWindowCount())) {
@ -4060,6 +4136,19 @@ public abstract class SimpleBasePlayer extends BasePlayer {
.getWindow(getCurrentMediaItemIndexInternal(oldState), window) .getWindow(getCurrentMediaItemIndexInternal(oldState), window)
.uid .uid
.equals(newTimeline.getWindow(newIndex, 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) { if (oldOrNewPlaylistEmpty || mediaItemChanged || newPositionMs < oldPositionMs) {
// New item or seeking back. Assume no buffer and no ad playback persists. // New item or seeking back. Assume no buffer and no ad playback persists.
stateBuilder stateBuilder
@ -4098,9 +4187,36 @@ public abstract class SimpleBasePlayer extends BasePlayer {
return stateBuilder.build(); return stateBuilder.build();
} }
private static List<MediaItemData> buildMutablePlaylistFromState(State state) { 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); 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 {} private static final class PlaceholderUid {}
} }

View File

@ -575,7 +575,8 @@ public abstract class Timeline {
*/ */
public boolean isPlaceholder; 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. */ /** Creates a new instance with no ad playback state. */
public Period() { 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.Cue;
import androidx.media3.common.text.CueGroup; import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Size; import androidx.media3.common.util.Size;
import androidx.media3.extractor.metadata.icy.IcyInfo;
import androidx.media3.test.utils.FakeMetadataEntry; import androidx.media3.test.utils.FakeMetadataEntry;
import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.TestUtil; import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -225,6 +227,15 @@ public class SimpleBasePlayerTest {
Size surfaceSize = new Size(480, 360); Size surfaceSize = new Size(480, 360);
DeviceInfo deviceInfo = DeviceInfo deviceInfo =
new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL).setMaxVolume(7).build(); 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<SimpleBasePlayer.MediaItemData> playlist =
ImmutableList.of( ImmutableList.of(
new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(), new SimpleBasePlayer.MediaItemData.Builder(/* uid= */ new Object()).build(),
@ -236,6 +247,8 @@ public class SimpleBasePlayerTest {
new AdPlaybackState( new AdPlaybackState(
/* adsId= */ new Object(), /* adGroupTimesUs...= */ 555, 666)) /* adsId= */ new Object(), /* adGroupTimesUs...= */ 555, 666))
.build())) .build()))
.setMediaMetadata(mediaMetadata)
.setTracks(tracks)
.build()); .build());
MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build(); MediaMetadata playlistMetadata = new MediaMetadata.Builder().setArtist("artist").build();
SimpleBasePlayer.PositionSupplier contentPositionSupplier = () -> 456; SimpleBasePlayer.PositionSupplier contentPositionSupplier = () -> 456;
@ -314,6 +327,8 @@ public class SimpleBasePlayerTest {
assertThat(state.timedMetadata).isEqualTo(timedMetadata); assertThat(state.timedMetadata).isEqualTo(timedMetadata);
assertThat(state.getPlaylist()).isEqualTo(playlist); assertThat(state.getPlaylist()).isEqualTo(playlist);
assertThat(state.timeline.getWindowCount()).isEqualTo(2); assertThat(state.timeline.getWindowCount()).isEqualTo(2);
assertThat(state.currentTracks).isEqualTo(tracks);
assertThat(state.currentMetadata).isEqualTo(mediaMetadata);
assertThat(state.playlistMetadata).isEqualTo(playlistMetadata); assertThat(state.playlistMetadata).isEqualTo(playlistMetadata);
assertThat(state.currentMediaItemIndex).isEqualTo(1); assertThat(state.currentMediaItemIndex).isEqualTo(1);
assertThat(state.currentAdGroupIndex).isEqualTo(1); assertThat(state.currentAdGroupIndex).isEqualTo(1);
@ -328,6 +343,69 @@ public class SimpleBasePlayerTest {
assertThat(state.discontinuityPositionMs).isEqualTo(400); 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 @Test
public void stateBuilderBuild_emptyTimelineWithReadyState_throwsException() { public void stateBuilderBuild_emptyTimelineWithReadyState_throwsException() {
assertThrows( assertThrows(
@ -8070,6 +8148,211 @@ public class SimpleBasePlayerTest {
verifyNoMoreInteractions(listener); 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 @Test
public void seekTo_withoutAvailableCommandForSeekToMediaItem_isNotForwarded() { public void seekTo_withoutAvailableCommandForSeekToMediaItem_isNotForwarded() {
State state = State state =