mirror of
https://github.com/samsonjs/media.git
synced 2026-04-04 11:05:47 +00:00
Playlist API: add Playlist and PlaylistTest
PiperOrigin-RevId: 278875587
This commit is contained in:
parent
cd2c1f2f24
commit
5c2806ecca
5 changed files with 1222 additions and 5 deletions
|
|
@ -13,16 +13,14 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
/** Abstract base class for the concatenation of one or more {@link Timeline}s. */
|
||||
/* package */ abstract class AbstractConcatenatedTimeline extends Timeline {
|
||||
public abstract class AbstractConcatenatedTimeline extends Timeline {
|
||||
|
||||
private final int childCount;
|
||||
private final ShuffleOrder shuffleOrder;
|
||||
|
|
@ -0,0 +1,707 @@
|
|||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import android.os.Handler;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
||||
import com.google.android.exoplayer2.source.MaskingMediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MaskingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
|
||||
* during playback. It is valid for the same {@link MediaSource} instance to be present more than
|
||||
* once in the playlist.
|
||||
*
|
||||
* <p>With the exception of the constructor, all methods are called on the playback thread.
|
||||
*/
|
||||
/* package */ class Playlist {
|
||||
|
||||
/** Listener for source events. */
|
||||
public interface PlaylistInfoRefreshListener {
|
||||
|
||||
/**
|
||||
* Called when the timeline of a media item has changed and a new timeline that reflects the
|
||||
* current playlist state needs to be created by calling {@link #createTimeline()}.
|
||||
*
|
||||
* <p>Called on the playback thread.
|
||||
*/
|
||||
void onPlaylistUpdateRequested();
|
||||
}
|
||||
|
||||
private final List<MediaSourceHolder> mediaSourceHolders;
|
||||
private final Map<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
|
||||
private final Map<Object, MediaSourceHolder> mediaSourceByUid;
|
||||
private final PlaylistInfoRefreshListener playlistInfoListener;
|
||||
private final MediaSourceEventListener.EventDispatcher eventDispatcher;
|
||||
private final HashMap<Playlist.MediaSourceHolder, MediaSourceAndListener> childSources;
|
||||
private final Set<MediaSourceHolder> enabledMediaSourceHolders;
|
||||
|
||||
private ShuffleOrder shuffleOrder;
|
||||
private boolean isPrepared;
|
||||
|
||||
@Nullable private TransferListener mediaTransferListener;
|
||||
|
||||
@SuppressWarnings("initialization")
|
||||
public Playlist(PlaylistInfoRefreshListener listener) {
|
||||
playlistInfoListener = listener;
|
||||
shuffleOrder = new DefaultShuffleOrder(0);
|
||||
mediaSourceByMediaPeriod = new IdentityHashMap<>();
|
||||
mediaSourceByUid = new HashMap<>();
|
||||
mediaSourceHolders = new ArrayList<>();
|
||||
eventDispatcher = new MediaSourceEventListener.EventDispatcher();
|
||||
childSources = new HashMap<>();
|
||||
enabledMediaSourceHolders = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the media sources replacing any sources previously contained in the playlist.
|
||||
*
|
||||
* @param holders The list of {@link MediaSourceHolder}s to set.
|
||||
* @param shuffleOrder The new shuffle order.
|
||||
* @return The new {@link Timeline}.
|
||||
*/
|
||||
public final Timeline setMediaSources(
|
||||
List<MediaSourceHolder> holders, ShuffleOrder shuffleOrder) {
|
||||
removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size());
|
||||
return addMediaSources(/* index= */ this.mediaSourceHolders.size(), holders, shuffleOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple {@link MediaSourceHolder}s to the playlist.
|
||||
*
|
||||
* @param index The index at which the new {@link MediaSourceHolder}s will be inserted. This index
|
||||
* must be in the range of 0 <= index <= {@link #getSize()}.
|
||||
* @param holders A list of {@link MediaSourceHolder}s to be added.
|
||||
* @param shuffleOrder The new shuffle order.
|
||||
* @return The new {@link Timeline}.
|
||||
*/
|
||||
public final Timeline addMediaSources(
|
||||
int index, List<MediaSourceHolder> holders, ShuffleOrder shuffleOrder) {
|
||||
if (!holders.isEmpty()) {
|
||||
this.shuffleOrder = shuffleOrder;
|
||||
for (int insertionIndex = index; insertionIndex < index + holders.size(); insertionIndex++) {
|
||||
MediaSourceHolder holder = holders.get(insertionIndex - index);
|
||||
if (insertionIndex > 0) {
|
||||
MediaSourceHolder previousHolder = mediaSourceHolders.get(insertionIndex - 1);
|
||||
Timeline previousTimeline = previousHolder.mediaSource.getTimeline();
|
||||
holder.reset(
|
||||
/* firstWindowInChildIndex= */ previousHolder.firstWindowIndexInChild
|
||||
+ previousTimeline.getWindowCount());
|
||||
} else {
|
||||
holder.reset(/* firstWindowIndexInChild= */ 0);
|
||||
}
|
||||
Timeline newTimeline = holder.mediaSource.getTimeline();
|
||||
correctOffsets(
|
||||
/* startIndex= */ insertionIndex,
|
||||
/* windowOffsetUpdate= */ newTimeline.getWindowCount());
|
||||
mediaSourceHolders.add(insertionIndex, holder);
|
||||
mediaSourceByUid.put(holder.uid, holder);
|
||||
if (isPrepared) {
|
||||
prepareChildSource(holder);
|
||||
if (mediaSourceByMediaPeriod.isEmpty()) {
|
||||
enabledMediaSourceHolders.add(holder);
|
||||
} else {
|
||||
disableChildSource(holder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return createTimeline();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a range of {@link MediaSourceHolder}s from the playlist, by specifying an initial index
|
||||
* (included) and a final index (excluded).
|
||||
*
|
||||
* <p>Note: when specified range is empty, no actual media source is removed and no exception is
|
||||
* thrown.
|
||||
*
|
||||
* @param fromIndex The initial range index, pointing to the first media source that will be
|
||||
* removed. This index must be in the range of 0 <= index <= {@link #getSize()}.
|
||||
* @param toIndex The final range index, pointing to the first media source that will be left
|
||||
* untouched. This index must be in the range of 0 <= index <= {@link #getSize()}.
|
||||
* @param shuffleOrder The new shuffle order.
|
||||
* @return The new {@link Timeline}.
|
||||
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0,
|
||||
* {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex}
|
||||
*/
|
||||
public final Timeline removeMediaSourceRange(
|
||||
int fromIndex, int toIndex, ShuffleOrder shuffleOrder) {
|
||||
Assertions.checkArgument(fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize());
|
||||
this.shuffleOrder = shuffleOrder;
|
||||
removeMediaSourcesInternal(fromIndex, toIndex);
|
||||
return createTimeline();
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves an existing media source within the playlist.
|
||||
*
|
||||
* @param currentIndex The current index of the media source in the playlist. This index must be
|
||||
* in the range of 0 <= index < {@link #getSize()}.
|
||||
* @param newIndex The target index of the media source in the playlist. This index must be in the
|
||||
* range of 0 <= index < {@link #getSize()}.
|
||||
* @param shuffleOrder The new shuffle order.
|
||||
* @return The new {@link Timeline}.
|
||||
* @throws IllegalArgumentException When an index is invalid, i.e. {@code currentIndex} < 0,
|
||||
* {@code currentIndex} >= {@link #getSize()}, {@code newIndex} < 0
|
||||
*/
|
||||
public final Timeline moveMediaSource(int currentIndex, int newIndex, ShuffleOrder shuffleOrder) {
|
||||
return moveMediaSourceRange(currentIndex, currentIndex + 1, newIndex, shuffleOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a range of media sources within the playlist.
|
||||
*
|
||||
* <p>Note: when specified range is empty or the from index equals the new from index, no actual
|
||||
* media source is moved and no exception is thrown.
|
||||
*
|
||||
* @param fromIndex The initial range index, pointing to the first media source of the range that
|
||||
* will be moved. This index must be in the range of 0 <= index <= {@link #getSize()}.
|
||||
* @param toIndex The final range index, pointing to the first media source that will be left
|
||||
* untouched. This index must be larger or equals than {@code fromIndex}.
|
||||
* @param newFromIndex The target index of the first media source of the range that will be moved.
|
||||
* @param shuffleOrder The new shuffle order.
|
||||
* @return The new {@link Timeline}.
|
||||
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0,
|
||||
* {@code toIndex} < {@code fromIndex}, {@code fromIndex} > {@code toIndex}, {@code
|
||||
* newFromIndex} < 0
|
||||
*/
|
||||
public Timeline moveMediaSourceRange(
|
||||
int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) {
|
||||
Assertions.checkArgument(
|
||||
fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize() && newFromIndex >= 0);
|
||||
this.shuffleOrder = shuffleOrder;
|
||||
if (fromIndex == toIndex || fromIndex == newFromIndex) {
|
||||
return createTimeline();
|
||||
}
|
||||
int startIndex = Math.min(fromIndex, newFromIndex);
|
||||
int newEndIndex = newFromIndex + (toIndex - fromIndex) - 1;
|
||||
int endIndex = Math.max(newEndIndex, toIndex - 1);
|
||||
int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild;
|
||||
moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex);
|
||||
for (int i = startIndex; i <= endIndex; i++) {
|
||||
MediaSourceHolder holder = mediaSourceHolders.get(i);
|
||||
holder.firstWindowIndexInChild = windowOffset;
|
||||
windowOffset += holder.mediaSource.getTimeline().getWindowCount();
|
||||
}
|
||||
return createTimeline();
|
||||
}
|
||||
|
||||
/** Clears the playlist. */
|
||||
public final Timeline clear(@Nullable ShuffleOrder shuffleOrder) {
|
||||
this.shuffleOrder = shuffleOrder != null ? shuffleOrder : this.shuffleOrder.cloneAndClear();
|
||||
removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ getSize());
|
||||
return createTimeline();
|
||||
}
|
||||
|
||||
/** Whether the playlist is prepared. */
|
||||
public final boolean isPrepared() {
|
||||
return isPrepared;
|
||||
}
|
||||
|
||||
/** Returns the number of media sources in the playlist. */
|
||||
public final int getSize() {
|
||||
return mediaSourceHolders.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link AnalyticsCollector}.
|
||||
*
|
||||
* @param handler The handler on which to call the collector.
|
||||
* @param analyticsCollector The analytics collector.
|
||||
*/
|
||||
public final void setAnalyticsCollector(Handler handler, AnalyticsCollector analyticsCollector) {
|
||||
eventDispatcher.addEventListener(handler, analyticsCollector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new shuffle order to use when shuffling the child media sources.
|
||||
*
|
||||
* @param shuffleOrder A {@link ShuffleOrder}.
|
||||
*/
|
||||
public final Timeline setShuffleOrder(ShuffleOrder shuffleOrder) {
|
||||
int size = getSize();
|
||||
if (shuffleOrder.getLength() != size) {
|
||||
shuffleOrder =
|
||||
shuffleOrder
|
||||
.cloneAndClear()
|
||||
.cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
|
||||
}
|
||||
this.shuffleOrder = shuffleOrder;
|
||||
return createTimeline();
|
||||
}
|
||||
|
||||
/** Prepares the playlist. */
|
||||
public final void prepare(@Nullable TransferListener mediaTransferListener) {
|
||||
Assertions.checkState(!isPrepared);
|
||||
this.mediaTransferListener = mediaTransferListener;
|
||||
for (int i = 0; i < mediaSourceHolders.size(); i++) {
|
||||
MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
|
||||
prepareChildSource(mediaSourceHolder);
|
||||
enabledMediaSourceHolders.add(mediaSourceHolder);
|
||||
}
|
||||
isPrepared = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link MediaPeriod} identified by {@code periodId}.
|
||||
*
|
||||
* @param id The identifier of the period.
|
||||
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
|
||||
* @param startPositionUs The expected start position, in microseconds.
|
||||
* @return A new {@link MediaPeriod}.
|
||||
*/
|
||||
public MediaPeriod createPeriod(
|
||||
MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||
Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
|
||||
MediaSource.MediaPeriodId childMediaPeriodId =
|
||||
id.copyWithPeriodUid(getChildPeriodUid(id.periodUid));
|
||||
MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid));
|
||||
enableMediaSource(holder);
|
||||
holder.activeMediaPeriodIds.add(childMediaPeriodId);
|
||||
MediaPeriod mediaPeriod =
|
||||
holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
|
||||
mediaSourceByMediaPeriod.put(mediaPeriod, holder);
|
||||
disableUnusedMediaSources();
|
||||
return mediaPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the period.
|
||||
*
|
||||
* @param mediaPeriod The period to release.
|
||||
*/
|
||||
public final void releasePeriod(MediaPeriod mediaPeriod) {
|
||||
MediaSourceHolder holder =
|
||||
Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
|
||||
holder.mediaSource.releasePeriod(mediaPeriod);
|
||||
holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id);
|
||||
if (!mediaSourceByMediaPeriod.isEmpty()) {
|
||||
disableUnusedMediaSources();
|
||||
}
|
||||
maybeReleaseChildSource(holder);
|
||||
}
|
||||
|
||||
/** Releases the playlist. */
|
||||
public final void release() {
|
||||
for (MediaSourceAndListener childSource : childSources.values()) {
|
||||
childSource.mediaSource.releaseSource(childSource.caller);
|
||||
childSource.mediaSource.removeEventListener(childSource.eventListener);
|
||||
}
|
||||
childSources.clear();
|
||||
enabledMediaSourceHolders.clear();
|
||||
isPrepared = false;
|
||||
}
|
||||
|
||||
/** Throws any pending error encountered while loading or refreshing. */
|
||||
public final void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||
for (MediaSourceAndListener childSource : childSources.values()) {
|
||||
childSource.mediaSource.maybeThrowSourceInfoRefreshError();
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates a timeline reflecting the current state of the playlist. */
|
||||
public final Timeline createTimeline() {
|
||||
if (mediaSourceHolders.isEmpty()) {
|
||||
return Timeline.EMPTY;
|
||||
}
|
||||
int windowOffset = 0;
|
||||
for (int i = 0; i < mediaSourceHolders.size(); i++) {
|
||||
MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
|
||||
mediaSourceHolder.firstWindowIndexInChild = windowOffset;
|
||||
windowOffset += mediaSourceHolder.mediaSource.getTimeline().getWindowCount();
|
||||
}
|
||||
return new PlaylistTimeline(mediaSourceHolders, shuffleOrder);
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void enableMediaSource(MediaSourceHolder mediaSourceHolder) {
|
||||
enabledMediaSourceHolders.add(mediaSourceHolder);
|
||||
@Nullable MediaSourceAndListener enabledChild = childSources.get(mediaSourceHolder);
|
||||
if (enabledChild != null) {
|
||||
enabledChild.mediaSource.enable(enabledChild.caller);
|
||||
}
|
||||
}
|
||||
|
||||
private void disableUnusedMediaSources() {
|
||||
Iterator<MediaSourceHolder> iterator = enabledMediaSourceHolders.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
MediaSourceHolder holder = iterator.next();
|
||||
if (holder.activeMediaPeriodIds.isEmpty()) {
|
||||
disableChildSource(holder);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void disableChildSource(MediaSourceHolder holder) {
|
||||
@Nullable MediaSourceAndListener disabledChild = childSources.get(holder);
|
||||
if (disabledChild != null) {
|
||||
disabledChild.mediaSource.disable(disabledChild.caller);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeMediaSourcesInternal(int fromIndex, int toIndex) {
|
||||
for (int index = toIndex - 1; index >= fromIndex; index--) {
|
||||
MediaSourceHolder holder = mediaSourceHolders.remove(index);
|
||||
mediaSourceByUid.remove(holder.uid);
|
||||
Timeline oldTimeline = holder.mediaSource.getTimeline();
|
||||
correctOffsets(
|
||||
/* startIndex= */ index, /* windowOffsetUpdate= */ -oldTimeline.getWindowCount());
|
||||
holder.isRemoved = true;
|
||||
if (isPrepared) {
|
||||
maybeReleaseChildSource(holder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void correctOffsets(int startIndex, int windowOffsetUpdate) {
|
||||
for (int i = startIndex; i < mediaSourceHolders.size(); i++) {
|
||||
MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
|
||||
mediaSourceHolder.firstWindowIndexInChild += windowOffsetUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
// Internal methods to manage child sources.
|
||||
|
||||
@Nullable
|
||||
private static MediaSource.MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
|
||||
MediaSourceHolder mediaSourceHolder, MediaSource.MediaPeriodId mediaPeriodId) {
|
||||
for (int i = 0; i < mediaSourceHolder.activeMediaPeriodIds.size(); i++) {
|
||||
// Ensure the reported media period id has the same window sequence number as the one created
|
||||
// by this media source. Otherwise it does not belong to this child source.
|
||||
if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber
|
||||
== mediaPeriodId.windowSequenceNumber) {
|
||||
Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid);
|
||||
return mediaPeriodId.copyWithPeriodUid(periodUid);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int getWindowIndexForChildWindowIndex(
|
||||
MediaSourceHolder mediaSourceHolder, int windowIndex) {
|
||||
return windowIndex + mediaSourceHolder.firstWindowIndexInChild;
|
||||
}
|
||||
|
||||
private void prepareChildSource(MediaSourceHolder holder) {
|
||||
MediaSource mediaSource = holder.mediaSource;
|
||||
MediaSource.MediaSourceCaller caller =
|
||||
(source, timeline) -> playlistInfoListener.onPlaylistUpdateRequested();
|
||||
MediaSourceEventListener eventListener = new ForwardingEventListener(holder);
|
||||
childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener));
|
||||
mediaSource.addEventListener(new Handler(), eventListener);
|
||||
mediaSource.prepareSource(caller, mediaTransferListener);
|
||||
}
|
||||
|
||||
private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {
|
||||
// Release if the source has been removed from the playlist and no periods are still active.
|
||||
if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) {
|
||||
MediaSourceAndListener removedChild =
|
||||
Assertions.checkNotNull(childSources.remove(mediaSourceHolder));
|
||||
removedChild.mediaSource.releaseSource(removedChild.caller);
|
||||
removedChild.mediaSource.removeEventListener(removedChild.eventListener);
|
||||
enabledMediaSourceHolders.remove(mediaSourceHolder);
|
||||
}
|
||||
}
|
||||
|
||||
/** Return uid of media source holder from period uid of concatenated source. */
|
||||
private static Object getMediaSourceHolderUid(Object periodUid) {
|
||||
return PlaylistTimeline.getChildTimelineUidFromConcatenatedUid(periodUid);
|
||||
}
|
||||
|
||||
/** Return uid of child period from period uid of concatenated source. */
|
||||
private static Object getChildPeriodUid(Object periodUid) {
|
||||
return PlaylistTimeline.getChildPeriodUidFromConcatenatedUid(periodUid);
|
||||
}
|
||||
|
||||
private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) {
|
||||
return PlaylistTimeline.getConcatenatedUid(holder.uid, childPeriodUid);
|
||||
}
|
||||
|
||||
/* package */ static void moveMediaSourceHolders(
|
||||
List<MediaSourceHolder> mediaSourceHolders, int fromIndex, int toIndex, int newFromIndex) {
|
||||
MediaSourceHolder[] removedItems = new MediaSourceHolder[toIndex - fromIndex];
|
||||
for (int i = removedItems.length - 1; i >= 0; i--) {
|
||||
removedItems[i] = mediaSourceHolders.remove(fromIndex + i);
|
||||
}
|
||||
mediaSourceHolders.addAll(
|
||||
Math.min(newFromIndex, mediaSourceHolders.size()), Arrays.asList(removedItems));
|
||||
}
|
||||
|
||||
/** Data class to hold playlist media sources together with meta data needed to process them. */
|
||||
/* package */ static final class MediaSourceHolder {
|
||||
|
||||
public final MaskingMediaSource mediaSource;
|
||||
public final Object uid;
|
||||
public final List<MediaSource.MediaPeriodId> activeMediaPeriodIds;
|
||||
|
||||
public int firstWindowIndexInChild;
|
||||
public boolean isRemoved;
|
||||
|
||||
public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) {
|
||||
this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation);
|
||||
this.activeMediaPeriodIds = new ArrayList<>();
|
||||
this.uid = new Object();
|
||||
}
|
||||
|
||||
public void reset(int firstWindowIndexInChild) {
|
||||
this.firstWindowIndexInChild = firstWindowIndexInChild;
|
||||
this.isRemoved = false;
|
||||
this.activeMediaPeriodIds.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/** Timeline exposing concatenated timelines of playlist media sources. */
|
||||
/* package */ static final class PlaylistTimeline extends AbstractConcatenatedTimeline {
|
||||
|
||||
private final int windowCount;
|
||||
private final int periodCount;
|
||||
private final int[] firstPeriodInChildIndices;
|
||||
private final int[] firstWindowInChildIndices;
|
||||
private final Timeline[] timelines;
|
||||
private final Object[] uids;
|
||||
private final HashMap<Object, Integer> childIndexByUid;
|
||||
|
||||
public PlaylistTimeline(
|
||||
Collection<MediaSourceHolder> mediaSourceHolders, ShuffleOrder shuffleOrder) {
|
||||
super(/* isAtomic= */ false, shuffleOrder);
|
||||
int childCount = mediaSourceHolders.size();
|
||||
firstPeriodInChildIndices = new int[childCount];
|
||||
firstWindowInChildIndices = new int[childCount];
|
||||
timelines = new Timeline[childCount];
|
||||
uids = new Object[childCount];
|
||||
childIndexByUid = new HashMap<>();
|
||||
int index = 0;
|
||||
int windowCount = 0;
|
||||
int periodCount = 0;
|
||||
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
|
||||
timelines[index] = mediaSourceHolder.mediaSource.getTimeline();
|
||||
firstWindowInChildIndices[index] = windowCount;
|
||||
firstPeriodInChildIndices[index] = periodCount;
|
||||
windowCount += timelines[index].getWindowCount();
|
||||
periodCount += timelines[index].getPeriodCount();
|
||||
uids[index] = mediaSourceHolder.uid;
|
||||
childIndexByUid.put(uids[index], index++);
|
||||
}
|
||||
this.windowCount = windowCount;
|
||||
this.periodCount = periodCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getChildIndexByPeriodIndex(int periodIndex) {
|
||||
return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getChildIndexByWindowIndex(int windowIndex) {
|
||||
return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getChildIndexByChildUid(Object childUid) {
|
||||
Integer index = childIndexByUid.get(childUid);
|
||||
return index == null ? C.INDEX_UNSET : index;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Timeline getTimelineByChildIndex(int childIndex) {
|
||||
return timelines[childIndex];
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getFirstPeriodIndexByChildIndex(int childIndex) {
|
||||
return firstPeriodInChildIndices[childIndex];
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getFirstWindowIndexByChildIndex(int childIndex) {
|
||||
return firstWindowInChildIndices[childIndex];
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getChildUidByChildIndex(int childIndex) {
|
||||
return uids[childIndex];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWindowCount() {
|
||||
return windowCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPeriodCount() {
|
||||
return periodCount;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MediaSourceAndListener {
|
||||
|
||||
public final MediaSource mediaSource;
|
||||
public final MediaSource.MediaSourceCaller caller;
|
||||
public final MediaSourceEventListener eventListener;
|
||||
|
||||
public MediaSourceAndListener(
|
||||
MediaSource mediaSource,
|
||||
MediaSource.MediaSourceCaller caller,
|
||||
MediaSourceEventListener eventListener) {
|
||||
this.mediaSource = mediaSource;
|
||||
this.caller = caller;
|
||||
this.eventListener = eventListener;
|
||||
}
|
||||
}
|
||||
|
||||
private final class ForwardingEventListener implements MediaSourceEventListener {
|
||||
|
||||
private final Playlist.MediaSourceHolder id;
|
||||
private EventDispatcher eventDispatcher;
|
||||
|
||||
public ForwardingEventListener(Playlist.MediaSourceHolder id) {
|
||||
eventDispatcher = Playlist.this.eventDispatcher;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
eventDispatcher.mediaPeriodCreated();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
eventDispatcher.mediaPeriodReleased();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadStarted(
|
||||
int windowIndex,
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||
LoadEventInfo loadEventData,
|
||||
MediaLoadData mediaLoadData) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
eventDispatcher.loadStarted(loadEventData, mediaLoadData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(
|
||||
int windowIndex,
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||
LoadEventInfo loadEventData,
|
||||
MediaLoadData mediaLoadData) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
eventDispatcher.loadCompleted(loadEventData, mediaLoadData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(
|
||||
int windowIndex,
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||
LoadEventInfo loadEventData,
|
||||
MediaLoadData mediaLoadData) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
eventDispatcher.loadCanceled(loadEventData, mediaLoadData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadError(
|
||||
int windowIndex,
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||
LoadEventInfo loadEventData,
|
||||
MediaLoadData mediaLoadData,
|
||||
IOException error,
|
||||
boolean wasCanceled) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
eventDispatcher.loadError(loadEventData, mediaLoadData, error, wasCanceled);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
eventDispatcher.readingStarted();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpstreamDiscarded(
|
||||
int windowIndex,
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||
MediaLoadData mediaLoadData) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
eventDispatcher.upstreamDiscarded(mediaLoadData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownstreamFormatChanged(
|
||||
int windowIndex,
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||
MediaLoadData mediaLoadData) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
eventDispatcher.downstreamFormatChanged(mediaLoadData);
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the event dispatcher and returns whether the event should be dispatched. */
|
||||
private boolean maybeUpdateEventDispatcher(
|
||||
int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) {
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId = null;
|
||||
if (childMediaPeriodId != null) {
|
||||
mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId);
|
||||
if (mediaPeriodId == null) {
|
||||
// Media period not found. Ignore event.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex);
|
||||
if (eventDispatcher.windowIndex != windowIndex
|
||||
|| !Util.areEqual(eventDispatcher.mediaPeriodId, mediaPeriodId)) {
|
||||
eventDispatcher =
|
||||
Playlist.this.eventDispatcher.withParameters(
|
||||
windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import android.os.Handler;
|
|||
import android.os.Message;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.AbstractConcatenatedTimeline;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.AbstractConcatenatedTimeline;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,510 @@
|
|||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link Playlist}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class PlaylistTest {
|
||||
|
||||
private static final int PLAYLIST_SIZE = 4;
|
||||
|
||||
private Playlist playlist;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
playlist = new Playlist(mock(Playlist.PlaylistInfoRefreshListener.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyPlaylist_expectConstantTimelineInstanceEMPTY() {
|
||||
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0);
|
||||
List<Playlist.MediaSourceHolder> fakeHolders = createFakeHolders();
|
||||
|
||||
Timeline timeline = playlist.setMediaSources(fakeHolders, shuffleOrder);
|
||||
assertNotSame(timeline, Timeline.EMPTY);
|
||||
|
||||
// Remove all media sources.
|
||||
timeline =
|
||||
playlist.removeMediaSourceRange(
|
||||
/* fromIndex= */ 0, /* toIndex= */ timeline.getWindowCount(), shuffleOrder);
|
||||
assertSame(timeline, Timeline.EMPTY);
|
||||
|
||||
timeline = playlist.setMediaSources(fakeHolders, shuffleOrder);
|
||||
assertNotSame(timeline, Timeline.EMPTY);
|
||||
// Clear.
|
||||
timeline = playlist.clear(shuffleOrder);
|
||||
assertSame(timeline, Timeline.EMPTY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrepareAndReprepareAfterRelease_expectSourcePreparationAfterPlaylistPrepare() {
|
||||
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||
playlist.setMediaSources(
|
||||
createFakeHoldersWithSources(
|
||||
/* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2),
|
||||
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2));
|
||||
// Verify prepare is called once on prepare.
|
||||
verify(mockMediaSource1, times(0))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
verify(mockMediaSource2, times(0))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
|
||||
playlist.prepare(/* mediaTransferListener= */ null);
|
||||
assertThat(playlist.isPrepared()).isTrue();
|
||||
// Verify prepare is called once on prepare.
|
||||
verify(mockMediaSource1, times(1))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
verify(mockMediaSource2, times(1))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
|
||||
playlist.release();
|
||||
playlist.prepare(/* mediaTransferListener= */ null);
|
||||
// Verify prepare is called a second time on re-prepare.
|
||||
verify(mockMediaSource1, times(2))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
verify(mockMediaSource2, times(2))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetMediaSources_playlistUnprepared_notUsingLazyPreparation() {
|
||||
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2);
|
||||
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||
List<Playlist.MediaSourceHolder> mediaSources =
|
||||
createFakeHoldersWithSources(
|
||||
/* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2);
|
||||
Timeline timeline = playlist.setMediaSources(mediaSources, shuffleOrder);
|
||||
|
||||
assertThat(timeline.getWindowCount()).isEqualTo(2);
|
||||
assertThat(playlist.getSize()).isEqualTo(2);
|
||||
|
||||
// Assert holder offsets have been set properly
|
||||
for (int i = 0; i < mediaSources.size(); i++) {
|
||||
Playlist.MediaSourceHolder mediaSourceHolder = mediaSources.get(i);
|
||||
assertThat(mediaSourceHolder.isRemoved).isFalse();
|
||||
assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i);
|
||||
}
|
||||
|
||||
// Set media items again. The second holder is re-used.
|
||||
List<Playlist.MediaSourceHolder> moreMediaSources =
|
||||
createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class));
|
||||
moreMediaSources.add(mediaSources.get(1));
|
||||
timeline = playlist.setMediaSources(moreMediaSources, shuffleOrder);
|
||||
|
||||
assertThat(playlist.getSize()).isEqualTo(2);
|
||||
assertThat(timeline.getWindowCount()).isEqualTo(2);
|
||||
for (int i = 0; i < moreMediaSources.size(); i++) {
|
||||
Playlist.MediaSourceHolder mediaSourceHolder = moreMediaSources.get(i);
|
||||
assertThat(mediaSourceHolder.isRemoved).isFalse();
|
||||
assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i);
|
||||
}
|
||||
// Expect removed holders and sources to be removed without releasing.
|
||||
verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
assertThat(mediaSources.get(0).isRemoved).isTrue();
|
||||
// Expect re-used holder and source not to be removed.
|
||||
verify(mockMediaSource2, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
assertThat(mediaSources.get(1).isRemoved).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetMediaSources_playlistPrepared_notUsingLazyPreparation() {
|
||||
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2);
|
||||
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||
List<Playlist.MediaSourceHolder> mediaSources =
|
||||
createFakeHoldersWithSources(
|
||||
/* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2);
|
||||
|
||||
playlist.prepare(/* mediaTransferListener= */ null);
|
||||
playlist.setMediaSources(mediaSources, shuffleOrder);
|
||||
|
||||
// Verify sources are prepared.
|
||||
verify(mockMediaSource1, times(1))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
verify(mockMediaSource2, times(1))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
|
||||
// Set media items again. The second holder is re-used.
|
||||
List<Playlist.MediaSourceHolder> moreMediaSources =
|
||||
createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class));
|
||||
moreMediaSources.add(mediaSources.get(1));
|
||||
playlist.setMediaSources(moreMediaSources, shuffleOrder);
|
||||
|
||||
// Expect removed holders and sources to be removed and released.
|
||||
verify(mockMediaSource1, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
assertThat(mediaSources.get(0).isRemoved).isTrue();
|
||||
// Expect re-used holder and source not to be removed but released.
|
||||
verify(mockMediaSource2, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
assertThat(mediaSources.get(1).isRemoved).isFalse();
|
||||
verify(mockMediaSource2, times(2))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddMediaSources_playlistUnprepared_notUsingLazyPreparation_expectUnprepared() {
|
||||
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||
List<Playlist.MediaSourceHolder> mediaSources =
|
||||
createFakeHoldersWithSources(
|
||||
/* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2);
|
||||
playlist.addMediaSources(/* index= */ 0, mediaSources, new ShuffleOrder.DefaultShuffleOrder(2));
|
||||
|
||||
assertThat(playlist.getSize()).isEqualTo(2);
|
||||
// Verify lazy initialization does not call prepare on sources.
|
||||
verify(mockMediaSource1, times(0))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
verify(mockMediaSource2, times(0))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
|
||||
for (int i = 0; i < mediaSources.size(); i++) {
|
||||
assertThat(mediaSources.get(i).firstWindowIndexInChild).isEqualTo(i);
|
||||
assertThat(mediaSources.get(i).isRemoved).isFalse();
|
||||
}
|
||||
|
||||
// Add for more sources in between.
|
||||
List<Playlist.MediaSourceHolder> moreMediaSources = createFakeHolders();
|
||||
playlist.addMediaSources(
|
||||
/* index= */ 1, moreMediaSources, new ShuffleOrder.DefaultShuffleOrder(/* length= */ 3));
|
||||
|
||||
assertThat(mediaSources.get(0).firstWindowIndexInChild).isEqualTo(0);
|
||||
assertThat(moreMediaSources.get(0).firstWindowIndexInChild).isEqualTo(1);
|
||||
assertThat(moreMediaSources.get(3).firstWindowIndexInChild).isEqualTo(4);
|
||||
assertThat(mediaSources.get(1).firstWindowIndexInChild).isEqualTo(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddMediaSources_playlistPrepared_notUsingLazyPreparation_expectPrepared() {
|
||||
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||
playlist.prepare(/* mediaTransferListener= */ null);
|
||||
playlist.addMediaSources(
|
||||
/* index= */ 0,
|
||||
createFakeHoldersWithSources(
|
||||
/* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2),
|
||||
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2));
|
||||
|
||||
// Verify prepare is called on sources when added.
|
||||
verify(mockMediaSource1, times(1))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
verify(mockMediaSource2, times(1))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMoveMediaSources() {
|
||||
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4);
|
||||
List<Playlist.MediaSourceHolder> holders = createFakeHolders();
|
||||
playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder);
|
||||
|
||||
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||
playlist.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 3, shuffleOrder);
|
||||
assertFirstWindowInChildIndices(holders, 3, 0, 1, 2);
|
||||
playlist.moveMediaSource(/* currentIndex= */ 3, /* newIndex= */ 0, shuffleOrder);
|
||||
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||
|
||||
playlist.moveMediaSourceRange(
|
||||
/* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder);
|
||||
assertFirstWindowInChildIndices(holders, 2, 3, 0, 1);
|
||||
playlist.moveMediaSourceRange(
|
||||
/* fromIndex= */ 2, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder);
|
||||
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||
|
||||
playlist.moveMediaSourceRange(
|
||||
/* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder);
|
||||
assertFirstWindowInChildIndices(holders, 2, 3, 0, 1);
|
||||
playlist.moveMediaSourceRange(
|
||||
/* fromIndex= */ 2, /* toIndex= */ 3, /* newFromIndex= */ 0, shuffleOrder);
|
||||
assertFirstWindowInChildIndices(holders, 0, 3, 1, 2);
|
||||
playlist.moveMediaSourceRange(
|
||||
/* fromIndex= */ 3, /* toIndex= */ 4, /* newFromIndex= */ 1, shuffleOrder);
|
||||
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||
|
||||
// No-ops.
|
||||
playlist.moveMediaSourceRange(
|
||||
/* fromIndex= */ 0, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder);
|
||||
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||
playlist.moveMediaSourceRange(
|
||||
/* fromIndex= */ 0, /* toIndex= */ 0, /* newFromIndex= */ 3, shuffleOrder);
|
||||
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveMediaSources_whenUnprepared_expectNoRelease() {
|
||||
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||
MediaSource mockMediaSource3 = mock(MediaSource.class);
|
||||
MediaSource mockMediaSource4 = mock(MediaSource.class);
|
||||
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4);
|
||||
|
||||
List<Playlist.MediaSourceHolder> holders =
|
||||
createFakeHoldersWithSources(
|
||||
/* useLazyPreparation= */ false,
|
||||
mockMediaSource1,
|
||||
mockMediaSource2,
|
||||
mockMediaSource3,
|
||||
mockMediaSource4);
|
||||
playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder);
|
||||
playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder);
|
||||
|
||||
assertThat(playlist.getSize()).isEqualTo(2);
|
||||
Playlist.MediaSourceHolder removedHolder1 = holders.remove(1);
|
||||
Playlist.MediaSourceHolder removedHolder2 = holders.remove(1);
|
||||
|
||||
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||
assertThat(removedHolder1.isRemoved).isTrue();
|
||||
assertThat(removedHolder2.isRemoved).isTrue();
|
||||
verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
verify(mockMediaSource2, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
verify(mockMediaSource3, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
verify(mockMediaSource4, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveMediaSources_whenPrepared_expectRelease() {
|
||||
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||
MediaSource mockMediaSource3 = mock(MediaSource.class);
|
||||
MediaSource mockMediaSource4 = mock(MediaSource.class);
|
||||
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4);
|
||||
|
||||
List<Playlist.MediaSourceHolder> holders =
|
||||
createFakeHoldersWithSources(
|
||||
/* useLazyPreparation= */ false,
|
||||
mockMediaSource1,
|
||||
mockMediaSource2,
|
||||
mockMediaSource3,
|
||||
mockMediaSource4);
|
||||
playlist.prepare(/* mediaTransferListener */ null);
|
||||
playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder);
|
||||
playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder);
|
||||
|
||||
assertThat(playlist.getSize()).isEqualTo(2);
|
||||
holders.remove(2);
|
||||
holders.remove(1);
|
||||
|
||||
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||
verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
verify(mockMediaSource2, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
verify(mockMediaSource3, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
verify(mockMediaSource4, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRelease_playlistUnprepared_expectSourcesNotReleased() {
|
||||
MediaSource mockMediaSource = mock(MediaSource.class);
|
||||
Playlist.MediaSourceHolder mediaSourceHolder =
|
||||
new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false);
|
||||
|
||||
playlist.setMediaSources(
|
||||
Collections.singletonList(mediaSourceHolder),
|
||||
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1));
|
||||
verify(mockMediaSource, times(0))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
playlist.release();
|
||||
verify(mockMediaSource, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
assertThat(mediaSourceHolder.isRemoved).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRelease_playlistPrepared_expectSourcesReleasedNotRemoved() {
|
||||
MediaSource mockMediaSource = mock(MediaSource.class);
|
||||
Playlist.MediaSourceHolder mediaSourceHolder =
|
||||
new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false);
|
||||
|
||||
playlist.prepare(/* mediaTransferListener= */ null);
|
||||
playlist.setMediaSources(
|
||||
Collections.singletonList(mediaSourceHolder),
|
||||
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1));
|
||||
verify(mockMediaSource, times(1))
|
||||
.prepareSource(
|
||||
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||
playlist.release();
|
||||
verify(mockMediaSource, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||
assertThat(mediaSourceHolder.isRemoved).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClearPlaylist_expectSourcesReleasedAndRemoved() {
|
||||
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4);
|
||||
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||
List<Playlist.MediaSourceHolder> holders =
|
||||
createFakeHoldersWithSources(
|
||||
/* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2);
|
||||
playlist.setMediaSources(holders, shuffleOrder);
|
||||
playlist.prepare(/* mediaTransferListener= */ null);
|
||||
|
||||
Timeline timeline = playlist.clear(shuffleOrder);
|
||||
assertThat(timeline.isEmpty()).isTrue();
|
||||
assertThat(holders.get(0).isRemoved).isTrue();
|
||||
assertThat(holders.get(1).isRemoved).isTrue();
|
||||
verify(mockMediaSource1, times(1)).releaseSource(any());
|
||||
verify(mockMediaSource2, times(1)).releaseSource(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetMediaSources_expectTimelineUsesCustomShuffleOrder() {
|
||||
Timeline timeline =
|
||||
playlist.setMediaSources(createFakeHolders(), new FakeShuffleOrder(/* length=*/ 4));
|
||||
assertTimelineUsesFakeShuffleOrder(timeline);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddMediaSources_expectTimelineUsesCustomShuffleOrder() {
|
||||
Timeline timeline =
|
||||
playlist.addMediaSources(
|
||||
/* index= */ 0, createFakeHolders(), new FakeShuffleOrder(PLAYLIST_SIZE));
|
||||
assertTimelineUsesFakeShuffleOrder(timeline);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMoveMediaSources_expectTimelineUsesCustomShuffleOrder() {
|
||||
ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE);
|
||||
playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder);
|
||||
Timeline timeline =
|
||||
playlist.moveMediaSource(
|
||||
/* currentIndex= */ 0, /* newIndex= */ 1, new FakeShuffleOrder(PLAYLIST_SIZE));
|
||||
assertTimelineUsesFakeShuffleOrder(timeline);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() {
|
||||
ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE);
|
||||
playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder);
|
||||
Timeline timeline =
|
||||
playlist.moveMediaSourceRange(
|
||||
/* fromIndex= */ 0,
|
||||
/* toIndex= */ 2,
|
||||
/* newFromIndex= */ 2,
|
||||
new FakeShuffleOrder(PLAYLIST_SIZE));
|
||||
assertTimelineUsesFakeShuffleOrder(timeline);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() {
|
||||
ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE);
|
||||
playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder);
|
||||
Timeline timeline =
|
||||
playlist.removeMediaSourceRange(
|
||||
/* fromIndex= */ 0, /* toIndex= */ 2, new FakeShuffleOrder(/* length= */ 2));
|
||||
assertTimelineUsesFakeShuffleOrder(timeline);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetShuffleOrder_expectTimelineUsesCustomShuffleOrder() {
|
||||
playlist.setMediaSources(
|
||||
createFakeHolders(), new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE));
|
||||
assertTimelineUsesFakeShuffleOrder(
|
||||
playlist.setShuffleOrder(new FakeShuffleOrder(PLAYLIST_SIZE)));
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private static void assertTimelineUsesFakeShuffleOrder(Timeline timeline) {
|
||||
assertThat(
|
||||
timeline.getNextWindowIndex(
|
||||
/* windowIndex= */ 0, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true))
|
||||
.isEqualTo(-1);
|
||||
assertThat(
|
||||
timeline.getPreviousWindowIndex(
|
||||
/* windowIndex= */ timeline.getWindowCount() - 1,
|
||||
Player.REPEAT_MODE_OFF,
|
||||
/* shuffleModeEnabled= */ true))
|
||||
.isEqualTo(-1);
|
||||
}
|
||||
|
||||
private static void assertDefaultFirstWindowInChildIndexOrder(
|
||||
List<Playlist.MediaSourceHolder> holders) {
|
||||
int[] indices = new int[holders.size()];
|
||||
for (int i = 0; i < indices.length; i++) {
|
||||
indices[i] = i;
|
||||
}
|
||||
assertFirstWindowInChildIndices(holders, indices);
|
||||
}
|
||||
|
||||
private static void assertFirstWindowInChildIndices(
|
||||
List<Playlist.MediaSourceHolder> holders, int... firstWindowInChildIndices) {
|
||||
assertThat(holders).hasSize(firstWindowInChildIndices.length);
|
||||
for (int i = 0; i < holders.size(); i++) {
|
||||
assertThat(holders.get(i).firstWindowIndexInChild).isEqualTo(firstWindowInChildIndices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Playlist.MediaSourceHolder> createFakeHolders() {
|
||||
MediaSource fakeMediaSource = new FakeMediaSource(new FakeTimeline(1));
|
||||
List<Playlist.MediaSourceHolder> holders = new ArrayList<>();
|
||||
for (int i = 0; i < PLAYLIST_SIZE; i++) {
|
||||
holders.add(new Playlist.MediaSourceHolder(fakeMediaSource, /* useLazyPreparation= */ true));
|
||||
}
|
||||
return holders;
|
||||
}
|
||||
|
||||
private static List<Playlist.MediaSourceHolder> createFakeHoldersWithSources(
|
||||
boolean useLazyPreparation, MediaSource... sources) {
|
||||
List<Playlist.MediaSourceHolder> holders = new ArrayList<>();
|
||||
for (MediaSource mediaSource : sources) {
|
||||
holders.add(
|
||||
new Playlist.MediaSourceHolder(
|
||||
mediaSource, /* useLazyPreparation= */ useLazyPreparation));
|
||||
}
|
||||
return holders;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue