mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
32b4db361f
commit
2f4a3d2e5d
@ -4,12 +4,15 @@
|
||||
|
||||
* 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
|
||||
also in parallel to allow adding them multiple times to a concatenation.
|
||||
([#3498](https://github.com/google/ExoPlayer/issues/3498)).
|
||||
* 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)).
|
||||
* 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)).
|
||||
|
||||
### 2.7.0 ###
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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 <= index <= {@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 <= index <= {@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 <= index <= {@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 <= index <= {@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 <= index < {@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 <= index < {@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 <= index < {@link #getSize()}.
|
||||
* @param newIndex The target index of the media source in the playlist. This index must be in the
|
||||
* range of 0 <= index < {@link #getSize()}.
|
||||
*/
|
||||
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 <= index < {@link #getSize()}.
|
||||
* @param newIndex The target index of the media source in the playlist. This index must be in the
|
||||
* range of 0 <= index < {@link #getSize()}.
|
||||
* @param 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 <= index <= {@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;
|
||||
}
|
||||
}
|
||||
for (Timeline timeline : timelines) {
|
||||
if (timeline == null) {
|
||||
// Don't invoke the listener until all sources have timelines.
|
||||
return;
|
||||
}
|
||||
}
|
||||
timeline = new ConcatenatedTimeline(timelines.clone(), isAtomic, shuffleOrder);
|
||||
refreshSourceInfo(timeline, manifests.clone());
|
||||
Timeline timeline,
|
||||
@Nullable Object manifest) {
|
||||
updateMediaSourceInternal(mediaSourceHolder, timeline);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@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();
|
||||
}
|
||||
return duplicateFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Timeline} that is the concatenation of one or more {@link Timeline}s.
|
||||
*/
|
||||
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[] 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 <= index <= {@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 <= index <= {@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 <= index <= {@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 <= index <= {@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 <= index < {@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 <= index < {@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 <= index < {@link #getSize()}.
|
||||
* @param newIndex The target index of the media source in the playlist. This index must be in the
|
||||
* range of 0 <= index < {@link #getSize()}.
|
||||
*/
|
||||
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 <= index < {@link #getSize()}.
|
||||
* @param newIndex The target index of the media source in the playlist. This index must be in the
|
||||
* range of 0 <= index < {@link #getSize()}.
|
||||
* @param 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 <= index <= {@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;
|
||||
}
|
||||
super(isAtomic, shuffleOrder);
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user