Replace ConcatenatingMediaSource with DynamicConcatenatingMediaSource.

The non-dynamic media source has a strict subset of features of the dynamic one and
thus can be replaced.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=187299432
This commit is contained in:
tonihei 2018-02-28 02:30:20 -08:00 committed by Andrew Lewis
parent 32b4db361f
commit 2f4a3d2e5d
8 changed files with 1512 additions and 2049 deletions

View File

@ -4,9 +4,12 @@
* Downloading: Add `DownloadService`, `DownloadManager` and
related classes ([#2643](https://github.com/google/ExoPlayer/issues/2643)).
* MediaSources: Allow reusing media sources after they have been released and
* MediaSources:
* Allow reusing media sources after they have been released and
also in parallel to allow adding them multiple times to a concatenation.
([#3498](https://github.com/google/ExoPlayer/issues/3498)).
* Merged `DynamicConcatenatingMediaSource` into `ConcatenatingMediaSource` and
deprecated `DynamicConcatenatingMediaSource`.
* Allow clipping of child media sources where the period and window have a
non-zero offset with `ClippingMediaSource`
([#3888](https://github.com/google/ExoPlayer/issues/3888)).

View File

@ -31,7 +31,7 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.castdemo.DemoUtil.Sample;
import com.google.android.exoplayer2.ext.cast.CastPlayer;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
@ -80,7 +80,7 @@ import java.util.ArrayList;
private final ArrayList<DemoUtil.Sample> mediaQueue;
private final QueuePositionListener queuePositionListener;
private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource;
private ConcatenatingMediaSource concatenatingMediaSource;
private boolean castMediaQueueCreationPending;
private int currentItemIndex;
private Player currentPlayer;
@ -155,7 +155,7 @@ import java.util.ArrayList;
public void addItem(Sample sample) {
mediaQueue.add(sample);
if (currentPlayer == exoPlayer) {
dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(sample));
concatenatingMediaSource.addMediaSource(buildMediaSource(sample));
} else {
castPlayer.addItems(buildMediaQueueItem(sample));
}
@ -186,7 +186,7 @@ import java.util.ArrayList;
*/
public boolean removeItem(int itemIndex) {
if (currentPlayer == exoPlayer) {
dynamicConcatenatingMediaSource.removeMediaSource(itemIndex);
concatenatingMediaSource.removeMediaSource(itemIndex);
} else {
if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
@ -215,7 +215,7 @@ import java.util.ArrayList;
public boolean moveItem(int fromIndex, int toIndex) {
// Player update.
if (currentPlayer == exoPlayer) {
dynamicConcatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
concatenatingMediaSource.moveMediaSource(fromIndex, toIndex);
} else if (castPlayer.getPlaybackState() != Player.STATE_IDLE) {
Timeline castTimeline = castPlayer.getCurrentTimeline();
int periodCount = castTimeline.getPeriodCount();
@ -349,11 +349,11 @@ import java.util.ArrayList;
// Media queue management.
castMediaQueueCreationPending = currentPlayer == castPlayer;
if (currentPlayer == exoPlayer) {
dynamicConcatenatingMediaSource = new DynamicConcatenatingMediaSource();
concatenatingMediaSource = new ConcatenatingMediaSource();
for (int i = 0; i < mediaQueue.size(); i++) {
dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i)));
concatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i)));
}
exoPlayer.prepare(dynamicConcatenatingMediaSource);
exoPlayer.prepare(concatenatingMediaSource);
}
// Playback transition.

View File

@ -25,21 +25,21 @@ import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.util.Util;
import java.util.List;
/**
* A {@link MediaSessionConnector.QueueEditor} implementation based on the
* {@link DynamicConcatenatingMediaSource}.
* <p>
* This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles
* A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link
* ConcatenatingMediaSource}.
*
* <p>This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles
* the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it.
* This allows to move the currently playing window without interrupting playback.
*/
public final class TimelineQueueEditor implements MediaSessionConnector.QueueEditor,
MediaSessionConnector.CommandReceiver {
public final class TimelineQueueEditor
implements MediaSessionConnector.QueueEditor, MediaSessionConnector.CommandReceiver {
public static final String COMMAND_MOVE_QUEUE_ITEM = "exo_move_window";
public static final String EXTRA_FROM_INDEX = "from_index";
@ -125,20 +125,21 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
private final QueueDataAdapter queueDataAdapter;
private final MediaSourceFactory sourceFactory;
private final MediaDescriptionEqualityChecker equalityChecker;
private final DynamicConcatenatingMediaSource queueMediaSource;
private final ConcatenatingMediaSource queueMediaSource;
/**
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
*
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
* @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to
* manipulate.
* @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
*/
public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController,
@NonNull DynamicConcatenatingMediaSource queueMediaSource,
@NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory) {
public TimelineQueueEditor(
@NonNull MediaControllerCompat mediaController,
@NonNull ConcatenatingMediaSource queueMediaSource,
@NonNull QueueDataAdapter queueDataAdapter,
@NonNull MediaSourceFactory sourceFactory) {
this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory,
new MediaIdEqualityChecker());
}
@ -147,15 +148,16 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi
* Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory.
*
* @param mediaController A {@link MediaControllerCompat} to read the current queue.
* @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to
* manipulate.
* @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate.
* @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data.
* @param sourceFactory The {@link MediaSourceFactory} to build media sources.
* @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items.
*/
public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController,
@NonNull DynamicConcatenatingMediaSource queueMediaSource,
@NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory,
public TimelineQueueEditor(
@NonNull MediaControllerCompat mediaController,
@NonNull ConcatenatingMediaSource queueMediaSource,
@NonNull QueueDataAdapter queueDataAdapter,
@NonNull MediaSourceFactory sourceFactory,
@NonNull MediaDescriptionEqualityChecker equalityChecker) {
this.mediaController = mediaController;
this.queueMediaSource = queueMediaSource;

View File

@ -21,7 +21,6 @@ import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.source.ClippingMediaSource;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
@ -54,8 +53,7 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
* implementation for loading single media samples ({@link SingleSampleMediaSource}) that's
* most often used for side-loaded subtitle files, and implementations for building more
* complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link
* ConcatenatingMediaSource}, {@link DynamicConcatenatingMediaSource}, {@link
* LoopingMediaSource} and {@link ClippingMediaSource}).
* ConcatenatingMediaSource}, {@link LoopingMediaSource} and {@link ClippingMediaSource}).
* <li><b>{@link Renderer}</b>s that render individual components of the media. The library
* provides default implementations for common media types ({@link MediaCodecVideoRenderer},
* {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A

View File

@ -15,199 +15,700 @@
*/
package com.google.android.exoplayer2.source;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.SparseIntArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.PlayerMessage;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder;
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
/**
* Concatenates multiple {@link MediaSource}s. It is valid for the same {@link MediaSource} instance
* to be present more than once in the concatenation.
* 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 concatenation. Access to this class is thread-safe.
*/
public final class ConcatenatingMediaSource extends CompositeMediaSource<Integer> {
public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHolder>
implements PlayerMessage.Target {
private final MediaSource[] mediaSources;
private final Timeline[] timelines;
private final Object[] manifests;
private final Map<MediaPeriod, Integer> sourceIndexByMediaPeriod;
private static final int MSG_ADD = 0;
private static final int MSG_ADD_MULTIPLE = 1;
private static final int MSG_REMOVE = 2;
private static final int MSG_MOVE = 3;
private static final int MSG_NOTIFY_LISTENER = 4;
private static final int MSG_ON_COMPLETION = 5;
// Accessed on the app thread.
private final List<MediaSource> mediaSourcesPublic;
// Accessed on the playback thread.
private final List<MediaSourceHolder> mediaSourceHolders;
private final MediaSourceHolder query;
private final Map<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
private final List<DeferredMediaPeriod> deferredMediaPeriods;
private final List<EventDispatcher> pendingOnCompletionActions;
private final boolean isAtomic;
private final ShuffleOrder shuffleOrder;
private ConcatenatedTimeline timeline;
private ExoPlayer player;
private boolean listenerNotificationScheduled;
private ShuffleOrder shuffleOrder;
private int windowCount;
private int periodCount;
/** Creates a new concatenating media source. */
public ConcatenatingMediaSource() {
this(/* isAtomic= */ false, new DefaultShuffleOrder(0));
}
/**
* Creates a new concatenating media source.
*
* @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
* as a single item for repeating and shuffling.
*/
public ConcatenatingMediaSource(boolean isAtomic) {
this(isAtomic, new DefaultShuffleOrder(0));
}
/**
* Creates a new concatenating media source with a custom shuffle order.
*
* @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
* as a single item for repeating and shuffling.
* @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources.
*/
public ConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) {
this(isAtomic, shuffleOrder, new MediaSource[0]);
}
/**
* @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same
* {@link MediaSource} instance to be present more than once in the array.
*/
public ConcatenatingMediaSource(MediaSource... mediaSources) {
this(false, mediaSources);
this(/* isAtomic= */ false, mediaSources);
}
/**
* @param isAtomic Whether the concatenated media source shall be treated as atomic,
* i.e., treated as a single item for repeating and shuffling.
* @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same
* {@link MediaSource} instance to be present more than once in the array.
* @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
* as a single item for repeating and shuffling.
* @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link
* MediaSource} instance to be present more than once in the array.
*/
public ConcatenatingMediaSource(boolean isAtomic, MediaSource... mediaSources) {
this(isAtomic, new DefaultShuffleOrder(mediaSources.length), mediaSources);
this(isAtomic, new DefaultShuffleOrder(0), mediaSources);
}
/**
* @param isAtomic Whether the concatenated media source shall be treated as atomic,
* i.e., treated as a single item for repeating and shuffling.
* @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. The
* number of elements in the shuffle order must match the number of concatenated
* {@link MediaSource}s.
* @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same
* {@link MediaSource} instance to be present more than once in the array.
* @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
* as a single item for repeating and shuffling.
* @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources.
* @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link
* MediaSource} instance to be present more than once in the array.
*/
public ConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder,
MediaSource... mediaSources) {
public ConcatenatingMediaSource(
boolean isAtomic, ShuffleOrder shuffleOrder, MediaSource... mediaSources) {
for (MediaSource mediaSource : mediaSources) {
Assertions.checkNotNull(mediaSource);
}
Assertions.checkArgument(shuffleOrder.getLength() == mediaSources.length);
this.mediaSources = mediaSources;
this.shuffleOrder = shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;
this.mediaSourceByMediaPeriod = new IdentityHashMap<>();
this.mediaSourcesPublic = new ArrayList<>(Arrays.asList(mediaSources));
this.mediaSourceHolders = new ArrayList<>();
this.deferredMediaPeriods = new ArrayList<>(1);
this.pendingOnCompletionActions = new ArrayList<>();
this.query = new MediaSourceHolder(null, null, -1, -1, -1);
this.isAtomic = isAtomic;
this.shuffleOrder = shuffleOrder;
timelines = new Timeline[mediaSources.length];
manifests = new Object[mediaSources.length];
sourceIndexByMediaPeriod = new HashMap<>();
}
/**
* Appends a {@link MediaSource} to the playlist.
*
* @param mediaSource The {@link MediaSource} to be added to the list.
*/
public final synchronized void addMediaSource(MediaSource mediaSource) {
addMediaSource(mediaSourcesPublic.size(), mediaSource, null);
}
/**
* Appends a {@link MediaSource} to the playlist and executes a custom action on completion.
*
* @param mediaSource The {@link MediaSource} to be added to the list.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
* source has been added to the playlist.
*/
public final synchronized void addMediaSource(
MediaSource mediaSource, @Nullable Runnable actionOnCompletion) {
addMediaSource(mediaSourcesPublic.size(), mediaSource, actionOnCompletion);
}
/**
* Adds a {@link MediaSource} to the playlist.
*
* @param index The index at which the new {@link MediaSource} will be inserted. This index must
* be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param mediaSource The {@link MediaSource} to be added to the list.
*/
public final synchronized void addMediaSource(int index, MediaSource mediaSource) {
addMediaSource(index, mediaSource, null);
}
/**
* Adds a {@link MediaSource} to the playlist and executes a custom action on completion.
*
* @param index The index at which the new {@link MediaSource} will be inserted. This index must
* be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param mediaSource The {@link MediaSource} to be added to the list.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
* source has been added to the playlist.
*/
public final synchronized void addMediaSource(
int index, MediaSource mediaSource, @Nullable Runnable actionOnCompletion) {
Assertions.checkNotNull(mediaSource);
mediaSourcesPublic.add(index, mediaSource);
if (player != null) {
player
.createMessage(this)
.setType(MSG_ADD)
.setPayload(new MessageData<>(index, mediaSource, actionOnCompletion))
.send();
} else if (actionOnCompletion != null) {
actionOnCompletion.run();
}
}
/**
* Appends multiple {@link MediaSource}s to the playlist.
*
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
* sources are added in the order in which they appear in this collection.
*/
public final synchronized void addMediaSources(Collection<MediaSource> mediaSources) {
addMediaSources(mediaSourcesPublic.size(), mediaSources, null);
}
/**
* Appends multiple {@link MediaSource}s to the playlist and executes a custom action on
* completion.
*
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
* sources are added in the order in which they appear in this collection.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
* sources have been added to the playlist.
*/
public final synchronized void addMediaSources(
Collection<MediaSource> mediaSources, @Nullable Runnable actionOnCompletion) {
addMediaSources(mediaSourcesPublic.size(), mediaSources, actionOnCompletion);
}
/**
* Adds multiple {@link MediaSource}s to the playlist.
*
* @param index The index at which the new {@link MediaSource}s will be inserted. This index must
* be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
* sources are added in the order in which they appear in this collection.
*/
public final synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) {
addMediaSources(index, mediaSources, null);
}
/**
* Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion.
*
* @param index The index at which the new {@link MediaSource}s will be inserted. This index must
* be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
* sources are added in the order in which they appear in this collection.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
* sources have been added to the playlist.
*/
public final synchronized void addMediaSources(
int index, Collection<MediaSource> mediaSources, @Nullable Runnable actionOnCompletion) {
for (MediaSource mediaSource : mediaSources) {
Assertions.checkNotNull(mediaSource);
}
mediaSourcesPublic.addAll(index, mediaSources);
if (player != null && !mediaSources.isEmpty()) {
player
.createMessage(this)
.setType(MSG_ADD_MULTIPLE)
.setPayload(new MessageData<>(index, mediaSources, actionOnCompletion))
.send();
} else if (actionOnCompletion != null) {
actionOnCompletion.run();
}
}
/**
* Removes a {@link MediaSource} from the playlist.
*
* <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,
* int)} instead.
*
* @param index The index at which the media source will be removed. This index must be in the
* range of 0 &lt;= index &lt; {@link #getSize()}.
*/
public final synchronized void removeMediaSource(int index) {
removeMediaSource(index, null);
}
/**
* Removes a {@link MediaSource} from the playlist and executes a custom action on completion.
*
* <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,
* int)} instead.
*
* @param index The index at which the media source will be removed. This index must be in the
* range of 0 &lt;= index &lt; {@link #getSize()}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
* source has been removed from the playlist.
*/
public final synchronized void removeMediaSource(
int index, @Nullable Runnable actionOnCompletion) {
mediaSourcesPublic.remove(index);
if (player != null) {
player
.createMessage(this)
.setType(MSG_REMOVE)
.setPayload(new MessageData<>(index, null, actionOnCompletion))
.send();
} else if (actionOnCompletion != null) {
actionOnCompletion.run();
}
}
/**
* Moves an existing {@link MediaSource} within the playlist.
*
* @param currentIndex The current index of the media source in the playlist. This index must be
* in the range of 0 &lt;= index &lt; {@link #getSize()}.
* @param newIndex The target index of the media source in the playlist. This index must be in the
* range of 0 &lt;= index &lt; {@link #getSize()}.
*/
public final synchronized void moveMediaSource(int currentIndex, int newIndex) {
moveMediaSource(currentIndex, newIndex, null);
}
/**
* Moves an existing {@link MediaSource} within the playlist and executes a custom action on
* completion.
*
* @param currentIndex The current index of the media source in the playlist. This index must be
* in the range of 0 &lt;= index &lt; {@link #getSize()}.
* @param newIndex The target index of the media source in the playlist. This index must be in the
* range of 0 &lt;= index &lt; {@link #getSize()}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
* source has been moved.
*/
public final synchronized void moveMediaSource(
int currentIndex, int newIndex, @Nullable Runnable actionOnCompletion) {
if (currentIndex == newIndex) {
return;
}
mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
if (player != null) {
player
.createMessage(this)
.setType(MSG_MOVE)
.setPayload(new MessageData<>(currentIndex, newIndex, actionOnCompletion))
.send();
} else if (actionOnCompletion != null) {
actionOnCompletion.run();
}
}
/** Returns the number of media sources in the playlist. */
public final synchronized int getSize() {
return mediaSourcesPublic.size();
}
/**
* Returns the {@link MediaSource} at a specified index.
*
* @param index An index in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @return The {@link MediaSource} at this index.
*/
public final synchronized MediaSource getMediaSource(int index) {
return mediaSourcesPublic.get(index);
}
@Override
public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
public final synchronized void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
super.prepareSourceInternal(player, isTopLevelSource);
boolean[] duplicateFlags = buildDuplicateFlags(mediaSources);
if (mediaSources.length == 0) {
refreshSourceInfo(Timeline.EMPTY, /* manifest= */ null);
this.player = player;
if (mediaSourcesPublic.isEmpty()) {
notifyListener();
} else {
for (int i = 0; i < mediaSources.length; i++) {
if (!duplicateFlags[i]) {
prepareChildSource(i, mediaSources[i]);
}
}
shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size());
addMediaSourcesInternal(0, mediaSourcesPublic);
scheduleListenerNotification(/* actionOnCompletion= */ null);
}
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
int sourceIndex = timeline.getChildIndexByPeriodIndex(id.periodIndex);
MediaPeriodId periodIdInSource = id.copyWithPeriodIndex(
id.periodIndex - timeline.getFirstPeriodIndexByChildIndex(sourceIndex));
MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIdInSource, allocator);
sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex);
public final MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex);
MediaSourceHolder holder = mediaSourceHolders.get(mediaSourceHolderIndex);
MediaPeriodId idInSource =
id.copyWithPeriodIndex(id.periodIndex - holder.firstPeriodIndexInChild);
MediaPeriod mediaPeriod;
if (!holder.isPrepared) {
mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, idInSource, allocator);
deferredMediaPeriods.add((DeferredMediaPeriod) mediaPeriod);
} else {
mediaPeriod = holder.mediaSource.createPeriod(idInSource, allocator);
}
mediaSourceByMediaPeriod.put(mediaPeriod, holder);
holder.activeMediaPeriods++;
return mediaPeriod;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
int sourceIndex = sourceIndexByMediaPeriod.get(mediaPeriod);
sourceIndexByMediaPeriod.remove(mediaPeriod);
mediaSources[sourceIndex].releasePeriod(mediaPeriod);
public final void releasePeriod(MediaPeriod mediaPeriod) {
MediaSourceHolder holder = mediaSourceByMediaPeriod.remove(mediaPeriod);
if (mediaPeriod instanceof DeferredMediaPeriod) {
deferredMediaPeriods.remove(mediaPeriod);
((DeferredMediaPeriod) mediaPeriod).releasePeriod();
} else {
holder.mediaSource.releasePeriod(mediaPeriod);
}
holder.activeMediaPeriods--;
if (holder.activeMediaPeriods == 0 && holder.isRemoved) {
releaseChildSource(holder);
}
}
@Override
public void releaseSourceInternal() {
public final void releaseSourceInternal() {
super.releaseSourceInternal();
timeline = null;
mediaSourceHolders.clear();
player = null;
shuffleOrder = shuffleOrder.cloneAndClear();
windowCount = 0;
periodCount = 0;
}
@Override
protected void onChildSourceInfoRefreshed(
Integer index,
protected final void onChildSourceInfoRefreshed(
MediaSourceHolder mediaSourceHolder,
MediaSource mediaSource,
Timeline sourceTimeline,
@Nullable Object sourceManifest) {
// Set the timeline and manifest.
timelines[index] = sourceTimeline;
manifests[index] = sourceManifest;
// Also set the timeline and manifest for any duplicate entries of the same source.
for (int i = index + 1; i < mediaSources.length; i++) {
if (mediaSources[i] == mediaSource) {
timelines[i] = sourceTimeline;
manifests[i] = sourceManifest;
Timeline timeline,
@Nullable Object manifest) {
updateMediaSourceInternal(mediaSourceHolder, timeline);
}
@Override
@SuppressWarnings("unchecked")
public final void handleMessage(int messageType, Object message) throws ExoPlaybackException {
switch (messageType) {
case MSG_ADD:
MessageData<MediaSource> addMessage = (MessageData<MediaSource>) message;
shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, 1);
addMediaSourceInternal(addMessage.index, addMessage.customData);
scheduleListenerNotification(addMessage.actionOnCompletion);
break;
case MSG_ADD_MULTIPLE:
MessageData<Collection<MediaSource>> addMultipleMessage =
(MessageData<Collection<MediaSource>>) message;
shuffleOrder =
shuffleOrder.cloneAndInsert(
addMultipleMessage.index, addMultipleMessage.customData.size());
addMediaSourcesInternal(addMultipleMessage.index, addMultipleMessage.customData);
scheduleListenerNotification(addMultipleMessage.actionOnCompletion);
break;
case MSG_REMOVE:
MessageData<Void> removeMessage = (MessageData<Void>) message;
shuffleOrder = shuffleOrder.cloneAndRemove(removeMessage.index);
removeMediaSourceInternal(removeMessage.index);
scheduleListenerNotification(removeMessage.actionOnCompletion);
break;
case MSG_MOVE:
MessageData<Integer> moveMessage = (MessageData<Integer>) message;
shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index);
shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1);
moveMediaSourceInternal(moveMessage.index, moveMessage.customData);
scheduleListenerNotification(moveMessage.actionOnCompletion);
break;
case MSG_NOTIFY_LISTENER:
notifyListener();
break;
case MSG_ON_COMPLETION:
List<EventDispatcher> actionsOnCompletion = ((List<EventDispatcher>) message);
for (int i = 0; i < actionsOnCompletion.size(); i++) {
actionsOnCompletion.get(i).dispatchEvent();
}
break;
default:
throw new IllegalStateException();
}
}
for (Timeline timeline : timelines) {
if (timeline == null) {
// Don't invoke the listener until all sources have timelines.
private void scheduleListenerNotification(@Nullable EventDispatcher actionOnCompletion) {
if (!listenerNotificationScheduled) {
player.createMessage(this).setType(MSG_NOTIFY_LISTENER).send();
listenerNotificationScheduled = true;
}
if (actionOnCompletion != null) {
pendingOnCompletionActions.add(actionOnCompletion);
}
}
private void notifyListener() {
listenerNotificationScheduled = false;
List<EventDispatcher> actionsOnCompletion =
pendingOnCompletionActions.isEmpty()
? Collections.<EventDispatcher>emptyList()
: new ArrayList<>(pendingOnCompletionActions);
pendingOnCompletionActions.clear();
refreshSourceInfo(
new ConcatenatedTimeline(
mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic),
/* manifest= */ null);
if (!actionsOnCompletion.isEmpty()) {
player.createMessage(this).setType(MSG_ON_COMPLETION).setPayload(actionsOnCompletion).send();
}
}
private void addMediaSourceInternal(int newIndex, MediaSource newMediaSource) {
final MediaSourceHolder newMediaSourceHolder;
DeferredTimeline newTimeline = new DeferredTimeline();
if (newIndex > 0) {
MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1);
newMediaSourceHolder =
new MediaSourceHolder(
newMediaSource,
newTimeline,
newIndex,
previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(),
previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount());
} else {
newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, 0, 0, 0);
}
correctOffsets(
newIndex,
/* childIndexUpdate= */ 1,
newTimeline.getWindowCount(),
newTimeline.getPeriodCount());
mediaSourceHolders.add(newIndex, newMediaSourceHolder);
prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource);
}
private void addMediaSourcesInternal(int index, Collection<MediaSource> mediaSources) {
for (MediaSource mediaSource : mediaSources) {
addMediaSourceInternal(index++, mediaSource);
}
}
private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) {
if (mediaSourceHolder == null) {
throw new IllegalArgumentException();
}
DeferredTimeline deferredTimeline = mediaSourceHolder.timeline;
if (deferredTimeline.getTimeline() == timeline) {
return;
}
int windowOffsetUpdate = timeline.getWindowCount() - deferredTimeline.getWindowCount();
int periodOffsetUpdate = timeline.getPeriodCount() - deferredTimeline.getPeriodCount();
if (windowOffsetUpdate != 0 || periodOffsetUpdate != 0) {
correctOffsets(
mediaSourceHolder.childIndex + 1,
/* childIndexUpdate= */ 0,
windowOffsetUpdate,
periodOffsetUpdate);
}
timeline = new ConcatenatedTimeline(timelines.clone(), isAtomic, shuffleOrder);
refreshSourceInfo(timeline, manifests.clone());
mediaSourceHolder.timeline = deferredTimeline.cloneWithNewTimeline(timeline);
if (!mediaSourceHolder.isPrepared) {
for (int i = deferredMediaPeriods.size() - 1; i >= 0; i--) {
if (deferredMediaPeriods.get(i).mediaSource == mediaSourceHolder.mediaSource) {
deferredMediaPeriods.get(i).createPeriod();
deferredMediaPeriods.remove(i);
}
}
}
mediaSourceHolder.isPrepared = true;
scheduleListenerNotification(/* actionOnCompletion= */ null);
}
private static boolean[] buildDuplicateFlags(MediaSource[] mediaSources) {
boolean[] duplicateFlags = new boolean[mediaSources.length];
IdentityHashMap<MediaSource, Void> sources = new IdentityHashMap<>(mediaSources.length);
for (int i = 0; i < mediaSources.length; i++) {
MediaSource source = mediaSources[i];
if (!sources.containsKey(source)) {
sources.put(source, null);
} else {
duplicateFlags[i] = true;
private void removeMediaSourceInternal(int index) {
MediaSourceHolder holder = mediaSourceHolders.get(index);
mediaSourceHolders.remove(index);
Timeline oldTimeline = holder.timeline;
correctOffsets(
index,
/* childIndexUpdate= */ -1,
-oldTimeline.getWindowCount(),
-oldTimeline.getPeriodCount());
holder.isRemoved = true;
if (holder.activeMediaPeriods == 0) {
releaseChildSource(holder);
}
}
return duplicateFlags;
}
/**
* A {@link Timeline} that is the concatenation of one or more {@link Timeline}s.
*/
private void moveMediaSourceInternal(int currentIndex, int newIndex) {
int startIndex = Math.min(currentIndex, newIndex);
int endIndex = Math.max(currentIndex, newIndex);
int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild;
int periodOffset = mediaSourceHolders.get(startIndex).firstPeriodIndexInChild;
mediaSourceHolders.add(newIndex, mediaSourceHolders.remove(currentIndex));
for (int i = startIndex; i <= endIndex; i++) {
MediaSourceHolder holder = mediaSourceHolders.get(i);
holder.firstWindowIndexInChild = windowOffset;
holder.firstPeriodIndexInChild = periodOffset;
windowOffset += holder.timeline.getWindowCount();
periodOffset += holder.timeline.getPeriodCount();
}
}
private void correctOffsets(
int startIndex, int childIndexUpdate, int windowOffsetUpdate, int periodOffsetUpdate) {
windowCount += windowOffsetUpdate;
periodCount += periodOffsetUpdate;
for (int i = startIndex; i < mediaSourceHolders.size(); i++) {
mediaSourceHolders.get(i).childIndex += childIndexUpdate;
mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate;
mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate;
}
}
private int findMediaSourceHolderByPeriodIndex(int periodIndex) {
query.firstPeriodIndexInChild = periodIndex;
int index = Collections.binarySearch(mediaSourceHolders, query);
if (index < 0) {
return -index - 2;
}
while (index < mediaSourceHolders.size() - 1
&& mediaSourceHolders.get(index + 1).firstPeriodIndexInChild == periodIndex) {
index++;
}
return index;
}
/** Data class to hold playlist media sources together with meta data needed to process them. */
/* package */ static final class MediaSourceHolder implements Comparable<MediaSourceHolder> {
public final MediaSource mediaSource;
public final int uid;
public DeferredTimeline timeline;
public int childIndex;
public int firstWindowIndexInChild;
public int firstPeriodIndexInChild;
public boolean isPrepared;
public boolean isRemoved;
public int activeMediaPeriods;
public MediaSourceHolder(
MediaSource mediaSource,
DeferredTimeline timeline,
int childIndex,
int window,
int period) {
this.mediaSource = mediaSource;
this.timeline = timeline;
this.childIndex = childIndex;
this.firstWindowIndexInChild = window;
this.firstPeriodIndexInChild = period;
this.uid = System.identityHashCode(this);
}
@Override
public int compareTo(@NonNull MediaSourceHolder other) {
return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild;
}
}
/** Can be used to dispatch a runnable on the thread the object was created on. */
private static final class EventDispatcher {
public final Handler eventHandler;
public final Runnable runnable;
public EventDispatcher(Runnable runnable) {
this.runnable = runnable;
this.eventHandler =
new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper());
}
public void dispatchEvent() {
eventHandler.post(runnable);
}
}
/** Message used to post actions from app thread to playback thread. */
private static final class MessageData<T> {
public final int index;
public final T customData;
public final @Nullable EventDispatcher actionOnCompletion;
public MessageData(int index, T customData, @Nullable Runnable actionOnCompletion) {
this.index = index;
this.actionOnCompletion =
actionOnCompletion != null ? new EventDispatcher(actionOnCompletion) : null;
this.customData = customData;
}
}
/** Timeline exposing concatenated timelines of playlist media sources. */
private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline {
private final int windowCount;
private final int periodCount;
private final int[] firstPeriodInChildIndices;
private final int[] firstWindowInChildIndices;
private final Timeline[] timelines;
private final int[] sourcePeriodOffsets;
private final int[] sourceWindowOffsets;
private final int[] uids;
private final SparseIntArray childIndexByUid;
public ConcatenatedTimeline(Timeline[] timelines, boolean isAtomic, ShuffleOrder shuffleOrder) {
public ConcatenatedTimeline(
Collection<MediaSourceHolder> mediaSourceHolders,
int windowCount,
int periodCount,
ShuffleOrder shuffleOrder,
boolean isAtomic) {
super(isAtomic, shuffleOrder);
int[] sourcePeriodOffsets = new int[timelines.length];
int[] sourceWindowOffsets = new int[timelines.length];
long periodCount = 0;
int windowCount = 0;
for (int i = 0; i < timelines.length; i++) {
Timeline timeline = timelines[i];
periodCount += timeline.getPeriodCount();
Assertions.checkState(periodCount <= Integer.MAX_VALUE,
"ConcatenatingMediaSource children contain too many periods");
sourcePeriodOffsets[i] = (int) periodCount;
windowCount += timeline.getWindowCount();
sourceWindowOffsets[i] = windowCount;
this.windowCount = windowCount;
this.periodCount = periodCount;
int childCount = mediaSourceHolders.size();
firstPeriodInChildIndices = new int[childCount];
firstWindowInChildIndices = new int[childCount];
timelines = new Timeline[childCount];
uids = new int[childCount];
childIndexByUid = new SparseIntArray();
int index = 0;
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
timelines[index] = mediaSourceHolder.timeline;
firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild;
firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild;
uids[index] = mediaSourceHolder.uid;
childIndexByUid.put(uids[index], index++);
}
this.timelines = timelines;
this.sourcePeriodOffsets = sourcePeriodOffsets;
this.sourceWindowOffsets = sourceWindowOffsets;
}
@Override
public int getWindowCount() {
return sourceWindowOffsets[sourceWindowOffsets.length - 1];
}
@Override
public int getPeriodCount() {
return sourcePeriodOffsets[sourcePeriodOffsets.length - 1];
}
@Override
protected int getChildIndexByPeriodIndex(int periodIndex) {
return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex + 1, false, false) + 1;
return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false);
}
@Override
protected int getChildIndexByWindowIndex(int windowIndex) {
return Util.binarySearchFloor(sourceWindowOffsets, windowIndex + 1, false, false) + 1;
return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false);
}
@Override
@ -215,7 +716,8 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<Integer
if (!(childUid instanceof Integer)) {
return C.INDEX_UNSET;
}
return (Integer) childUid;
int index = childIndexByUid.get((int) childUid, -1);
return index == -1 ? C.INDEX_UNSET : index;
}
@Override
@ -225,20 +727,123 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<Integer
@Override
protected int getFirstPeriodIndexByChildIndex(int childIndex) {
return childIndex == 0 ? 0 : sourcePeriodOffsets[childIndex - 1];
return firstPeriodInChildIndices[childIndex];
}
@Override
protected int getFirstWindowIndexByChildIndex(int childIndex) {
return childIndex == 0 ? 0 : sourceWindowOffsets[childIndex - 1];
return firstWindowInChildIndices[childIndex];
}
@Override
protected Object getChildUidByChildIndex(int childIndex) {
return childIndex;
return uids[childIndex];
}
@Override
public int getWindowCount() {
return windowCount;
}
@Override
public int getPeriodCount() {
return periodCount;
}
}
/**
* Timeline used as placeholder for an unprepared media source. After preparation, a copy of the
* DeferredTimeline is used to keep the originally assigned first period ID.
*/
private static final class DeferredTimeline extends ForwardingTimeline {
private static final Object DUMMY_ID = new Object();
private static final Period period = new Period();
private static final DummyTimeline dummyTimeline = new DummyTimeline();
private final Object replacedId;
public DeferredTimeline() {
this(dummyTimeline, /* replacedId= */ null);
}
private DeferredTimeline(Timeline timeline, Object replacedId) {
super(timeline);
this.replacedId = replacedId;
}
public DeferredTimeline cloneWithNewTimeline(Timeline timeline) {
return new DeferredTimeline(
timeline,
replacedId == null && timeline.getPeriodCount() > 0
? timeline.getPeriod(0, period, true).uid
: replacedId);
}
public Timeline getTimeline() {
return timeline;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
timeline.getPeriod(periodIndex, period, setIds);
if (Util.areEqual(period.uid, replacedId)) {
period.uid = DUMMY_ID;
}
return period;
}
@Override
public int getIndexOfPeriod(Object uid) {
return timeline.getIndexOfPeriod(DUMMY_ID.equals(uid) ? replacedId : uid);
}
}
/** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */
private static final class DummyTimeline extends Timeline {
@Override
public int getWindowCount() {
return 1;
}
@Override
public Window getWindow(
int windowIndex, Window window, boolean setIds, long defaultPositionProjectionUs) {
// Dynamic window to indicate pending timeline updates.
return window.set(
/* id= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* isSeekable= */ false,
/* isDynamic= */ true,
/* defaultPositionUs= */ 0,
/* durationUs= */ C.TIME_UNSET,
/* firstPeriodIndex= */ 0,
/* lastPeriodIndex= */ 0,
/* positionInFirstPeriodUs= */ 0);
}
@Override
public int getPeriodCount() {
return 1;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
return period.set(
/* id= */ null,
/* uid= */ null,
/* windowIndex= */ 0,
/* durationUs = */ C.TIME_UNSET,
/* positionInWindowUs= */ C.TIME_UNSET);
}
@Override
public int getIndexOfPeriod(Object uid) {
return uid == null ? 0 : C.INDEX_UNSET;
}
}
}

