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:
parent
fafd927702
commit
b2585aad0f
@ -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
|
||||
|
@ -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 {}
|
||||
|
@ -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() {
|
||||
|
@ -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 =
|
||||
|
Loading…
x
Reference in New Issue
Block a user