View File

@ -15,811 +15,26 @@
*/
package com.google.android.exoplayer2.source;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.SparseIntArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.PlayerMessage;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource.MediaSourceHolder;
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
/** @deprecated Use {@link ConcatenatingMediaSource} instead. */
@Deprecated
public final class DynamicConcatenatingMediaSource extends ConcatenatingMediaSource {
/**
* 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 concatenation. Access to this class is thread-safe.
*/
public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHolder>
implements PlayerMessage.Target {
/** @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource()} instead. */
@Deprecated
public DynamicConcatenatingMediaSource() {}
private static final int MSG_ADD = 0;
private static final int MSG_ADD_MULTIPLE = 1;
private static final int MSG_REMOVE = 2;
private static final int MSG_MOVE = 3;
private static final int MSG_NOTIFY_LISTENER = 4;
private static final int MSG_ON_COMPLETION = 5;
// Accessed on the app thread.
private final List<MediaSource> mediaSourcesPublic;
// Accessed on the playback thread.
private final List<MediaSourceHolder> mediaSourceHolders;
private final MediaSourceHolder query;
private final Map<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
private final List<DeferredMediaPeriod> deferredMediaPeriods;
private final List<EventDispatcher> pendingOnCompletionActions;
private final boolean isAtomic;
private ExoPlayer player;
private boolean listenerNotificationScheduled;
private ShuffleOrder shuffleOrder;
private int windowCount;
private int periodCount;
/**
* Creates a new dynamic concatenating media source.
*/
public DynamicConcatenatingMediaSource() {
this(/* isAtomic= */ false, new DefaultShuffleOrder(0));
}
/**
* Creates a new dynamic concatenating media source.
*
* @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
* as a single item for repeating and shuffling.
*/
/** @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean)} instead. */
@Deprecated
public DynamicConcatenatingMediaSource(boolean isAtomic) {
this(isAtomic, new DefaultShuffleOrder(0));
super(isAtomic);
}
/**
* Creates a new dynamic concatenating media source with a custom shuffle order.
*
* @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
* as a single item for repeating and shuffling.
* @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources.
* This shuffle order must be empty.
* @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean,
* ShuffleOrder)} instead.
*/
@Deprecated
public DynamicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) {
this.shuffleOrder = shuffleOrder;
this.mediaSourceByMediaPeriod = new IdentityHashMap<>();
this.mediaSourcesPublic = new ArrayList<>();
this.mediaSourceHolders = new ArrayList<>();
this.deferredMediaPeriods = new ArrayList<>(1);
this.pendingOnCompletionActions = new ArrayList<>();
this.query = new MediaSourceHolder(null, null, -1, -1, -1);
this.isAtomic = isAtomic;
}
/**
* Appends a {@link MediaSource} to the playlist.
*
* @param mediaSource The {@link MediaSource} to be added to the list.
*/
public synchronized void addMediaSource(MediaSource mediaSource) {
addMediaSource(mediaSourcesPublic.size(), mediaSource, null);
}
/**
* Appends a {@link MediaSource} to the playlist and executes a custom action on completion.
*
* @param mediaSource The {@link MediaSource} to be added to the list.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
* source has been added to the playlist.
*/
public synchronized void addMediaSource(MediaSource mediaSource,
@Nullable Runnable actionOnCompletion) {
addMediaSource(mediaSourcesPublic.size(), mediaSource, actionOnCompletion);
}
/**
* Adds a {@link MediaSource} to the playlist.
*
* @param index The index at which the new {@link MediaSource} will be inserted. This index must
* be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param mediaSource The {@link MediaSource} to be added to the list.
*/
public synchronized void addMediaSource(int index, MediaSource mediaSource) {
addMediaSource(index, mediaSource, null);
}
/**
* Adds a {@link MediaSource} to the playlist and executes a custom action on completion.
*
* @param index The index at which the new {@link MediaSource} will be inserted. This index must
* be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param mediaSource The {@link MediaSource} to be added to the list.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
* source has been added to the playlist.
*/
public synchronized void addMediaSource(int index, MediaSource mediaSource,
@Nullable Runnable actionOnCompletion) {
Assertions.checkNotNull(mediaSource);
mediaSourcesPublic.add(index, mediaSource);
if (player != null) {
player
.createMessage(this)
.setType(MSG_ADD)
.setPayload(new MessageData<>(index, mediaSource, actionOnCompletion))
.send();
} else if (actionOnCompletion != null) {
actionOnCompletion.run();
}
}
/**
* Appends multiple {@link MediaSource}s to the playlist.
*
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
* sources are added in the order in which they appear in this collection.
*/
public synchronized void addMediaSources(Collection<MediaSource> mediaSources) {
addMediaSources(mediaSourcesPublic.size(), mediaSources, null);
}
/**
* Appends multiple {@link MediaSource}s to the playlist and executes a custom action on
* completion.
*
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
* sources are added in the order in which they appear in this collection.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
* sources have been added to the playlist.
*/
public synchronized void addMediaSources(Collection<MediaSource> mediaSources,
@Nullable Runnable actionOnCompletion) {
addMediaSources(mediaSourcesPublic.size(), mediaSources, actionOnCompletion);
}
/**
* Adds multiple {@link MediaSource}s to the playlist.
*
* @param index The index at which the new {@link MediaSource}s will be inserted. This index must
* be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
* sources are added in the order in which they appear in this collection.
*/
public synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) {
addMediaSources(index, mediaSources, null);
}
/**
* Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion.
*
* @param index The index at which the new {@link MediaSource}s will be inserted. This index must
* be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
* sources are added in the order in which they appear in this collection.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
* sources have been added to the playlist.
*/
public synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources,
@Nullable Runnable actionOnCompletion) {
for (MediaSource mediaSource : mediaSources) {
Assertions.checkNotNull(mediaSource);
}
mediaSourcesPublic.addAll(index, mediaSources);
if (player != null && !mediaSources.isEmpty()) {
player
.createMessage(this)
.setType(MSG_ADD_MULTIPLE)
.setPayload(new MessageData<>(index, mediaSources, actionOnCompletion))
.send();
} else if (actionOnCompletion != null){
actionOnCompletion.run();
}
}
/**
* Removes a {@link MediaSource} from the playlist.
*
* <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,
* int)} instead.
*
* @param index The index at which the media source will be removed. This index must be in the
* range of 0 &lt;= index &lt; {@link #getSize()}.
*/
public synchronized void removeMediaSource(int index) {
removeMediaSource(index, null);
}
/**
* Removes a {@link MediaSource} from the playlist and executes a custom action on completion.
*
* <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,
* int)} instead.
*
* @param index The index at which the media source will be removed. This index must be in the
* range of 0 &lt;= index &lt; {@link #getSize()}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
* source has been removed from the playlist.
*/
public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) {
mediaSourcesPublic.remove(index);
if (player != null) {
player
.createMessage(this)
.setType(MSG_REMOVE)
.setPayload(new MessageData<>(index, null, actionOnCompletion))
.send();
} else if (actionOnCompletion != null) {
actionOnCompletion.run();
}
}
/**
* Moves an existing {@link MediaSource} within the playlist.
*
* @param currentIndex The current index of the media source in the playlist. This index must be
* in the range of 0 &lt;= index &lt; {@link #getSize()}.
* @param newIndex The target index of the media source in the playlist. This index must be in the
* range of 0 &lt;= index &lt; {@link #getSize()}.
*/
public synchronized void moveMediaSource(int currentIndex, int newIndex) {
moveMediaSource(currentIndex, newIndex, null);
}
/**
* Moves an existing {@link MediaSource} within the playlist and executes a custom action on
* completion.
*
* @param currentIndex The current index of the media source in the playlist. This index must be
* in the range of 0 &lt;= index &lt; {@link #getSize()}.
* @param newIndex The target index of the media source in the playlist. This index must be in the
* range of 0 &lt;= index &lt; {@link #getSize()}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
* source has been moved.
*/
public synchronized void moveMediaSource(int currentIndex, int newIndex,
@Nullable Runnable actionOnCompletion) {
if (currentIndex == newIndex) {
return;
}
mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
if (player != null) {
player
.createMessage(this)
.setType(MSG_MOVE)
.setPayload(new MessageData<>(currentIndex, newIndex, actionOnCompletion))
.send();
} else if (actionOnCompletion != null) {
actionOnCompletion.run();
}
}
/**
* Returns the number of media sources in the playlist.
*/
public synchronized int getSize() {
return mediaSourcesPublic.size();
}
/**
* Returns the {@link MediaSource} at a specified index.
*
* @param index An index in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @return The {@link MediaSource} at this index.
*/
public synchronized MediaSource getMediaSource(int index) {
return mediaSourcesPublic.get(index);
}
@Override
public synchronized void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
super.prepareSourceInternal(player, isTopLevelSource);
this.player = player;
if (mediaSourcesPublic.isEmpty()) {
notifyListener();
} else {
shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size());
addMediaSourcesInternal(0, mediaSourcesPublic);
scheduleListenerNotification(/* actionOnCompletion= */ null);
}
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex);
MediaSourceHolder holder = mediaSourceHolders.get(mediaSourceHolderIndex);
MediaPeriodId idInSource = id.copyWithPeriodIndex(
id.periodIndex - holder.firstPeriodIndexInChild);
MediaPeriod mediaPeriod;
if (!holder.isPrepared) {
mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, idInSource, allocator);
deferredMediaPeriods.add((DeferredMediaPeriod) mediaPeriod);
} else {
mediaPeriod = holder.mediaSource.createPeriod(idInSource, allocator);
}
mediaSourceByMediaPeriod.put(mediaPeriod, holder);
holder.activeMediaPeriods++;
return mediaPeriod;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
MediaSourceHolder holder = mediaSourceByMediaPeriod.remove(mediaPeriod);
if (mediaPeriod instanceof DeferredMediaPeriod) {
deferredMediaPeriods.remove(mediaPeriod);
((DeferredMediaPeriod) mediaPeriod).releasePeriod();
} else {
holder.mediaSource.releasePeriod(mediaPeriod);
}
holder.activeMediaPeriods--;
if (holder.activeMediaPeriods == 0 && holder.isRemoved) {
releaseChildSource(holder);
}
}
@Override
public void releaseSourceInternal() {
super.releaseSourceInternal();
mediaSourceHolders.clear();
player = null;
shuffleOrder = shuffleOrder.cloneAndClear();
windowCount = 0;
periodCount = 0;
}
@Override
protected void onChildSourceInfoRefreshed(
MediaSourceHolder mediaSourceHolder,
MediaSource mediaSource,
Timeline timeline,
@Nullable Object manifest) {
updateMediaSourceInternal(mediaSourceHolder, timeline);
}
@Override
@SuppressWarnings("unchecked")
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
switch (messageType) {
case MSG_ADD:
MessageData<MediaSource> addMessage = (MessageData<MediaSource>) message;
shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, 1);
addMediaSourceInternal(addMessage.index, addMessage.customData);
scheduleListenerNotification(addMessage.actionOnCompletion);
break;
case MSG_ADD_MULTIPLE:
MessageData<Collection<MediaSource>> addMultipleMessage =
(MessageData<Collection<MediaSource>>) message;
shuffleOrder =
shuffleOrder.cloneAndInsert(
addMultipleMessage.index, addMultipleMessage.customData.size());
addMediaSourcesInternal(addMultipleMessage.index, addMultipleMessage.customData);
scheduleListenerNotification(addMultipleMessage.actionOnCompletion);
break;
case MSG_REMOVE:
MessageData<Void> removeMessage = (MessageData<Void>) message;
shuffleOrder = shuffleOrder.cloneAndRemove(removeMessage.index);
removeMediaSourceInternal(removeMessage.index);
scheduleListenerNotification(removeMessage.actionOnCompletion);
break;
case MSG_MOVE:
MessageData<Integer> moveMessage = (MessageData<Integer>) message;
shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index);
shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1);
moveMediaSourceInternal(moveMessage.index, moveMessage.customData);
scheduleListenerNotification(moveMessage.actionOnCompletion);
break;
case MSG_NOTIFY_LISTENER:
notifyListener();
break;
case MSG_ON_COMPLETION:
List<EventDispatcher> actionsOnCompletion = ((List<EventDispatcher>) message);
for (int i = 0; i < actionsOnCompletion.size(); i++) {
actionsOnCompletion.get(i).dispatchEvent();
}
break;
default:
throw new IllegalStateException();
}
}
private void scheduleListenerNotification(@Nullable EventDispatcher actionOnCompletion) {
if (!listenerNotificationScheduled) {
player.createMessage(this).setType(MSG_NOTIFY_LISTENER).send();
listenerNotificationScheduled = true;
}
if (actionOnCompletion != null) {
pendingOnCompletionActions.add(actionOnCompletion);
}
}
private void notifyListener() {
listenerNotificationScheduled = false;
List<EventDispatcher> actionsOnCompletion =
pendingOnCompletionActions.isEmpty()
? Collections.<EventDispatcher>emptyList()
: new ArrayList<>(pendingOnCompletionActions);
pendingOnCompletionActions.clear();
refreshSourceInfo(
new ConcatenatedTimeline(
mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic),
/* manifest= */ null);
if (!actionsOnCompletion.isEmpty()) {
player.createMessage(this).setType(MSG_ON_COMPLETION).setPayload(actionsOnCompletion).send();
}
}
private void addMediaSourceInternal(int newIndex, MediaSource newMediaSource) {
final MediaSourceHolder newMediaSourceHolder;
DeferredTimeline newTimeline = new DeferredTimeline();
if (newIndex > 0) {
MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1);
newMediaSourceHolder =
new MediaSourceHolder(
newMediaSource,
newTimeline,
newIndex,
previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(),
previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount());
} else {
newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, 0, 0, 0);
}
correctOffsets(
newIndex,
/* childIndexUpdate= */ 1,
newTimeline.getWindowCount(),
newTimeline.getPeriodCount());
mediaSourceHolders.add(newIndex, newMediaSourceHolder);
prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource);
}
private void addMediaSourcesInternal(int index, Collection<MediaSource> mediaSources) {
for (MediaSource mediaSource : mediaSources) {
addMediaSourceInternal(index++, mediaSource);
}
}
private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) {
if (mediaSourceHolder == null) {
throw new IllegalArgumentException();
}
DeferredTimeline deferredTimeline = mediaSourceHolder.timeline;
if (deferredTimeline.getTimeline() == timeline) {
return;
}
int windowOffsetUpdate = timeline.getWindowCount() - deferredTimeline.getWindowCount();
int periodOffsetUpdate = timeline.getPeriodCount() - deferredTimeline.getPeriodCount();
if (windowOffsetUpdate != 0 || periodOffsetUpdate != 0) {
correctOffsets(
mediaSourceHolder.childIndex + 1,
/* childIndexUpdate= */ 0,
windowOffsetUpdate,
periodOffsetUpdate);
}
mediaSourceHolder.timeline = deferredTimeline.cloneWithNewTimeline(timeline);
if (!mediaSourceHolder.isPrepared) {
for (int i = deferredMediaPeriods.size() - 1; i >= 0; i--) {
if (deferredMediaPeriods.get(i).mediaSource == mediaSourceHolder.mediaSource) {
deferredMediaPeriods.get(i).createPeriod();
deferredMediaPeriods.remove(i);
}
}
}
mediaSourceHolder.isPrepared = true;
scheduleListenerNotification(/* actionOnCompletion= */ null);
}
private void removeMediaSourceInternal(int index) {
MediaSourceHolder holder = mediaSourceHolders.get(index);
mediaSourceHolders.remove(index);
Timeline oldTimeline = holder.timeline;
correctOffsets(
index,
/* childIndexUpdate= */ -1,
-oldTimeline.getWindowCount(),
-oldTimeline.getPeriodCount());
holder.isRemoved = true;
if (holder.activeMediaPeriods == 0) {
releaseChildSource(holder);
}
}
private void moveMediaSourceInternal(int currentIndex, int newIndex) {
int startIndex = Math.min(currentIndex, newIndex);
int endIndex = Math.max(currentIndex, newIndex);
int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild;
int periodOffset = mediaSourceHolders.get(startIndex).firstPeriodIndexInChild;
mediaSourceHolders.add(newIndex, mediaSourceHolders.remove(currentIndex));
for (int i = startIndex; i <= endIndex; i++) {
MediaSourceHolder holder = mediaSourceHolders.get(i);
holder.firstWindowIndexInChild = windowOffset;
holder.firstPeriodIndexInChild = periodOffset;
windowOffset += holder.timeline.getWindowCount();
periodOffset += holder.timeline.getPeriodCount();
}
}
private void correctOffsets(
int startIndex, int childIndexUpdate, int windowOffsetUpdate, int periodOffsetUpdate) {
windowCount += windowOffsetUpdate;
periodCount += periodOffsetUpdate;
for (int i = startIndex; i < mediaSourceHolders.size(); i++) {
mediaSourceHolders.get(i).childIndex += childIndexUpdate;
mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate;
mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate;
}
}
private int findMediaSourceHolderByPeriodIndex(int periodIndex) {
query.firstPeriodIndexInChild = periodIndex;
int index = Collections.binarySearch(mediaSourceHolders, query);
if (index < 0) {
return -index - 2;
}
while (index < mediaSourceHolders.size() - 1
&& mediaSourceHolders.get(index + 1).firstPeriodIndexInChild == periodIndex) {
index++;
}
return index;
}
/** Data class to hold playlist media sources together with meta data needed to process them. */
/* package */ static final class MediaSourceHolder implements Comparable<MediaSourceHolder> {
public final MediaSource mediaSource;
public final int uid;
public DeferredTimeline timeline;
public int childIndex;
public int firstWindowIndexInChild;
public int firstPeriodIndexInChild;
public boolean isPrepared;
public boolean isRemoved;
public int activeMediaPeriods;
public MediaSourceHolder(
MediaSource mediaSource,
DeferredTimeline timeline,
int childIndex,
int window,
int period) {
this.mediaSource = mediaSource;
this.timeline = timeline;
this.childIndex = childIndex;
this.firstWindowIndexInChild = window;
this.firstPeriodIndexInChild = period;
this.uid = System.identityHashCode(this);
}
@Override
public int compareTo(@NonNull MediaSourceHolder other) {
return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild;
}
}
/**
* Can be used to dispatch a runnable on the thread the object was created on.
*/
private static final class EventDispatcher {
public final Handler eventHandler;
public final Runnable runnable;
public EventDispatcher(Runnable runnable) {
this.runnable = runnable;
this.eventHandler = new Handler(Looper.myLooper() != null ? Looper.myLooper()
: Looper.getMainLooper());
}
public void dispatchEvent() {
eventHandler.post(runnable);
}
}
/** Message used to post actions from app thread to playback thread. */
private static final class MessageData<T> {
public final int index;
public final T customData;
public final @Nullable EventDispatcher actionOnCompletion;
public MessageData(int index, T customData, @Nullable Runnable actionOnCompletion) {
this.index = index;
this.actionOnCompletion = actionOnCompletion != null
? new EventDispatcher(actionOnCompletion) : null;
this.customData = customData;
}
}
/**
* Timeline exposing concatenated timelines of playlist media sources.
*/
private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline {
private final int windowCount;
private final int periodCount;
private final int[] firstPeriodInChildIndices;
private final int[] firstWindowInChildIndices;
private final Timeline[] timelines;
private final int[] uids;
private final SparseIntArray childIndexByUid;
public ConcatenatedTimeline(
Collection<MediaSourceHolder> mediaSourceHolders,
int windowCount,
int periodCount,
ShuffleOrder shuffleOrder,
boolean isAtomic) {
super(isAtomic, shuffleOrder);
this.windowCount = windowCount;
this.periodCount = periodCount;
int childCount = mediaSourceHolders.size();
firstPeriodInChildIndices = new int[childCount];
firstWindowInChildIndices = new int[childCount];
timelines = new Timeline[childCount];
uids = new int[childCount];
childIndexByUid = new SparseIntArray();
int index = 0;
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
timelines[index] = mediaSourceHolder.timeline;
firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild;
firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild;
uids[index] = mediaSourceHolder.uid;
childIndexByUid.put(uids[index], index++);
}
}
@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) {
if (!(childUid instanceof Integer)) {
return C.INDEX_UNSET;
}
int index = childIndexByUid.get((int) childUid, -1);
return index == -1 ? 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;
}
}
/**
* Timeline used as placeholder for an unprepared media source. After preparation, a copy of the
* DeferredTimeline is used to keep the originally assigned first period ID.
*/
private static final class DeferredTimeline extends ForwardingTimeline {
private static final Object DUMMY_ID = new Object();
private static final Period period = new Period();
private static final DummyTimeline dummyTimeline = new DummyTimeline();
private final Object replacedId;
public DeferredTimeline() {
this(dummyTimeline, /* replacedId= */ null);
}
private DeferredTimeline(Timeline timeline, Object replacedId) {
super(timeline);
this.replacedId = replacedId;
}
public DeferredTimeline cloneWithNewTimeline(Timeline timeline) {
return new DeferredTimeline(
timeline,
replacedId == null && timeline.getPeriodCount() > 0
? timeline.getPeriod(0, period, true).uid
: replacedId);
}
public Timeline getTimeline() {
return timeline;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
timeline.getPeriod(periodIndex, period, setIds);
if (Util.areEqual(period.uid, replacedId)) {
period.uid = DUMMY_ID;
}
return period;
}
@Override
public int getIndexOfPeriod(Object uid) {
return timeline.getIndexOfPeriod(DUMMY_ID.equals(uid) ? replacedId : uid);
}
}
/** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */
private static final class DummyTimeline extends Timeline {
@Override
public int getWindowCount() {
return 1;
}
@Override
public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) {
// Dynamic window to indicate pending timeline updates.
return window.set(
/* id= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* isSeekable= */ false,
/* isDynamic= */ true,
/* defaultPositionUs= */ 0,
/* durationUs= */ C.TIME_UNSET,
/* firstPeriodIndex= */ 0,
/* lastPeriodIndex= */ 0,
/* positionInFirstPeriodUs= */ 0);
}
@Override
public int getPeriodCount() {
return 1;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
return period.set(
/* id= */ null,
/* uid= */ null,
/* windowIndex= */ 0,
/* durationUs = */ C.TIME_UNSET,
/* positionInWindowUs= */ C.TIME_UNSET);
}
@Override
public int getIndexOfPeriod(Object uid) {
return uid == null ? 0 : C.INDEX_UNSET;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2016 The Android Open Source Project
* Copyright (C) 2017 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.
@ -16,11 +16,15 @@
package com.google.android.exoplayer2.source;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.verify;
import android.os.ConditionVariable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.testutil.DummyMainThread;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
import com.google.android.exoplayer2.testutil.FakeTimeline;
@ -29,8 +33,13 @@ import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@ -39,142 +48,292 @@ import org.robolectric.annotation.Config;
@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})
public final class ConcatenatingMediaSourceTest {
@Test
public void testEmptyConcatenation() throws IOException {
for (boolean atomic : new boolean[] {false, true}) {
Timeline timeline = getConcatenatedTimeline(atomic);
TimelineAsserts.assertEmpty(timeline);
private ConcatenatingMediaSource mediaSource;
private MediaSourceTestRunner testRunner;
timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY);
TimelineAsserts.assertEmpty(timeline);
timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY, Timeline.EMPTY, Timeline.EMPTY);
TimelineAsserts.assertEmpty(timeline);
@Before
public void setUp() throws Exception {
mediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0));
testRunner = new MediaSourceTestRunner(mediaSource, null);
}
@After
public void tearDown() throws Exception {
testRunner.release();
}
@Test
public void testSingleMediaSource() throws IOException {
Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111));
public void testPlaylistChangesAfterPreparation() throws IOException, InterruptedException {
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertEmpty(timeline);
FakeMediaSource[] childSources = createMediaSources(7);
// Add first source.
mediaSource.addMediaSource(childSources[0]);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1);
TimelineAsserts.assertWindowIds(timeline, 111);
TimelineAsserts.assertPeriodCounts(timeline, 3);
for (boolean shuffled : new boolean[] {false, true}) {
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0);
// Add at front of queue.
mediaSource.addMediaSource(0, childSources[1]);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 1);
TimelineAsserts.assertWindowIds(timeline, 222, 111);
// Add at back of queue.
mediaSource.addMediaSource(childSources[2]);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 111, 333);
// Add in the middle.
mediaSource.addMediaSource(1, childSources[3]);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333);
// Add bulk.
mediaSource.addMediaSources(
3, Arrays.<MediaSource>asList(childSources[4], childSources[5], childSources[6]));
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333);
// Move sources.
mediaSource.moveMediaSource(2, 3);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 5, 1, 6, 7, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 444, 555, 111, 666, 777, 333);
mediaSource.moveMediaSource(3, 2);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333);
mediaSource.moveMediaSource(0, 6);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 4, 1, 5, 6, 7, 3, 2);
TimelineAsserts.assertWindowIds(timeline, 444, 111, 555, 666, 777, 333, 222);
mediaSource.moveMediaSource(6, 0);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333);
// Remove in the middle.
mediaSource.removeMediaSource(3);
testRunner.assertTimelineChangeBlocking();
mediaSource.removeMediaSource(3);
testRunner.assertTimelineChangeBlocking();
mediaSource.removeMediaSource(3);
testRunner.assertTimelineChangeBlocking();
mediaSource.removeMediaSource(1);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 111, 333);
for (int i = 3; i <= 6; i++) {
childSources[i].assertReleased();
}
timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111));
TimelineAsserts.assertWindowIds(timeline, 111);
TimelineAsserts.assertPeriodCounts(timeline, 3);
for (boolean shuffled : new boolean[] {false, true}) {
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0);
}
}
@Test
public void testMultipleMediaSources() throws IOException {
Timeline[] timelines = {
createFakeTimeline(3, 111), createFakeTimeline(1, 222), createFakeTimeline(3, 333)
};
Timeline timeline = getConcatenatedTimeline(false, timelines);
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1);
// Assert correct next and previous indices behavior after some insertions and removals.
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0);
timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1);
assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0);
assertThat(timeline.getLastWindowIndex(false)).isEqualTo(timeline.getWindowCount() - 1);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1);
assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0);
assertThat(timeline.getLastWindowIndex(false)).isEqualTo(2);
assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0);
assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(timeline.getWindowCount() - 1);
assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0);
timeline = getConcatenatedTimeline(true, timelines);
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3);
for (boolean shuffled : new boolean[] {false, true}) {
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ONE, shuffled, 2, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1);
// Assert all periods can be prepared.
testRunner.assertPrepareAndReleaseAllPeriods();
// Remove at front of queue.
mediaSource.removeMediaSource(0);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 3);
TimelineAsserts.assertWindowIds(timeline, 111, 333);
childSources[1].assertReleased();
// Remove at back of queue.
mediaSource.removeMediaSource(1);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1);
TimelineAsserts.assertWindowIds(timeline, 111);
childSources[2].assertReleased();
// Remove last source.
mediaSource.removeMediaSource(0);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertEmpty(timeline);
childSources[3].assertReleased();
}
@Test
public void testPlaylistChangesBeforePreparation() throws IOException, InterruptedException {
FakeMediaSource[] childSources = createMediaSources(4);
mediaSource.addMediaSource(childSources[0]);
mediaSource.addMediaSource(childSources[1]);
mediaSource.addMediaSource(0, childSources[2]);
mediaSource.moveMediaSource(0, 2);
mediaSource.removeMediaSource(0);
mediaSource.moveMediaSource(1, 0);
mediaSource.addMediaSource(1, childSources[3]);
testRunner.assertNoTimelineChange();
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2);
TimelineAsserts.assertWindowIds(timeline, 333, 444, 222);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0);
assertThat(timeline.getFirstWindowIndex(shuffled)).isEqualTo(0);
assertThat(timeline.getLastWindowIndex(shuffled)).isEqualTo(2);
timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET);
testRunner.assertPrepareAndReleaseAllPeriods();
testRunner.releaseSource();
for (int i = 1; i < 4; i++) {
childSources[i].assertReleased();
}
}
@Test
public void testNestedMediaSources() throws IOException {
Timeline timeline =
getConcatenatedTimeline(
false,
getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)),
getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444)));
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444);
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ALL, false, 3, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, false, 1, 2, 3, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 3, 0);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, true, 1, 3, C.INDEX_UNSET, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 3, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 3, 0, 2);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 3, 1);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 3, 2);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1);
public void testPlaylistWithLazyMediaSource() throws IOException, InterruptedException {
// Create some normal (immediately preparing) sources and some lazy sources whose timeline
// updates need to be triggered.
FakeMediaSource[] fastSources = createMediaSources(2);
final FakeMediaSource[] lazySources = new FakeMediaSource[4];
for (int i = 0; i < 4; i++) {
lazySources[i] = new FakeMediaSource(null, null);
}
// Add lazy sources and normal sources before preparation. Also remove one lazy source again
// before preparation to check it doesn't throw or change the result.
mediaSource.addMediaSource(lazySources[0]);
mediaSource.addMediaSource(0, fastSources[0]);
mediaSource.removeMediaSource(1);
mediaSource.addMediaSource(1, lazySources[1]);
testRunner.assertNoTimelineChange();
// Prepare and assert that the timeline contains all information for normal sources while having
// placeholder information for lazy sources.
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1);
TimelineAsserts.assertWindowIds(timeline, 111, null);
TimelineAsserts.assertWindowIsDynamic(timeline, false, true);
// Trigger source info refresh for lazy source and check that the timeline now contains all
// information for all windows.
testRunner.runOnPlaybackThread(
new Runnable() {
@Override
public void run() {
lazySources[1].setNewSourceInfo(createFakeTimeline(8), null);
}
});
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 9);
TimelineAsserts.assertWindowIds(timeline, 111, 999);
TimelineAsserts.assertWindowIsDynamic(timeline, false, false);
testRunner.assertPrepareAndReleaseAllPeriods();
// Add further lazy and normal sources after preparation. Also remove one lazy source again to
// check it doesn't throw or change the result.
mediaSource.addMediaSource(1, lazySources[2]);
testRunner.assertTimelineChangeBlocking();
mediaSource.addMediaSource(2, fastSources[1]);
testRunner.assertTimelineChangeBlocking();
mediaSource.addMediaSource(0, lazySources[3]);
testRunner.assertTimelineChangeBlocking();
mediaSource.removeMediaSource(2);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9);
TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999);
TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false);
// Create a period from an unprepared lazy media source and assert Callback.onPrepared is not
// called yet.
MediaPeriod lazyPeriod =
testRunner.createPeriod(
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0));
CountDownLatch preparedCondition = testRunner.preparePeriod(lazyPeriod, 0);
assertThat(preparedCondition.getCount()).isEqualTo(1);
// Assert that a second period can also be created and released without problems.
MediaPeriod secondLazyPeriod =
testRunner.createPeriod(
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0));
testRunner.releasePeriod(secondLazyPeriod);
// Trigger source info refresh for lazy media source. Assert that now all information is
// available again and the previously created period now also finished preparing.
testRunner.runOnPlaybackThread(
new Runnable() {
@Override
public void run() {
lazySources[3].setNewSourceInfo(createFakeTimeline(7), null);
}
});
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9);
TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999);
TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false);
assertThat(preparedCondition.getCount()).isEqualTo(0);
// Release the period and source.
testRunner.releasePeriod(lazyPeriod);
testRunner.releaseSource();
// Assert all sources were fully released.
for (FakeMediaSource fastSource : fastSources) {
fastSource.assertReleased();
}
for (FakeMediaSource lazySource : lazySources) {
lazySource.assertReleased();
}
}
@Test
public void testEmptyTimelineMediaSources() throws IOException {
// Empty timelines in the front, back, and the middle (single and multiple in a row).
Timeline[] timelines = {
Timeline.EMPTY,
createFakeTimeline(1, 111),
Timeline.EMPTY,
Timeline.EMPTY,
createFakeTimeline(2, 222),
Timeline.EMPTY,
createFakeTimeline(3, 333),
Timeline.EMPTY
};
Timeline timeline = getConcatenatedTimeline(false, timelines);
public void testEmptyTimelineMediaSource() throws IOException, InterruptedException {
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertEmpty(timeline);
mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null));
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertEmpty(timeline);
mediaSource.addMediaSources(
Arrays.asList(
new MediaSource[] {
new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null),
new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null),
new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null)
}));
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertEmpty(timeline);
// Insert non-empty media source to leave empty sources at the start, the end, and the middle
// (with single and multiple empty sources in a row).
MediaSource[] mediaSources = createMediaSources(3);
mediaSource.addMediaSource(1, mediaSources[0]);
testRunner.assertTimelineChangeBlocking();
mediaSource.addMediaSource(4, mediaSources[1]);
testRunner.assertTimelineChangeBlocking();
mediaSource.addMediaSource(6, mediaSources[2]);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3);
TimelineAsserts.assertPreviousWindowIndices(
@ -197,29 +356,263 @@ public final class ConcatenatingMediaSourceTest {
assertThat(timeline.getLastWindowIndex(false)).isEqualTo(2);
assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2);
assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0);
testRunner.assertPrepareAndReleaseAllPeriods();
}
timeline = getConcatenatedTimeline(true, timelines);
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3);
for (boolean shuffled : new boolean[] {false, true}) {
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ONE, shuffled, 2, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0);
assertThat(timeline.getFirstWindowIndex(shuffled)).isEqualTo(0);
assertThat(timeline.getLastWindowIndex(shuffled)).isEqualTo(2);
@Test
public void testDynamicChangeOfEmptyTimelines() throws IOException {
FakeMediaSource[] childSources =
new FakeMediaSource[] {
new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null),
new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null),
new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null),
};
Timeline nonEmptyTimeline = new FakeTimeline(/* windowCount = */ 1);
mediaSource.addMediaSources(Arrays.<MediaSource>asList(childSources));
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertEmpty(timeline);
childSources[0].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1);
childSources[2].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1);
childSources[1].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1);
}
@Test
public void testIllegalArguments() {
MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null);
// Null sources.
try {
mediaSource.addMediaSource(null);
fail("Null mediaSource not allowed.");
} catch (NullPointerException e) {
// Expected.
}
MediaSource[] mediaSources = {validSource, null};
try {
mediaSource.addMediaSources(Arrays.asList(mediaSources));
fail("Null mediaSource not allowed.");
} catch (NullPointerException e) {
// Expected.
}
}
@Test
public void testCustomCallbackBeforePreparationAddSingle() {
Runnable runnable = Mockito.mock(Runnable.class);
mediaSource.addMediaSource(createFakeMediaSource(), runnable);
verify(runnable).run();
}
@Test
public void testCustomCallbackBeforePreparationAddMultiple() {
Runnable runnable = Mockito.mock(Runnable.class);
mediaSource.addMediaSources(
Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}),
runnable);
verify(runnable).run();
}
@Test
public void testCustomCallbackBeforePreparationAddSingleWithIndex() {
Runnable runnable = Mockito.mock(Runnable.class);
mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable);
verify(runnable).run();
}
@Test
public void testCustomCallbackBeforePreparationAddMultipleWithIndex() {
Runnable runnable = Mockito.mock(Runnable.class);
mediaSource.addMediaSources(
/* index */ 0,
Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}),
runnable);
verify(runnable).run();
}
@Test
public void testCustomCallbackBeforePreparationRemove() {
Runnable runnable = Mockito.mock(Runnable.class);
mediaSource.addMediaSource(createFakeMediaSource());
mediaSource.removeMediaSource(/* index */ 0, runnable);
verify(runnable).run();
}
@Test
public void testCustomCallbackBeforePreparationMove() {
Runnable runnable = Mockito.mock(Runnable.class);
mediaSource.addMediaSources(
Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}));
mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable);
verify(runnable).run();
}
@Test
public void testCustomCallbackAfterPreparationAddSingle() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber);
}
});
Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();
assertThat(timeline.getWindowCount()).isEqualTo(1);
} finally {
dummyMainThread.release();
}
}
@Test
public void testCustomCallbackAfterPreparationAddMultiple() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.addMediaSources(
Arrays.asList(
new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}),
timelineGrabber);
}
});
Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();
assertThat(timeline.getWindowCount()).isEqualTo(2);
} finally {
dummyMainThread.release();
}
}
@Test
public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber);
}
});
Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();
assertThat(timeline.getWindowCount()).isEqualTo(1);
} finally {
dummyMainThread.release();
}
}
@Test
public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.addMediaSources(
/* index */ 0,
Arrays.asList(
new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}),
timelineGrabber);
}
});
Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();
assertThat(timeline.getWindowCount()).isEqualTo(2);
} finally {
dummyMainThread.release();
}
}
@Test
public void testCustomCallbackAfterPreparationRemove() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.addMediaSource(createFakeMediaSource());
}
});
testRunner.assertTimelineChangeBlocking();
final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.removeMediaSource(/* index */ 0, timelineGrabber);
}
});
Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();
assertThat(timeline.getWindowCount()).isEqualTo(0);
} finally {
dummyMainThread.release();
}
}
@Test
public void testCustomCallbackAfterPreparationMove() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.addMediaSources(
Arrays.asList(
new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}));
}
});
testRunner.assertTimelineChangeBlocking();
final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, timelineGrabber);
}
});
Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();
assertThat(timeline.getWindowCount()).isEqualTo(2);
} finally {
dummyMainThread.release();
}
}
@Test
public void testPeriodCreationWithAds() throws IOException, InterruptedException {
// Create media source with ad child source.
// Create concatenated media source with ad child source.
Timeline timelineContentOnly =
new FakeTimeline(
new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND));
@ -235,12 +628,12 @@ public final class ConcatenatingMediaSourceTest {
/* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0)));
FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null);
FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null);
ConcatenatingMediaSource mediaSource =
new ConcatenatingMediaSource(mediaSourceContentOnly, mediaSourceWithAds);
mediaSource.addMediaSource(mediaSourceContentOnly);
mediaSource.addMediaSource(mediaSourceWithAds);
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
try {
Timeline timeline = testRunner.prepareSource();
// Assert the timeline contains ad groups.
TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1);
// Create all periods and assert period creation of child media sources has been called.
@ -265,52 +658,155 @@ public final class ConcatenatingMediaSourceTest {
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
/* windowSequenceNumber= */ 1));
} finally {
testRunner.release();
}
@Test
public void testAtomicTimelineWindowOrder() throws IOException {
// Release default test runner with non-atomic media source and replace with new test runner.
testRunner.release();
ConcatenatingMediaSource mediaSource =
new ConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0));
testRunner = new MediaSourceTestRunner(mediaSource, null);
mediaSource.addMediaSources(Arrays.<MediaSource>asList(createMediaSources(3)));
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 2, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 2, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 2, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 1);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 1, 2, 0);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 1, 2, 0);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 0);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 2, 0);
assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0);
assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0);
assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(2);
assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2);
}
@Test
public void testNestedTimeline() throws IOException {
ConcatenatingMediaSource nestedSource1 =
new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0));
ConcatenatingMediaSource nestedSource2 =
new ConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0));
mediaSource.addMediaSource(nestedSource1);
mediaSource.addMediaSource(nestedSource2);
testRunner.prepareSource();
FakeMediaSource[] childSources = createMediaSources(4);
nestedSource1.addMediaSource(childSources[0]);
testRunner.assertTimelineChangeBlocking();
nestedSource1.addMediaSource(childSources[1]);
testRunner.assertTimelineChangeBlocking();
nestedSource2.addMediaSource(childSources[2]);
testRunner.assertTimelineChangeBlocking();
nestedSource2.addMediaSource(childSources[3]);
Timeline timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444);
TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3, 4);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 3, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, 3, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 3, 0);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 3, C.INDEX_UNSET, 2);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 3, 0, 2);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 3, 1);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 3, 1);
}
@Test
public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException {
FakeMediaSource childSource = createFakeMediaSource();
mediaSource.addMediaSource(childSource);
testRunner.prepareSource();
MediaPeriod mediaPeriod =
testRunner.createPeriod(
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0));
mediaSource.removeMediaSource(/* index= */ 0);
testRunner.assertTimelineChangeBlocking();
testRunner.releasePeriod(mediaPeriod);
childSource.assertReleased();
testRunner.releaseSource();
}
@Test
public void testDuplicateMediaSources() throws IOException, InterruptedException {
FakeMediaSource childSource =
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2), /* manifest= */ null);
ConcatenatingMediaSource mediaSource =
new ConcatenatingMediaSource(childSource, childSource, childSource);
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
try {
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1);
mediaSource.addMediaSource(childSource);
mediaSource.addMediaSource(childSource);
testRunner.prepareSource();
mediaSource.addMediaSources(Arrays.<MediaSource>asList(childSource, childSource));
Timeline timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1);
testRunner.assertPrepareAndReleaseAllPeriods();
assertThat(childSource.getCreatedMediaPeriods())
.containsAllOf(
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0),
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2),
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4),
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 6),
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1),
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 3),
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 5));
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 5),
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 7));
testRunner.releaseSource();
childSource.assertReleased();
} finally {
testRunner.release();
}
}
@Test
public void testDuplicateNestedMediaSources() throws IOException, InterruptedException {
FakeMediaSource childSource =
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null);
ConcatenatingMediaSource nestedConcatenation =
new ConcatenatingMediaSource(childSource, childSource);
ConcatenatingMediaSource mediaSource =
new ConcatenatingMediaSource(childSource, nestedConcatenation, nestedConcatenation);
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
try {
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1);
ConcatenatingMediaSource nestedConcatenation = new ConcatenatingMediaSource();
testRunner.prepareSource();
mediaSource.addMediaSources(
Arrays.<MediaSource>asList(childSource, nestedConcatenation, nestedConcatenation));
testRunner.assertTimelineChangeBlocking();
nestedConcatenation.addMediaSource(childSource);
testRunner.assertTimelineChangeBlocking();
nestedConcatenation.addMediaSource(childSource);
Timeline timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1);
testRunner.assertPrepareAndReleaseAllPeriods();
assertThat(childSource.getCreatedMediaPeriods())
.containsAllOf(
@ -322,38 +818,53 @@ public final class ConcatenatingMediaSourceTest {
testRunner.releaseSource();
childSource.assertReleased();
} finally {
testRunner.release();
}
}
/**
* Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns the
* concatenated timeline.
*/
private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic, Timeline... timelines)
throws IOException {
FakeMediaSource[] mediaSources = new FakeMediaSource[timelines.length];
for (int i = 0; i < timelines.length; i++) {
mediaSources[i] = new FakeMediaSource(timelines[i], null);
private static FakeMediaSource[] createMediaSources(int count) {
FakeMediaSource[] sources = new FakeMediaSource[count];
for (int i = 0; i < count; i++) {
sources[i] = new FakeMediaSource(createFakeTimeline(i), null);
}
ConcatenatingMediaSource mediaSource =
new ConcatenatingMediaSource(
isRepeatOneAtomic, new FakeShuffleOrder(mediaSources.length), mediaSources);
MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
return sources;
}
private static FakeMediaSource createFakeMediaSource() {
return new FakeMediaSource(createFakeTimeline(/* index */ 0), null);
}
private static FakeTimeline createFakeTimeline(int index) {
return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111));
}
private static final class TimelineGrabber implements Runnable {
private final MediaSourceTestRunner testRunner;
private final ConditionVariable finishedCondition;
private Timeline timeline;
private AssertionError error;
public TimelineGrabber(MediaSourceTestRunner testRunner) {
this.testRunner = testRunner;
finishedCondition = new ConditionVariable();
}
@Override
public void run() {
try {
Timeline timeline = testRunner.prepareSource();
testRunner.releaseSource();
for (int i = 0; i < mediaSources.length; i++) {
mediaSources[i].assertReleased();
timeline = testRunner.assertTimelineChange();
} catch (AssertionError e) {
error = e;
}
finishedCondition.open();
}
public Timeline assertTimelineChangeBlocking() {
assertThat(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue();
if (error != null) {
throw error;
}
return timeline;
} finally {
testRunner.release();
}
}
private static FakeTimeline createFakeTimeline(int periodCount, int windowId) {
return new FakeTimeline(new TimelineWindowDefinition(periodCount, windowId));
}
}

View File

@ -1,871 +0,0 @@
/*
* Copyright (C) 2017 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.source;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.verify;
import android.os.ConditionVariable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.testutil.DummyMainThread;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
import com.google.android.exoplayer2.testutil.RobolectricUtil;
import com.google.android.exoplayer2.testutil.TimelineAsserts;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/** Unit tests for {@link DynamicConcatenatingMediaSource} */
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})
public final class DynamicConcatenatingMediaSourceTest {
private DynamicConcatenatingMediaSource mediaSource;
private MediaSourceTestRunner testRunner;
@Before
public void setUp() throws Exception {
mediaSource =
new DynamicConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0));
testRunner = new MediaSourceTestRunner(mediaSource, null);
}
@After
public void tearDown() throws Exception {
testRunner.release();
}
@Test
public void testPlaylistChangesAfterPreparation() throws IOException, InterruptedException {
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertEmpty(timeline);
FakeMediaSource[] childSources = createMediaSources(7);
// Add first source.
mediaSource.addMediaSource(childSources[0]);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1);
TimelineAsserts.assertWindowIds(timeline, 111);
// Add at front of queue.
mediaSource.addMediaSource(0, childSources[1]);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 1);
TimelineAsserts.assertWindowIds(timeline, 222, 111);
// Add at back of queue.
mediaSource.addMediaSource(childSources[2]);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 111, 333);
// Add in the middle.
mediaSource.addMediaSource(1, childSources[3]);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333);
// Add bulk.
mediaSource.addMediaSources(
3, Arrays.<MediaSource>asList(childSources[4], childSources[5], childSources[6]));
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333);
// Move sources.
mediaSource.moveMediaSource(2, 3);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 5, 1, 6, 7, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 444, 555, 111, 666, 777, 333);
mediaSource.moveMediaSource(3, 2);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333);
mediaSource.moveMediaSource(0, 6);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 4, 1, 5, 6, 7, 3, 2);
TimelineAsserts.assertWindowIds(timeline, 444, 111, 555, 666, 777, 333, 222);
mediaSource.moveMediaSource(6, 0);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333);
// Remove in the middle.
mediaSource.removeMediaSource(3);
testRunner.assertTimelineChangeBlocking();
mediaSource.removeMediaSource(3);
testRunner.assertTimelineChangeBlocking();
mediaSource.removeMediaSource(3);
testRunner.assertTimelineChangeBlocking();
mediaSource.removeMediaSource(1);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3);
TimelineAsserts.assertWindowIds(timeline, 222, 111, 333);
for (int i = 3; i <= 6; i++) {
childSources[i].assertReleased();
}
// Assert correct next and previous indices behavior after some insertions and removals.
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1);
assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0);
assertThat(timeline.getLastWindowIndex(false)).isEqualTo(timeline.getWindowCount() - 1);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0);
assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(timeline.getWindowCount() - 1);
assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0);
// Assert all periods can be prepared.
testRunner.assertPrepareAndReleaseAllPeriods();
// Remove at front of queue.
mediaSource.removeMediaSource(0);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 3);
TimelineAsserts.assertWindowIds(timeline, 111, 333);
childSources[1].assertReleased();
// Remove at back of queue.
mediaSource.removeMediaSource(1);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1);
TimelineAsserts.assertWindowIds(timeline, 111);
childSources[2].assertReleased();
// Remove last source.
mediaSource.removeMediaSource(0);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertEmpty(timeline);
childSources[3].assertReleased();
}
@Test
public void testPlaylistChangesBeforePreparation() throws IOException, InterruptedException {
FakeMediaSource[] childSources = createMediaSources(4);
mediaSource.addMediaSource(childSources[0]);
mediaSource.addMediaSource(childSources[1]);
mediaSource.addMediaSource(0, childSources[2]);
mediaSource.moveMediaSource(0, 2);
mediaSource.removeMediaSource(0);
mediaSource.moveMediaSource(1, 0);
mediaSource.addMediaSource(1, childSources[3]);
testRunner.assertNoTimelineChange();
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2);
TimelineAsserts.assertWindowIds(timeline, 333, 444, 222);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET);
testRunner.assertPrepareAndReleaseAllPeriods();
testRunner.releaseSource();
for (int i = 1; i < 4; i++) {
childSources[i].assertReleased();
}
}
@Test
public void testPlaylistWithLazyMediaSource() throws IOException, InterruptedException {
// Create some normal (immediately preparing) sources and some lazy sources whose timeline
// updates need to be triggered.
FakeMediaSource[] fastSources = createMediaSources(2);
final FakeMediaSource[] lazySources = new FakeMediaSource[4];
for (int i = 0; i < 4; i++) {
lazySources[i] = new FakeMediaSource(null, null);
}
// Add lazy sources and normal sources before preparation. Also remove one lazy source again
// before preparation to check it doesn't throw or change the result.
mediaSource.addMediaSource(lazySources[0]);
mediaSource.addMediaSource(0, fastSources[0]);
mediaSource.removeMediaSource(1);
mediaSource.addMediaSource(1, lazySources[1]);
testRunner.assertNoTimelineChange();
// Prepare and assert that the timeline contains all information for normal sources while having
// placeholder information for lazy sources.
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1);
TimelineAsserts.assertWindowIds(timeline, 111, null);
TimelineAsserts.assertWindowIsDynamic(timeline, false, true);
// Trigger source info refresh for lazy source and check that the timeline now contains all
// information for all windows.
testRunner.runOnPlaybackThread(
new Runnable() {
@Override
public void run() {
lazySources[1].setNewSourceInfo(createFakeTimeline(8), null);
}
});
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 9);
TimelineAsserts.assertWindowIds(timeline, 111, 999);
TimelineAsserts.assertWindowIsDynamic(timeline, false, false);
testRunner.assertPrepareAndReleaseAllPeriods();
// Add further lazy and normal sources after preparation. Also remove one lazy source again to
// check it doesn't throw or change the result.
mediaSource.addMediaSource(1, lazySources[2]);
testRunner.assertTimelineChangeBlocking();
mediaSource.addMediaSource(2, fastSources[1]);
testRunner.assertTimelineChangeBlocking();
mediaSource.addMediaSource(0, lazySources[3]);
testRunner.assertTimelineChangeBlocking();
mediaSource.removeMediaSource(2);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9);
TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999);
TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false);
// Create a period from an unprepared lazy media source and assert Callback.onPrepared is not
// called yet.
MediaPeriod lazyPeriod =
testRunner.createPeriod(
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0));
CountDownLatch preparedCondition = testRunner.preparePeriod(lazyPeriod, 0);
assertThat(preparedCondition.getCount()).isEqualTo(1);
// Assert that a second period can also be created and released without problems.
MediaPeriod secondLazyPeriod =
testRunner.createPeriod(
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0));
testRunner.releasePeriod(secondLazyPeriod);
// Trigger source info refresh for lazy media source. Assert that now all information is
// available again and the previously created period now also finished preparing.
testRunner.runOnPlaybackThread(
new Runnable() {
@Override
public void run() {
lazySources[3].setNewSourceInfo(createFakeTimeline(7), null);
}
});
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9);
TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999);
TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false);
assertThat(preparedCondition.getCount()).isEqualTo(0);
// Release the period and source.
testRunner.releasePeriod(lazyPeriod);
testRunner.releaseSource();
// Assert all sources were fully released.
for (FakeMediaSource fastSource : fastSources) {
fastSource.assertReleased();
}
for (FakeMediaSource lazySource : lazySources) {
lazySource.assertReleased();
}
}
@Test
public void testEmptyTimelineMediaSource() throws IOException, InterruptedException {
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertEmpty(timeline);
mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null));
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertEmpty(timeline);
mediaSource.addMediaSources(
Arrays.asList(
new MediaSource[] {
new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null),
new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null),
new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null)
}));
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertEmpty(timeline);
// Insert non-empty media source to leave empty sources at the start, the end, and the middle
// (with single and multiple empty sources in a row).
MediaSource[] mediaSources = createMediaSources(3);
mediaSource.addMediaSource(1, mediaSources[0]);
testRunner.assertTimelineChangeBlocking();
mediaSource.addMediaSource(4, mediaSources[1]);
testRunner.assertTimelineChangeBlocking();
mediaSource.addMediaSource(6, mediaSources[2]);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1);
assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0);
assertThat(timeline.getLastWindowIndex(false)).isEqualTo(2);
assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2);
assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0);
testRunner.assertPrepareAndReleaseAllPeriods();
}
@Test
public void testDynamicChangeOfEmptyTimelines() throws IOException {
FakeMediaSource[] childSources =
new FakeMediaSource[] {
new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null),
new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null),
new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null),
};
Timeline nonEmptyTimeline = new FakeTimeline(/* windowCount = */ 1);
mediaSource.addMediaSources(Arrays.<MediaSource>asList(childSources));
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertEmpty(timeline);
childSources[0].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1);
childSources[2].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1);
childSources[1].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null);
timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1);
}
@Test
public void testIllegalArguments() {
MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null);
// Null sources.
try {
mediaSource.addMediaSource(null);
fail("Null mediaSource not allowed.");
} catch (NullPointerException e) {
// Expected.
}
MediaSource[] mediaSources = {validSource, null};
try {
mediaSource.addMediaSources(Arrays.asList(mediaSources));
fail("Null mediaSource not allowed.");
} catch (NullPointerException e) {
// Expected.
}
}
@Test
public void testCustomCallbackBeforePreparationAddSingle() {
Runnable runnable = Mockito.mock(Runnable.class);
mediaSource.addMediaSource(createFakeMediaSource(), runnable);
verify(runnable).run();
}
@Test
public void testCustomCallbackBeforePreparationAddMultiple() {
Runnable runnable = Mockito.mock(Runnable.class);
mediaSource.addMediaSources(
Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}),
runnable);
verify(runnable).run();
}
@Test
public void testCustomCallbackBeforePreparationAddSingleWithIndex() {
Runnable runnable = Mockito.mock(Runnable.class);
mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable);
verify(runnable).run();
}
@Test
public void testCustomCallbackBeforePreparationAddMultipleWithIndex() {
Runnable runnable = Mockito.mock(Runnable.class);
mediaSource.addMediaSources(
/* index */ 0,
Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}),
runnable);
verify(runnable).run();
}
@Test
public void testCustomCallbackBeforePreparationRemove() {
Runnable runnable = Mockito.mock(Runnable.class);
mediaSource.addMediaSource(createFakeMediaSource());
mediaSource.removeMediaSource(/* index */ 0, runnable);
verify(runnable).run();
}
@Test
public void testCustomCallbackBeforePreparationMove() {
Runnable runnable = Mockito.mock(Runnable.class);
mediaSource.addMediaSources(
Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}));
mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable);
verify(runnable).run();
}
@Test
public void testCustomCallbackAfterPreparationAddSingle() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber);
}
});
Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();
assertThat(timeline.getWindowCount()).isEqualTo(1);
} finally {
dummyMainThread.release();
}
}
@Test
public void testCustomCallbackAfterPreparationAddMultiple() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.addMediaSources(
Arrays.asList(
new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}),
timelineGrabber);
}
});
Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();
assertThat(timeline.getWindowCount()).isEqualTo(2);
} finally {
dummyMainThread.release();
}
}
@Test
public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber);
}
});
Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();
assertThat(timeline.getWindowCount()).isEqualTo(1);
} finally {
dummyMainThread.release();
}
}
@Test
public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.addMediaSources(
/* index */ 0,
Arrays.asList(
new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}),
timelineGrabber);
}
});
Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();
assertThat(timeline.getWindowCount()).isEqualTo(2);
} finally {
dummyMainThread.release();
}
}
@Test
public void testCustomCallbackAfterPreparationRemove() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.addMediaSource(createFakeMediaSource());
}
});
testRunner.assertTimelineChangeBlocking();
final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.removeMediaSource(/* index */ 0, timelineGrabber);
}
});
Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();
assertThat(timeline.getWindowCount()).isEqualTo(0);
} finally {
dummyMainThread.release();
}
}
@Test
public void testCustomCallbackAfterPreparationMove() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
try {
testRunner.prepareSource();
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.addMediaSources(
Arrays.asList(
new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}));
}
});
testRunner.assertTimelineChangeBlocking();
final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner);
dummyMainThread.runOnMainThread(
new Runnable() {
@Override
public void run() {
mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, timelineGrabber);
}
});
Timeline timeline = timelineGrabber.assertTimelineChangeBlocking();
assertThat(timeline.getWindowCount()).isEqualTo(2);
} finally {
dummyMainThread.release();
}
}
@Test
public void testPeriodCreationWithAds() throws IOException, InterruptedException {
// Create dynamic media source with ad child source.
Timeline timelineContentOnly =
new FakeTimeline(
new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND));
Timeline timelineWithAds =
new FakeTimeline(
new TimelineWindowDefinition(
2,
222,
true,
false,
10 * C.MICROS_PER_SECOND,
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0)));
FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null);
FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null);
mediaSource.addMediaSource(mediaSourceContentOnly);
mediaSource.addMediaSource(mediaSourceWithAds);
Timeline timeline = testRunner.prepareSource();
// Assert the timeline contains ad groups.
TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1);
// Create all periods and assert period creation of child media sources has been called.
testRunner.assertPrepareAndReleaseAllPeriods();
mediaSourceContentOnly.assertMediaPeriodCreated(
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0));
mediaSourceContentOnly.assertMediaPeriodCreated(
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0));
mediaSourceWithAds.assertMediaPeriodCreated(
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1));
mediaSourceWithAds.assertMediaPeriodCreated(
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1));
mediaSourceWithAds.assertMediaPeriodCreated(
new MediaPeriodId(
/* periodIndex= */ 0,
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
/* windowSequenceNumber= */ 1));
mediaSourceWithAds.assertMediaPeriodCreated(
new MediaPeriodId(
/* periodIndex= */ 1,
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
/* windowSequenceNumber= */ 1));
}
@Test
public void testAtomicTimelineWindowOrder() throws IOException {
// Release default test runner with non-atomic media source and replace with new test runner.
testRunner.release();
DynamicConcatenatingMediaSource mediaSource =
new DynamicConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0));
testRunner = new MediaSourceTestRunner(mediaSource, null);
mediaSource.addMediaSources(Arrays.<MediaSource>asList(createMediaSources(3)));
Timeline timeline = testRunner.prepareSource();
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 2, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 2, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 2, 0, 1);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 1);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 2, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 1, 2, 0);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 1, 2, 0);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 0);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 2, 0);
assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0);
assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0);
assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(2);
assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2);
}
@Test
public void testNestedTimeline() throws IOException {
DynamicConcatenatingMediaSource nestedSource1 =
new DynamicConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0));
DynamicConcatenatingMediaSource nestedSource2 =
new DynamicConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0));
mediaSource.addMediaSource(nestedSource1);
mediaSource.addMediaSource(nestedSource2);
testRunner.prepareSource();
FakeMediaSource[] childSources = createMediaSources(4);
nestedSource1.addMediaSource(childSources[0]);
testRunner.assertTimelineChangeBlocking();
nestedSource1.addMediaSource(childSources[1]);
testRunner.assertTimelineChangeBlocking();
nestedSource2.addMediaSource(childSources[2]);
testRunner.assertTimelineChangeBlocking();
nestedSource2.addMediaSource(childSources[3]);
Timeline timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444);
TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3, 4);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1, 2);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 3, 0, 1, 2);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, 3, C.INDEX_UNSET);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 3, 0);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 3, C.INDEX_UNSET, 2);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2);
TimelineAsserts.assertPreviousWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 3, 0, 2);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 3, 1);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2);
TimelineAsserts.assertNextWindowIndices(
timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 3, 1);
}
@Test
public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException {
FakeMediaSource childSource = createFakeMediaSource();
mediaSource.addMediaSource(childSource);
testRunner.prepareSource();
MediaPeriod mediaPeriod =
testRunner.createPeriod(
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0));
mediaSource.removeMediaSource(/* index= */ 0);
testRunner.assertTimelineChangeBlocking();
testRunner.releasePeriod(mediaPeriod);
childSource.assertReleased();
testRunner.releaseSource();
}
@Test
public void testDuplicateMediaSources() throws IOException, InterruptedException {
FakeMediaSource childSource =
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2), /* manifest= */ null);
mediaSource.addMediaSource(childSource);
mediaSource.addMediaSource(childSource);
testRunner.prepareSource();
mediaSource.addMediaSources(Arrays.<MediaSource>asList(childSource, childSource));
Timeline timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1);
testRunner.assertPrepareAndReleaseAllPeriods();
assertThat(childSource.getCreatedMediaPeriods())
.containsAllOf(
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0),
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2),
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4),
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 6),
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1),
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 3),
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 5),
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 7));
testRunner.releaseSource();
childSource.assertReleased();
}
@Test
public void testDuplicateNestedMediaSources() throws IOException, InterruptedException {
FakeMediaSource childSource =
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null);
DynamicConcatenatingMediaSource nestedConcatenation = new DynamicConcatenatingMediaSource();
testRunner.prepareSource();
mediaSource.addMediaSources(
Arrays.<MediaSource>asList(childSource, nestedConcatenation, nestedConcatenation));
testRunner.assertTimelineChangeBlocking();
nestedConcatenation.addMediaSource(childSource);
testRunner.assertTimelineChangeBlocking();
nestedConcatenation.addMediaSource(childSource);
Timeline timeline = testRunner.assertTimelineChangeBlocking();
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1);
testRunner.assertPrepareAndReleaseAllPeriods();
assertThat(childSource.getCreatedMediaPeriods())
.containsAllOf(
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0),
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1),
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2),
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 3),
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4));
testRunner.releaseSource();
childSource.assertReleased();
}
private static FakeMediaSource[] createMediaSources(int count) {
FakeMediaSource[] sources = new FakeMediaSource[count];
for (int i = 0; i < count; i++) {
sources[i] = new FakeMediaSource(createFakeTimeline(i), null);
}
return sources;
}
private static FakeMediaSource createFakeMediaSource() {
return new FakeMediaSource(createFakeTimeline(/* index */ 0), null);
}
private static FakeTimeline createFakeTimeline(int index) {
return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111));
}
private static final class TimelineGrabber implements Runnable {
private final MediaSourceTestRunner testRunner;
private final ConditionVariable finishedCondition;
private Timeline timeline;
private AssertionError error;
public TimelineGrabber(MediaSourceTestRunner testRunner) {
this.testRunner = testRunner;
finishedCondition = new ConditionVariable();
}
@Override
public void run() {
try {
timeline = testRunner.assertTimelineChange();
} catch (AssertionError e) {
error = e;
}
finishedCondition.open();
}
public Timeline assertTimelineChangeBlocking() {
assertThat(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue();
if (error != null) {
throw error;
}
return timeline;
}
}
}